def add_curves(ax, pressure, temperature, mixing_ratio, altitude, linewidth=1.0, LH_Tdepend=False): """ overlaying new curves of multiple soundings from profiles """ 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 energies if LH_Tdepend == False: mse = mpcalc.moist_static_energy(altitude, T, q) mse_s = mpcalc.moist_static_energy(altitude, T, qs) dse = mpcalc.dry_static_energy(altitude, T) else: # A short course in cloud physics, Roger and Yau (1989) Lvt = (2500.8 - 2.36 * T.magnitude + 0.0016 * T.magnitude**2 - 0.00006 * T.magnitude**3) * units( 'joule/gram') # latent heat of evaporation #Lf = 2834.1 - 0.29*T - 0.004*T**2 # latent heat of fusion mse = Cp_d * T + g * altitude + Lvt * q mse_s = Cp_d * T + g * altitude + Lvt * qs dse = mpcalc.dry_static_energy(altitude, T) ax.plot(dse, p, '--k', linewidth=linewidth) ax.plot(mse, p, '--b', linewidth=linewidth) ax.plot(mse_s, p, '--r', linewidth=linewidth)
def birner(pFull, TFull, lapseC=2.0*units("K/km"), height=False): """ Implements the calculation of the thermal tropopause as described by Birner in Appendix A of: T. Birner. Fine-scale structure of the extratropical tropopause region. Journal """ # Calculate the heights of the pressure levels using the hypsometric equation z = np.zeros_like(pFull)*units.km for h in range(0,pFull.size): z[h] = thickness_hydrostatic(pFull[0:h+1],TFull[0:h+1]) # Calculate the lapse rate through use of a centered difference # This needs the heights "z" at each pressure level, which can be calculated # using the hypsometric equation, or sent as a function argument lapse = (TFull[2:]-TFull[:-2])/(z[2:]-z[:-2]) found = False for i in range(3,lapse.size): if lapse[i] >= lapseC: k = i for j in range(k,lapse.size-2): meanAbove = np.mean(lapse[j:j+2]) meanBelow = np.mean(lapse[j-3:j-1]) if meanAbove >= lapseC and meanBelow < lapseC: if meanAbove > 0: minTempIdx = np.argmin(lapse[j-3:j+2]) iTrop = k+minTempIdx zTrop = z[iTrop] TTrop = TFull[iTrop] else: print("linearly interpolated...") minTempIdx = np.argmin(lapse[j-3:j+2]) iTrop = k+minTempIdx # Linearly interpolate and find intersection point ma = (TFull[j+2]-TFull[j])/(z[j+2]-z[j]) mb = (TFull[j-1]-TFull[j-3])/(z[j-1]-z[j-3]) zTrop = (TFull[j]-TFull[j-1]+mb*z[j-1]-ma*z[j])/(mb-ma) TTrop = ma*(zTrop-z[j])+T[j] # Find index of "layer" that has calculated tropopause n = 0 while z[n] < zTrop: iTrop = n found = np.abs(zTrop-z[j]) <= 250*units.meters found = found and pFull[iTrop] < 500*units.mbar i2km = iTrop+1 while z[i2km] - z[iTrop] < 2000*units.meters: found = found and (TFull[i2km]-TTrop)/(z[i2km]-zTrop) > lapseC found = found and (TFull[i2km+1]-TTrop)/(z[i2km+1]-zTrop) > lapseC i2km += 1 if found: break if found: break if height: return zTrop.to(units.km) pTrop = pFull[iTrop] return pTrop.to(units.mbar)
def test_thickness_hydrostatic(): """Test the thickness calculation for a moist layer.""" pressure = np.array([959., 779.2, 751.3, 724.3, 700., 269.]) * units.hPa temperature = np.array([22.2, 14.6, 12., 9.4, 7., -38.]) * units.degC mixing = np.array([0.01458, 0.00209, 0.00224, 0.00240, 0.00256, 0.00010]) thickness = thickness_hydrostatic(pressure, temperature, mixing=mixing) assert_almost_equal(thickness, 9892.07 * units.m, 2)
def test_thickness_hydrostatic_isothermal_subset(): """Test the thickness calculation for a dry isothermal layer subset at 0 degC.""" pressure = np.arange(1000, 500 - 1e-10, -10) * units.hPa temperature = np.zeros_like(pressure) * units.degC thickness = thickness_hydrostatic(pressure, temperature, bottom=850 * units.hPa, depth=350 * units.hPa) assert_almost_equal(thickness, 4242.68 * units.m, 2)
def vertical_wind_shear(u, v): diff_u = xr.zeros_like(u[:, 1:]) diff_u.attrs[ 'long_name'] = 'Horizontal wind U component delta to level below' diff_v = xr.zeros_like(v[:, 1:]) diff_v.attrs[ 'long_name'] = 'Horizontal wind V component delta to level below' diff_u[:, :] = u[:, 1:].values - u[:, :-1].values diff_v[:, :] = v[:, 1:].values - v[:, :-1].values # mpcalc.bulk_shear(ls.lev[1:3].values*units.hPa, ls.u[:1, 1:3].squeeze().values*units('m/s'), # ls.v[:1, 1:3].squeeze().values*units('m/s'), depth=25.*units.hPa) velo_change = np.sqrt(diff_u**2 + diff_v**2) # change of velocity is [m/s], but I want it per meter, which is dwind_dz => [m/s/m] = [1/s] thickness = delta_height(ls.lev[1:], ls.T[:, 1:]) thickness_my = thickness.copy(deep=True) thickness_metpy = thickness.copy(deep=True) thickness_metpy[:, :] = 0 for i in range(len(thickness.lev)): thickness_metpy[:, i] = mpcalc.thickness_hydrostatic( ls.lev[i + 1:i + 3], ls.T[:1, i + 1:i + 3])[0] shear = velo_change #/ thickness_metpy shear.attrs['long_name'] = 'Vertical wind shear' shear.attrs['units'] = 'm/s' return xr.merge([ls, xr.Dataset({'dwind_dz': shear})])
def test_thickness_hydrostatic_subset(): """Test the thickness calculation with a subset of the moist layer.""" pressure = np.array([959., 779.2, 751.3, 724.3, 700., 269.]) * units.hPa temperature = np.array([22.2, 14.6, 12., 9.4, 7., -38.]) * units.degC mixing = np.array([0.01458, 0.00209, 0.00224, 0.00240, 0.00256, 0.00010]) thickness = thickness_hydrostatic(pressure, temperature, mixing=mixing, bottom=850 * units.hPa, depth=150 * units.hPa) assert_almost_equal(thickness, 1630.81 * units.m, 2)
def coldestPoint(pFull, TFull, lapseC=2.0 * units("K/km"), height=False): """ Finds the tropopause as the coldest point in a sounding """ iTrop = np.argmin(TFull) if height: z = thickness_hydrostatic(pFull[0:iTrop], TFull[0:iTrop]) return z.to(units.km) pTrop = pFull[iTrop] return pTrop.to(units.mbar)
def moist_adiabat(z, SST): p = 1000 * np.exp(-9.81 * z / (287. * 270.)) * units.hPa Tp = mpcalc.moist_lapse(p, (SST - 1) * units.K) qp = 0.8 * mpcalc.saturation_mixing_ratio(p, Tp) ztrop1 = 17e3 ztrop2 = 19e3 idx1 = np.argmin((z - ztrop1)**2) idx2 = np.argmin((z - ztrop2)**2) Tp[idx1:idx2] = Tp[idx1] Tp[idx2:] = Tp[idx1] + 2e-3 * (z[idx2:] - ztrop2) * units.K thetap = mpcalc.potential_temperature(p, Tp) thicknesses = [ mpcalc.thickness_hydrostatic(p, Tp, bottom=p[i], depth=p[i] - p[i + 1]) / units.m for i in range(len(p) - 1) ] zp = np.concatenate([[0.], np.cumsum(thicknesses)]) thetaz = np.interp(z, zp, (thetap / units.K)) qz = np.interp(z, zp, qp) idxs = z < 35000 return z[idxs], np.array([float(x) for x in thetaz ])[idxs], np.array([float(x) for x in qz])[idxs]
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 test_thickness_hydrostatic_isothermal(): """Test the thickness calculation for a dry isothermal layer at 0 degC.""" pressure = np.arange(1000, 500 - 1e-10, -10) * units.hPa temperature = np.zeros_like(pressure) * units.degC thickness = thickness_hydrostatic(pressure, temperature) assert_almost_equal(thickness, 5542.12 * units.m, 2)
def wmo(pFull, TFull, lapseC=2.0 * units("K/km"), height=False): """ Implements NCAR's Fortran code in python: https://github.com/NCAR/ncl/blob/develop/ni/src/lib/nfpfort/stattrop_dp.f """ nLev = pFull.size nLevm = nLev - 1 pMin = 85.0 * units.mbar pMax = 450.0 * units.mbar dZ = 2000.0 * units.meters g = earth_gravity R = dry_air_gas_constant const = g / R found = False lapse = np.zeros(nLevm) * units.kelvin / units.km pHalf = np.zeros(nLevm) * units.mbar pTrop = 0 * units.mbar for iLev in range(0, nLevm): lapse[iLev] = const * np.log(TFull[iLev] / TFull[iLev + 1]) / np.log( pFull[iLev] / pFull[iLev + 1]) pHalf[iLev] = (pFull[iLev] + pFull[iLev + 1]) * 0.5 for iLev in range(0, nLevm - 1): if lapse[iLev] < lapseC and pFull[iLev] < pMax and not found: P1 = np.log(pHalf[iLev].magnitude) P2 = np.log(pHalf[iLev + 1].magnitude) if (lapse[iLev] != lapse[iLev + 1]): weight = (lapseC - lapse[iLev]) / (lapse[iLev + 1] - lapse[iLev]) #tropopause pressure pTrop = np.exp(P1 + weight * (P2 - P1)) * units.mbar else: pTrop = pHalf[iLev] p2km = pTrop * np.exp(-dZ * const / TFull[iLev]) lapseAvg = 0 lapseSum = 0 kount = 0 for L in range(iLev, nLevm): if pHalf[L] > p2km: lapseAvg = lapseSum + lapse[L] kount = kount + 1 lapseAvg = lapseSum / kount found = lapseAvg < lapseC if not found: print("Tropopause not found") else: iTrop = iLev pTrop = pMin if pTrop < pMin else pTrop if height: z = thickness_hydrostatic(pFull[0:iTrop], TFull[0:iTrop]) return z.to(units.km) return pTrop.to(units.mbar)
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)