推荐|Python绘制3D动态对流层顶(Tropopause)
- 2020 年 2 月 17 日
- 筆記

作者
Mathew Barlow: Professor of Climate Science University of Massachusetts Lowell
工具
GFS, the nomads server, python, and the python packages numpy, matplotlib, cartopy, scipy, and netcdf4
potential-vorticity: Python code (gfs_pv_1.2.py) for dynamic tropopause (DT) calculations: DT pressure, DT potential temperature (theta), PV on the 330K isentropic surface, and a PV and theta cross-section at the latitude where the tropopause is lowest in the domain. The date and time need to be set in the beginning of the code; the domain can be changed there as well. gfs_pv_1.2_3D.py is the same code but with additional (poor) 3D plots This code has a DOI and is citable: DOI The data source is the online GFS analysis, and the date and time need to be set within the period of available data. The program can take a few minutes to run because it is accessing data over the internet.
代码
https://github.com/mathewbarlow/potential-vorticity
具体参考以上链接
# # run on python 3.7 # # python code for some calculations related to the dynamic tropopause (DT) - # DT pressure, DT potential temperature, 330K PV, # and a cross-section of PV at the latitude where the tropopause is lowest - # all based on the GFS analysis available online. As the data is accessed # online, the program can take a while to run. # # the date and lat-lon range can be set below # # (poorly) coded by Mathew Barlow # initial release: 14 Nov 2017 # last updated: 10 Oct 2019 # # this code has *not* been extensively tested and has been # awkwardly translated from other coding languages, so if you find # any errors or have any suggestions or improvements, including for # the plotting, please let me know at [email protected] . Thanks! # # Support from NSF AGS-1623912 is gratefully acknowledged # import numpy as np import netCDF4 import matplotlib.pyplot as plt import matplotlib.ticker as tick from mpl_toolkits.mplot3d import axes3d import cartopy.crs as ccrs from scipy.ndimage import gaussian_filter from cartopy.feature import NaturalEarthFeature from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER from cartopy.mpl.ticker import LatitudeFormatter, LongitudeFormatter from datetime import datetime # VALUES TO SET ************************************************* # set date, lat-lon range, and PV-value definition of tropopause mydate='20191016' myhour='06' (lat1,lat2)=(20,70) (lon1,lon2)=(120,240.1) tpdef=2 # definition of tropopause in PVU #**************************************************************** #constants re=6.37e6 g=9.81 cp=1004.5 r=2*cp/7 kap=r/cp omega=7.292e-5 pi=3.14159265 # open dataset, retreive variables, close dataset url='https://nomads.ncep.noaa.gov/dods/gfs_0p25/gfs'+ mydate+'/gfs_0p25_'+myhour+'z_anl' file = netCDF4.Dataset(url) lat_in = file.variables['lat'][:] lon_in = file.variables['lon'][:] lev = file.variables['lev'][:] pres2pv_in = file.variables['pres2pv'][0,:,:] pressfc_in = file.variables['pressfc'][0,:,:] nlev = lev.size nx = lon_in.size ny = lat_in.size u_in = np.full((nlev, ny, nx), None) v_in = np.full((nlev, ny, nx), None) t_in = np.full((nlev, ny, nx), None) hgt_in = np.full((nlev, ny, nx), None) ilev = 0 while ilev < nlev: print(ilev) u_in[ilev, :, :] = file.variables['ugrdprs'][0, ilev, :, :] ilev = ilev + 1 ilev = 0 while ilev < nlev: v_in[ilev, :, :] = file.variables['vgrdprs'][0, ilev, :, :] ilev = ilev + 1 ilev = 0 while ilev < nlev: t_in[ilev, :, :] = file.variables['tmpprs'][0, ilev, :, :] ilev = ilev + 1 ilev = 0 while ilev < nlev: hgt_in[ilev, :, :] = file.variables['hgtprs'][0, ilev, :, :] ilev = ilev + 1 #t_in = file.variables['tmpprs'][0,:,:,:] #u_in = file.variables['ugrdprs'][0,:,:,:] #v_in = file.variables['vgrdprs'][0,:,:,:] #hgt_in = file.variables['hgtprs'][0,:,:,:] file.close() # get array indices for lat-lon range # specified above iy1 = np.argmin( np.abs( lat_in - lat1 ) ) iy2 = np.argmin( np.abs( lat_in - lat2 ) ) ix1 = np.argmin( np.abs( lon_in - lon1 ) ) ix2 = np.argmin( np.abs( lon_in - lon2 ) ) # select specified lat-lon range t=t_in[:,iy1:iy2,ix1:ix2] lon=lon_in[ix1:ix2] lat=lat_in[iy1:iy2] u=u_in[:,iy1:iy2,ix1:ix2] v=v_in[:,iy1:iy2,ix1:ix2] hgt=hgt_in[:,iy1:iy2,ix1:ix2] pres2pv=pres2pv_in[iy1:iy2,ix1:ix2] pressfc=pressfc_in[iy1:iy2,ix1:ix2] # some prep work for derivatives xlon,ylat=np.meshgrid(lon,lat) # define potential temperature and Coriolis parameter theta=t*(1.E5/(lev[:,np.newaxis,np.newaxis]*100))**kap f=2*omega*np.sin(ylat*pi/180) lon = np.array(lon, dtype='float') lat = np.array(lat, dtype='float') lev = np.array(lev, dtype='float') u = np.array(u, dtype='float') v = np.array(v, dtype='float') hgt = np.array(hgt, dtype='float') pres2pv = np.array(pres2pv, dtype='float') pressfc = np.array(pressfc, dtype='float') theta = np.array(theta, dtype='float') f = np.array(f, dtype='float') # calculate derivatives def ddp(f): # handle unevenly-spaced levels with 2nd order # Lagrange interpolation # except for top and bottom, where use forward diff lev3=lev.reshape(lev.size,1,1)*100 dpp=lev3-np.roll(lev3,-1,axis=0) dpm=lev3-np.roll(lev3,1,axis=0) fp=np.roll(f,-1,axis=0) fm=np.roll(f,1,axis=0) ddp_f=( fm*dpp/( (dpp-dpm)*(-dpm) ) + f*(dpp+dpm)/( dpm*dpp ) + fp*dpm/( (dpm-dpp)*(-dpp) ) ) ddp_f[0,:,:]=(f[1,:,:]-f[0,:,:])/(lev3[1,:,:]-lev3[0,:,:]) ddp_f[-1,:,:]=(f[-1,:,:]-f[-2,:,:])/(lev3[-2,:,:]-lev3[-1,:,:]) return(ddp_f) def ddx(f): # use center-difference, assuming evenly spaced lon # except for side-boundaries, where use forward diff x=(re*np.cos(ylat*np.pi/180)*np.pi/180)*lon x3=x.reshape(1,x.shape[0],x.shape[1]) dx3=np.roll(x3,-1,axis=2)-np.roll(x3,1,axis=2) ddx_f=(np.roll(f,-1,axis=2)-np.roll(f,1,axis=2))/dx3 ddx_f[:,:,0]=(f[:,:,1]-f[:,:,0])/(x3[:,:,1]-x3[:,:,0]) ddx_f[:,:,-1]=(f[:,:,-2]-f[:,:,-1])/(x3[:,:,-2]-x3[:,:,-1]) return(ddx_f) def ddy(f): # use center-difference, assuming evenly spaced lon # except for N/S boundaries, where use forward diff y=(re*np.pi/180)*lat y3=y.reshape(1,y.shape[0],1) dy3=np.roll(y3,-1,axis=1)-np.roll(y3,1,axis=1) ddy_f=(np.roll(f,-1,axis=1)-np.roll(f,1,axis=1))/dy3 ddy_f[:,0,:]=(f[:,1,:]-f[:,0,:])/(y3[:,1,:]-y3[:,0,:]) ddy_f[:,-1,:]=(f[:,-2,:]-f[:,-1,:])/(y3[:,-2,:]-y3[:,-1,:]) return(ddy_f) #lev3=lev.reshape(lev.size,1,1) #ddp_theta=np.gradient(theta,lev3*100,axis=0) #ddx_theta=np.gradient(theta,axis=2)/dx #ddy_theta=np.gradient(theta,axis=1)/dy gf=1 ddp_theta=ddp(theta) ddp_u=ddp(gaussian_filter(u,sigma=gf)) ddp_v=ddp(gaussian_filter(v,sigma=gf)) ddx_theta=ddx(theta) ddy_theta=ddy(theta) ddx_v=ddx(gaussian_filter(v,sigma=gf)) ddy_ucos=ddy(gaussian_filter(u,sigma=gf)*np.cos(ylat*pi/180)) # calculate contributions to PV and PV absvort=ddx_v-(1/np.cos(ylat*pi/180))*ddy_ucos+f pv_one=g*absvort*(-ddp_theta) pv_two=g*(ddp_v*ddx_theta-ddp_u*ddy_theta) pv=pv_one+pv_two # calculate pressure of tropopause, Fortran-style (alas!) # as well as potential temperature (theta) and height # # starting from 10hPa and working down, to avoid # more complicated vertical structure higher up # nx=ix2-ix1+1 ny=iy2-iy1+1 nz=lev.size nzs=np.argwhere(lev==50.0)[0,0] tp=np.empty((ny-1,nx-1))*np.nan # initialize as undef tp_theta=np.empty((ny-1,nx-1))*np.nan # initialize as undef tp_hgt=np.empty((ny-1,nx-1))*np.nan # initialize as undef for ix in range(0,nx-1): for iy in range(0,ny-1): for iz in range(nzs,0,-1): if pv[iz,iy,ix]/1e-6<=tpdef: if np.isnan(tp[iy,ix]): tp[iy,ix]=( (lev[iz]*(pv[iz+1,iy,ix]-tpdef*1e-6) -lev[iz+1]*(pv[iz,iy,ix]-tpdef*1e-6))/ (pv[iz+1,iy,ix]-pv[iz,iy,ix]) ) tp_theta[iy,ix]=( ((lev[iz]-tp[iy,ix])*theta[iz+1,iy,ix]+ (tp[iy,ix]-lev[iz+1])*theta[iz,iy,ix])/ (lev[iz]-lev[iz+1]) ) tp_hgt[iy,ix]=( ((lev[iz]-tp[iy,ix])*hgt[iz+1,iy,ix]+ (tp[iy,ix]-lev[iz+1])*hgt[iz,iy,ix])/ (lev[iz]-lev[iz+1]) ) # calculate PV on the 330K isentropic surface # (also not in a pythonic way) nx=ix2-ix1+1 ny=iy2-iy1+1 nz=lev.size pv330=np.empty((ny-1,nx-1))*np.nan # initialize as undef for ix in range(0,nx-1): for iy in range(0,ny-1): for iz in range(nz-2,0,-1): if theta[iz,iy,ix]>=330: if theta[iz-1,iy,ix]<=330: if np.isnan(pv330[iy,ix]): pv330[iy,ix]=( ((330-theta[iz-1,iy,ix])*pv[iz,iy,ix]+ (theta[iz,iy,ix]-330)*pv[iz-1,iy,ix])/ (theta[iz,iy,ix]-theta[iz-1,iy,ix]) ) # slight smoothing of result # (appears to work better than smoothing u,v,t first) tp=gaussian_filter(tp,sigma=1) tp_theta=gaussian_filter(tp_theta,sigma=1) pv330=gaussian_filter(pv330,sigma=1) # define spatial correlation function for testing results def scorr(a,b): abar=np.mean(a) bbar=np.mean(b) covar=sum((a-abar)*(b-bbar)) avar=sum((a-abar)**2) bvar=sum((b-bbar)**2) r=covar/np.sqrt(avar*bvar) return(r) # identify latitude of lowest tropopause maxloc=np.argwhere(tp==np.amax(tp)) latmax=lat[maxloc[0,0]] # now make some plots - these badly need to be improved states = NaturalEarthFeature(category='cultural', scale='50m', facecolor='none', name='admin_1_states_provinces_shp') # get date for plotting fdate=datetime.strptime(mydate, '%Y%m%d').strftime('%d %b %Y') plt.close(fig='all') print('got here') nframe=30 iframe=0 while iframe<=nframe: plt.figure(iframe,figsize=plt.figaspect(0.5)) pressfc_smooth=gaussian_filter(pressfc,sigma=1) ax=plt.gca(projection='3d') surf=ax.plot_surface(xlon,ylat,tp,cmap="coolwarm",alpha=1, rstride=1,cstride=1, linewidth=0, antialiased=False) ax.plot_surface(xlon,ylat,pressfc_smooth/100,color="lightgray", rstride=1,cstride=1, linewidth=0, antialiased=False) ax.set_zlim(1000,100) ax.set_xlim(lon1,lon2) ax.set_ylim(lat1,lat2) ax.view_init(elev=90 - iframe*90/nframe,azim=-90) plt.title('2PVU Dynamic Tropopause over topographyn'+myhour+'Z '+fdate) plt.colorbar(surf) plt.savefig('goo'+ '{:04d}'.format(iframe)+'.png', bbox_inches='tight', dpi=300) iframe=iframe+1
彩蛋
1.thorpe_tropopause
https://github.com/mathewbarlow/thorpe_tropopause
2.stratospheric-polar-vortex
https://github.com/mathewbarlow/stratospheric-polar-vortex