├── .gitattributes ├── LICENSE ├── README.md ├── __pycache__ ├── soundingmaps.cpython-39.pyc └── supplementary_tools.cpython-39.pyc ├── examples └── GFS │ ├── gfs_hrly_pytpe_gyx_sound_2021-02-13_0600Z.png │ ├── gfs_hrly_pytpe_reg_sound_2021-02-13_0600Z.png │ ├── gfs_hrly_pytpesound_2021-02-13_0600Z.png │ └── gfs_hrly_pytpesound_cape_2021-02-13_0600Z.png ├── get_soundingmaps.py ├── soundingmaps.py ├── supplementary_tools.py └── v0.1 ├── gfs_hrly_ptype_soundings.py ├── gfs_hrly_ptype_soundings_local_gyx.py ├── gfs_hrly_ptype_soundings_regional.py ├── gfs_hrly_ptype_soundings_with_cape.py └── soundingmaps.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Jack Sillin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SoundingMaps 2 | Overlay small soundings above a map to analyze spatial and temporal trends in thermodynamic profiles 3 | 4 | ## 0.2 Update 5 | Version 0.2 is here as of 3/6/21! 6 | NEW features: 7 | * get_soundingmaps.py is now the main interface through which you 8 | can make soundingmaps. Run in either interactive mode to set variables through 9 | the interpreter or tweak the variables in the script to pick your own region 10 | and model of interest. 11 | * severe weather parameters: 12 | * CAPE/CIN shading 13 | * Parcel path plot 14 | * HGZ outline for unstable soundings 15 | * LCL marker for unstable soundings 16 | * sub-ground data now masked out 17 | * option to plot wet bulb profiles 18 | * black dot marks origin of sounding data 19 | 20 | This code now relies on MetPy 1.0, so make sure you're working with the most up-to-date version 21 | 22 | This is very much a work in progress! The plotting routines are a bit clumsy 23 | and it's not as easy as it should be to change domain locations/sizes. 24 | 25 | I'm posting this so others in the community can adapt it for their own ideas, 26 | help make it better, and also so I can learn more about how the development 27 | of open-source code actually works. Any and all tips/advice about how to work 28 | best in github, good coding practices, how to make this script or others like 29 | it more efficient, etc. are very much welcomed! 30 | 31 | Enjoy and happy coding. 32 | -Jack 33 | -------------------------------------------------------------------------------- /__pycache__/soundingmaps.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsillin/SoundingMaps/84b4988d54c5991b637279c4c563827721e4ff97/__pycache__/soundingmaps.cpython-39.pyc -------------------------------------------------------------------------------- /__pycache__/supplementary_tools.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsillin/SoundingMaps/84b4988d54c5991b637279c4c563827721e4ff97/__pycache__/supplementary_tools.cpython-39.pyc -------------------------------------------------------------------------------- /examples/GFS/gfs_hrly_pytpe_gyx_sound_2021-02-13_0600Z.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsillin/SoundingMaps/84b4988d54c5991b637279c4c563827721e4ff97/examples/GFS/gfs_hrly_pytpe_gyx_sound_2021-02-13_0600Z.png -------------------------------------------------------------------------------- /examples/GFS/gfs_hrly_pytpe_reg_sound_2021-02-13_0600Z.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsillin/SoundingMaps/84b4988d54c5991b637279c4c563827721e4ff97/examples/GFS/gfs_hrly_pytpe_reg_sound_2021-02-13_0600Z.png -------------------------------------------------------------------------------- /examples/GFS/gfs_hrly_pytpesound_2021-02-13_0600Z.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsillin/SoundingMaps/84b4988d54c5991b637279c4c563827721e4ff97/examples/GFS/gfs_hrly_pytpesound_2021-02-13_0600Z.png -------------------------------------------------------------------------------- /examples/GFS/gfs_hrly_pytpesound_cape_2021-02-13_0600Z.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsillin/SoundingMaps/84b4988d54c5991b637279c4c563827721e4ff97/examples/GFS/gfs_hrly_pytpesound_cape_2021-02-13_0600Z.png -------------------------------------------------------------------------------- /get_soundingmaps.py: -------------------------------------------------------------------------------- 1 | # An interface for auto-plotting basic soundingmaps 2 | # Written by Jack Sillin, last updated 3/6/21 version 0.2 3 | 4 | # Imports 5 | import cartopy.crs as ccrs 6 | import cartopy.feature as cfeature 7 | import numpy as np 8 | import matplotlib.pyplot as plt 9 | import netCDF4 10 | import xarray as xr 11 | import metpy 12 | from datetime import datetime 13 | import datetime as dt 14 | from metpy.units import units 15 | import scipy.ndimage as ndimage 16 | from metpy.plots import USCOUNTIES 17 | import cartopy 18 | from scipy.ndimage.filters import generic_filter as gf 19 | from metpy.plots import USCOUNTIES 20 | from metpy.plots import SkewT 21 | import metpy.calc as mpcalc 22 | from math import log, exp 23 | import matplotlib.patches as mpatches 24 | import matplotlib.lines as lines 25 | import supplementary_tools as spt 26 | import soundingmaps as smap 27 | 28 | #Set interactive mode on (True) or off (False) 29 | interactive = True 30 | 31 | #In interactive mode, user is prompted for domain info 32 | if interactive == True: 33 | # Welcome message for user 34 | print("\nWelcome to Jack Sillin's Soundingmap Plotting Tool!") 35 | print('\n \nNow you can make your own sounding maps with just a couple inputs:') 36 | print('\nDomain Info') 37 | print("Domain Size: string either 'local' (wfo-size) or 'regional' (a bit bigger)") 38 | print('Domain Name: whatever you want (string)') 39 | print('Center Lat: latitude of the centerpoint of your domain (float) in deg N') 40 | print('Center Lon: longitude of the centerpoint of your domain (flotat) in deg W') 41 | print('\nSounding Plot Info:') 42 | print("Want to shade CAPE/CIN on soundings? Options are 'yes' or 'no'") 43 | print("Want to plot wet bulb profiles? Options are 'yes' or 'no'") 44 | print('\nOther: ') 45 | print("Model: forecast model to plot (string). Options are 'GFS','NAM' ('RAP' at your own risk)") 46 | print("What phenomenon are you interested in? Options are 'winter' or 'severe'") 47 | print('\n \nMore features and options will be added soon. Happy plotting!') 48 | 49 | # User-Defined Inputs 50 | domainsize=input('Domain Size: ') 51 | domainname=input('Domain Name: ') 52 | centerlat =float(input('Center Lat: ')) 53 | centerlon =float(input('Center Lon: ')) 54 | 55 | cape_input = input('Shade CAPE? ') 56 | wetbulb_input = input('Plot Wet Bulb? ') 57 | 58 | model = input('Model: ') 59 | season = input('Season: ') 60 | 61 | # In quiet mode, domain info is set with variables here 62 | else: 63 | domainsize='regional' 64 | domainname='raleigh' 65 | centerlat = 35.8 66 | centerlon = 78.8 67 | model = 'GFS' 68 | cape_input = 'no' 69 | wetbulb_input = 'yes' 70 | season = 'winter' 71 | 72 | # Regardless of which mode is used, a few more variables will need to be set 73 | # based on the domain of interest: 74 | if domainsize=='regional': 75 | south = centerlat-6 76 | north = centerlat+6 77 | east = 360-(centerlon-7.5) 78 | west = 360-(centerlon+7.5) 79 | 80 | elif domainsize=='local': 81 | south = centerlat-1.625 82 | north = centerlat+1.625 83 | east = 360-(centerlon-2) 84 | west = 360-(centerlon+2) 85 | 86 | # Convert CAPE/Wetbulb inputs to booleans 87 | if cape_input == 'yes': 88 | cape_bool = True 89 | elif cape_input == 'no': 90 | cape_bool = False 91 | else: 92 | print('Invalid Input For CAPE (Need to pick "yes" or "no")') 93 | 94 | if wetbulb_input == 'yes': 95 | wetbulb_bool = True 96 | elif wetbulb_input == 'no': 97 | wetbulb_bool = False 98 | else: 99 | print('Invalid Input For wetbulb (Need to pick "yes" or "no")') 100 | 101 | #Prepare to pull data from NOMADS 102 | url = spt.get_url(model) 103 | mdate, init_hour = spt.get_init_time(model) 104 | 105 | # Create new directory 106 | output_dir = mdate+'_'+init_hour+'00' 107 | spt.mkdir_p(output_dir) 108 | spt.mkdir_p(output_dir+'/'+model) 109 | 110 | #Parse data using MetPy 111 | ds = xr.open_dataset(url) 112 | times = ds['tmp2m'].metpy.time 113 | init_time = ds['time'][0] 114 | 115 | # If plotting GFS data, subset to North America to save some time 116 | if model == 'GFS': 117 | lats = np.arange(20,55,0.25) 118 | lons = np.arange(240,310,0.25) 119 | ds = ds.sel(lat=lats,lon=lons) 120 | 121 | #Get timestep info based on which model you're plotting. 122 | timestepinfo = spt.get_num_timesteps(model) 123 | etime = timestepinfo[0] 124 | delt = timestepinfo[1] 125 | 126 | if season == 'winter': 127 | #Run through each forecast hour and make the plots 128 | for i in range(0,etime): 129 | #Get the data for the forecast hour of interest 130 | data = ds.metpy.parse_cf() 131 | data = data.isel(time=i) 132 | 133 | #Rename variables to useful things 134 | data = data.rename(spt.get_varlist(model)) 135 | 136 | #Pull out the categorical precip type arrays 137 | catrain = data['catrain'].squeeze() 138 | catsnow = data['catsnow'].squeeze() 139 | catsleet = data['catsleet'].squeeze() 140 | catice = data['catice'].squeeze() 141 | 142 | cape = data['cape'].squeeze() 143 | u10m = data['u'].squeeze() 144 | v10m = data['v'].squeeze() 145 | u10m = u10m*1.94384449 146 | v10m = v10m*1.94384449 147 | wspd = ((u10m**2)+(v10m**2))**.5 148 | 149 | #This extends each ptype one gridpoint outwards to prevent a gap between 150 | #different ptypes 151 | radius = 1 152 | kernel = np.zeros((2*radius+1,2*radius+1)) 153 | y1,x1 = np.ogrid[-radius:radius+1,-radius:radius+1] 154 | mask=x1**2+y1**2 <=radius**2 155 | kernel[mask]=1 156 | 157 | #Make the ptype arrays nicer looking 158 | snowc= gf(catsnow,np.max,footprint=kernel) 159 | icec = gf(catice,np.max,footprint=kernel) 160 | sleetc = gf(catsleet,np.max,footprint=kernel) 161 | rainc = gf(catrain,np.max,footprint=kernel) 162 | 163 | #Coordinate stuff 164 | #vertical, = data['temperature'].metpy.coordinates('vertical') 165 | time = data['temperature'].metpy.time 166 | x, y = data['temperature'].metpy.coordinates('x', 'y') 167 | lats, lons = xr.broadcast(y, x) 168 | zH5_crs = data['temperature'].metpy.cartopy_crs 169 | wind_slice = spt.get_windslice(model,domainsize) 170 | #Processing surface temperature data 171 | t2m = data['sfc_temp'].squeeze() 172 | t2m = ((t2m - 273.15)*(9./5.))+32. 173 | 174 | td2m = data['sfc_td'].squeeze() 175 | td2m = ((td2m - 273.15)*(9./5.))+32. 176 | td2ms = ndimage.gaussian_filter(td2m,sigma=5,order=0) 177 | 178 | #Fetch reflectivity data 179 | reflectivity = data['radar'].squeeze() 180 | 181 | #Create masked arrays for each ptype 182 | rain = np.ma.masked_where(rainc==0,reflectivity) 183 | sleet = np.ma.masked_where(sleetc==0,reflectivity) 184 | ice = np.ma.masked_where(icec==0,reflectivity) 185 | snow = np.ma.masked_where(snowc==0,reflectivity) 186 | 187 | #Process pressure level temp/rh data 188 | prs_temps = data['temperature'] 189 | prs_relh = data['rh'] 190 | 191 | #Process MSLP data 192 | mslp = data['mslp']/100. 193 | mslpc = mslp.squeeze() 194 | mslpc=ndimage.gaussian_filter(mslpc,sigma=1,order=0) 195 | sfc_pressure = data['spres'].squeeze()/100 196 | 197 | #This creates a nice-looking datetime label 198 | dtfs = str(time.dt.strftime('%Y-%m-%d_%H%MZ').item()) 199 | 200 | ########## SET UP FIGURE ################################################## 201 | fig = plt.figure(figsize=(15,15)) 202 | ax1 = fig.add_subplot(111, projection = zH5_crs) 203 | 204 | ax1.coastlines(resolution='10m') 205 | ax1.add_feature(cfeature.BORDERS.with_scale('10m'), linewidth=1.5) 206 | ax1.add_feature(cfeature.STATES.with_scale('10m'), linewidth=2.0) 207 | ax1.add_feature(USCOUNTIES.with_scale('500k'), edgecolor='silver') 208 | 209 | ########## PLOTTING ####################################################### 210 | #Plot 2m 32F isotherm 211 | tmp_2m32 = ax1.contour(x,y,t2m,colors='b', alpha = 0.8, levels = [32]) 212 | 213 | #Plot labeled MSLP contours 214 | h_contour = ax1.contour(x, y, mslpc, colors='dimgray', levels=range(940,1080,4),linewidths=1,alpha=0.7) 215 | #h_contour.clabel(fontsize=14, colors='dimgray', inline=1, inline_spacing=4, fmt='%i mb', rightside_up=True, use_clabeltext=True) 216 | 217 | #Define levels and colors for plotting precip types 218 | ref_levs = [1,5,10,15,20, 25, 30, 35, 40, 45, 50, 55, 60, 65] 219 | qr_cols = ['#cfffbf','#a7ff8a','#85ff5c','#60ff2b','#40ff00','#ffff00','#e6e600','#cccc00','#e4cc00'] 220 | qs_cols = ['#b8ffff','#82ffff','#00ffff','#00cccc','#00adad','#007575','#0087f5','#0039f5','#1d00f5'] 221 | qi_cols = ['#eeccff','#dd99ff','#cc66ff','#bb33ff','#aa00ff','#8800cc','#660099','#440066','#6600cc'] 222 | qz_cols = ['#ff0066','#ff0080','#ff33cc','#ff00bf','#cc0099','#990073','#66004d','#b30000','#ff3333'] 223 | 224 | #Plot surface-based CAPE 225 | capep = ax1.contourf(x, y, cape, levels=[100, 250, 500, 750, 1000, 1250, 1500, 1750, 2000, 2500, 3000], alpha = 0.6, cmap='RdPu')#['#0099ff00', '#4066ffb3', '#8066ff8c', '#BF66ff66','#8cff66','#b3ff66','#d9ff66','#ffff66','#ffd966','#ffcc66','#ffb366','#ff8c66','#ff6666','#ff668c','#ff66b3','#ff66d9','#ff66ff']) 226 | 227 | #Plot the underlying precip type shadings 228 | #Use try/except so that if no ice/sleet/etc is present, things don't break 229 | try: 230 | ra = ax1.contourf(x,y,rain,colors=qr_cols,levels=ref_levs,alpha=0.4,extend='max') 231 | except: 232 | print('no rain') 233 | try: 234 | sn = ax1.contourf(x,y,snow,colors=qs_cols,levels=ref_levs,alpha=0.4,extend='max') 235 | except: 236 | print('no snow') 237 | try: 238 | ip = ax1.contourf(x,y,sleet,colors=qi_cols,levels=ref_levs,alpha=0.4,extend='max') 239 | except: 240 | print('no sleet') 241 | try: 242 | zr = ax1.contourf(x,y,ice, colors=qz_cols,levels=ref_levs,alpha=0.4,extend='max') 243 | except: 244 | print('no ice') 245 | 246 | #Plot 10m wind barbs 247 | ax1.barbs(x[wind_slice],y[wind_slice],u10m[wind_slice,wind_slice],v10m[wind_slice,wind_slice], length=6,color='gray') 248 | 249 | #Plot soundings 250 | smap.plot_soundings(fig,ax1,prs_temps,prs_relh,sfc_pressure,centerlat,centerlon,domainsize,model,cape=cape_bool,wetbulb=wetbulb_bool) 251 | #Set plot extent and titles 252 | ax1.set_extent((west,east,south,north)) 253 | ax1.set_title('Precipitation Type, MSLP, and Selected Soundings',fontsize=16) 254 | ax1.set_title('Valid: '+time.dt.strftime('%Y-%m-%d %H:%MZ').item(),fontsize=11,loc='right') 255 | ax1.set_title(model+' Init: '+init_time.dt.strftime('%Y-%m-%d %H:%MZ').item(),fontsize=11,loc='left') 256 | 257 | #Save plot 258 | plt.savefig(output_dir+'/'+model+'/'+domainname+'_winter_soundingmap'+dtfs+'.png',bbox_inches='tight',pad_inches=0.1) 259 | 260 | elif season=='severe': 261 | #Run through each forecast hour and make the plots 262 | for i in range(0,etime): 263 | #Get the data for the forecast hour of interest 264 | 265 | #Parse to get crs via metpy: 266 | 267 | #This is the old way for metpy 0.12.2 268 | #data = ds.metpy.parse_cf() 269 | 270 | #This is the new way for metpy 1.0.0 271 | data = ds.metpy.assign_crs(grid_mapping_name='latitude_longitude') 272 | 273 | #Select forecast hour of interest 274 | data = data.isel(time=i) 275 | 276 | #Rename variables to useful things 277 | data = data.rename(spt.get_varlist(model)) 278 | 279 | #Extract some data into variable-specific arrays 280 | cape = data['cape'].squeeze() 281 | u10m = data['u'].squeeze() 282 | v10m = data['v'].squeeze() 283 | u10m = u10m*1.94384449 284 | v10m = v10m*1.94384449 285 | wspd = ((u10m**2)+(v10m**2))**.5 286 | sfc_pressure = data['spres'].squeeze()/100 287 | 288 | #Coordinate stuff 289 | #vertical, = data['temperature'].metpy.coordinates('vertical') 290 | time = data['temperature'].metpy.time 291 | x, y = data['temperature'].metpy.coordinates('x', 'y') 292 | lats, lons = xr.broadcast(y, x) 293 | zH5_crs = data['temperature'].metpy.cartopy_crs 294 | wind_slice = spt.get_windslice(model,domainsize) 295 | 296 | #Processing surface temperature/dew point data 297 | t2m = data['sfc_temp'].squeeze() 298 | t2m = ((t2m - 273.15)*(9./5.))+32. 299 | td2m = data['sfc_td'].squeeze() 300 | td2m = ((td2m - 273.15)*(9./5.))+32. 301 | td2ms = ndimage.gaussian_filter(td2m,sigma=5,order=0) 302 | 303 | #Fetch reflectivity data 304 | reflectivity = data['radar'].squeeze() 305 | 306 | #Process pressure level temp/rh data 307 | if model =='GFS': 308 | ilev = 18 309 | elif model == 'NAM': 310 | ilev = 30 311 | elif model == 'RAP': 312 | ilev = 24 313 | 314 | prs_temps = data['temperature'].isel(lev=slice(1,ilev,1)) 315 | prs_relh = data['rh'].isel(lev=slice(1,ilev,1)) 316 | print(prs_temps) 317 | #Process MSLP data 318 | mslp = data['mslp']/100. 319 | mslpc = mslp.squeeze() 320 | mslpc=ndimage.gaussian_filter(mslpc,sigma=1,order=0) 321 | 322 | #This creates a nice-looking datetime label 323 | dtfs = str(time.dt.strftime('%Y-%m-%d_%H%MZ').item()) 324 | 325 | ########## SET UP FIGURE ################################################## 326 | fig = plt.figure(figsize=(15,15)) 327 | ax1 = fig.add_subplot(111, projection = zH5_crs) 328 | 329 | #Add various geographical information to the plot 330 | ax1.coastlines(resolution='10m') 331 | ax1.add_feature(cfeature.BORDERS.with_scale('10m'), linewidth=1.5) 332 | ax1.add_feature(cfeature.STATES.with_scale('10m'), linewidth=2.0) 333 | ax1.add_feature(USCOUNTIES.with_scale('500k'), edgecolor='silver') 334 | 335 | ########## PLOTTING ####################################################### 336 | #Plot 2m 60F isodrosotherm if you want: 337 | 338 | #td60_c = ax1.contour(x,y,td2m,colors='darkgreen', alpha = 0.8, levels = [60]) 339 | #td60_c.clabel(fontsize=12,colors='darkgreen',fmt='Td=60F',rightside_up=True,use_clabeltext=True) 340 | 341 | #Plot MSLP contours 342 | h_contour = ax1.contour(x, y, mslpc, colors='dimgray', levels=range(940,1080,2),linewidths=1,alpha=0.7) 343 | #Add label if you want: 344 | #h_contour.clabel(fontsize=14, colors='dimgray', inline=1, inline_spacing=4, fmt='%i mb', rightside_up=True, use_clabeltext=True) 345 | 346 | #Define levels and colors for plotting precip types 347 | ref_levs = [1,5,10,15,20, 25, 30, 35, 40, 45, 50, 55, 60, 65] 348 | qr_cols = ['#cfffbf','#a7ff8a','#85ff5c','#60ff2b','#40ff00','#ffff00','#e6e600','#cccc00','#e4cc00','#ff9933','#ff6600','#ff5050','#ff0066'] 349 | qs_cols = ['#b8ffff','#82ffff','#00ffff','#00cccc','#00adad','#007575','#0087f5','#0039f5','#1d00f5'] 350 | qi_cols = ['#eeccff','#dd99ff','#cc66ff','#bb33ff','#aa00ff','#8800cc','#660099','#440066','#6600cc'] 351 | qz_cols = ['#ff0066','#ff0080','#ff33cc','#ff00bf','#cc0099','#990073','#66004d','#b30000','#ff3333'] 352 | 353 | capelevs = [100, 250, 500, 750, 1000, 1250, 1500, 1750, 2000, 2500, 3000] 354 | 355 | #Plot surface-based CAPE 356 | capep = ax1.contourf(x, y, cape, levels=capelevs, alpha = 0.6, cmap='RdPu',extend='max')#['#0099ff00', '#4066ffb3', '#8066ff8c', '#BF66ff66','#8cff66','#b3ff66','#d9ff66','#ffff66','#ffd966','#ffcc66','#ffb366','#ff8c66','#ff6666','#ff668c','#ff66b3','#ff66d9','#ff66ff']) 357 | #Plot composite reflectivity 358 | ra = ax1.contourf(x,y,reflectivity,colors=qr_cols,levels=ref_levs,alpha=0.4,extend='max') 359 | 360 | #Plot 10m wind barbs 361 | ax1.barbs(x[wind_slice],y[wind_slice],u10m[wind_slice,wind_slice],v10m[wind_slice,wind_slice], length=6,color='gray') 362 | 363 | #Plot soundings 364 | smap.plot_soundings(fig,ax1,prs_temps,prs_relh,sfc_pressure,centerlat,centerlon,domainsize,model,cape=cape_bool,wetbulb=wetbulb_bool) 365 | 366 | #Add colorbars for CAPE and reflectivity 367 | spt.addcapecolorbar(ax1,fig,capep,capelevs) 368 | spt.addrefcolorbar(ax1,fig,ra,ref_levs) 369 | 370 | #Set plot extent and titles 371 | ax1.set_extent((west,east,south,north)) 372 | ax1.set_title('Composite Reflectivity, SBCAPE, MSLP, 10m Wind, and Selected Soundings',fontsize=12) 373 | ax1.set_title('Valid: '+time.dt.strftime('%Y-%m-%d %H:%MZ').item(),fontsize=11,loc='right') 374 | ax1.set_title(model+' Init: '+init_time.dt.strftime('%Y-%m-%d %H:%MZ').item(),fontsize=11,loc='left') 375 | 376 | #Save plot 377 | plt.savefig(output_dir+'/'+model+'/'+domainname+'_severe_soundingmap_v22_'+dtfs+'.png',bbox_inches='tight',pad_inches=0.1) 378 | plt.close() 379 | plt.clf() 380 | -------------------------------------------------------------------------------- /soundingmaps.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This script hosts the function that plots soundings on maps. 3 | ''' 4 | 5 | #### IMPORTS #### 6 | import numpy as np 7 | import matplotlib.pyplot as plt 8 | import xarray as xr 9 | import metpy 10 | from datetime import datetime 11 | import datetime as dt 12 | from metpy.units import units 13 | from metpy.plots import SkewT 14 | import matplotlib.patches as mpatches 15 | import matplotlib.lines as lines 16 | import metpy.calc as mpcalc 17 | import supplementary_tools as spt 18 | 19 | def plot_soundings(fig,ax,temp,rh,sfc_pressure,centerlat,centerlon,domainsize,model,cape=False,wetbulb=False): 20 | """ 21 | This function will plot a bunch of little soundings onto a matplotlib fig,ax. 22 | 23 | temp is an xarray dataarray with temperature data on pressure levels at least 24 | between 1000 and 300mb (you can change the ylimits for other datasets) 25 | 26 | rh is an xarray dataarray with temperature data on pressure levels at least 27 | between 1000 and 300mb (you can change ) 28 | 29 | sfc_pressure is an xarray dataarray with surface pressure data (NOT MSLP!) 30 | 31 | centerlat and centerlon are the coordinates around which you want your map 32 | to be centered. both are floats or integers and are in degrees of latitude 33 | and degrees of longitude west (i.e. 70W would be input as positive 70 here) 34 | 35 | domainsize is a string either 'local' for ~WFO-size domains or 'regional' for 36 | NE/SE/Mid-Atlantic-size domains (12 deg lat by 15 deg lon). More will be added soon. 37 | 38 | model is a string that specifies which model is providing data for the plots. 39 | This determines a few things, most importantly longitude selections. Models 40 | currently supported are 'GFS','NAM',and 'RAP' 41 | 42 | cape is a boolean to indicate whether you want to overlay parcel paths and 43 | shade CAPE/CIN on soundings with >100 J/kg of CAPE (this value can be changed) 44 | 45 | wetbulb is a boolean to indicate whether you want to draw wet bulb profiles 46 | 47 | note that this function doesn't "return" anything but if you just call it and 48 | provide the right arguments, it works. 49 | 50 | for example: 51 | import soundingmaps as smap 52 | ... 53 | smap.plot_soundings(fig,ax1,data['temperature'],data['rh'],30.5,87.5,'local',cape=True) 54 | 55 | """ 56 | r = 5 57 | if domainsize == "local": 58 | init_lat_delt = 1.625 59 | init_lon_delt = 0.45 60 | lat_delts = [0.2, 0.7, 1.2, 1.75, 2.25, 2.8] 61 | londelt = 0.76 62 | startlon = centerlon - 2 + 0.45 63 | 64 | elif domainsize == "regional": 65 | init_lat_delt = 6 66 | init_lon_delt = 1.6 67 | lat_delts = [0.6, 2.5, 4.5, 6.4, 8.4, 10.25] 68 | londelt = 2.9 69 | startlon = centerlon - 7.5 + 1.6 70 | 71 | # Lon adjustment for GFS because it's [0,360] not [-180,180] 72 | if model == 'GFS': 73 | startlon = 360-startlon 74 | 75 | # set lat/lon grid from which to pull data to plot soundings 76 | startlat = centerlat-init_lat_delt 77 | 78 | sound_lats = [] 79 | sound_lons = [] 80 | for i in range(0, 6): 81 | lats = startlat + lat_delts[i] 82 | sound_lats.append(lats) 83 | 84 | for i in range(0,r): 85 | if model == 'GFS': 86 | lons = startlon-(londelt*i) 87 | else: 88 | lons = -startlon-(londelt*i) 89 | sound_lons.append(lons) 90 | 91 | # this sets how high each row of soundings is on the plot 92 | plot_elevs=[0.2,0.3,0.4,0.5,0.6,0.7] 93 | 94 | # whole bunch of legend stuff 95 | dashed_red_line = lines.Line2D([], [], linestyle='solid', color='r', label='Temperature') 96 | dashed_purple_line = lines.Line2D([],[],linestyle='dashed',color='purple',label='0C Isotherm') 97 | dashed_green_line = lines.Line2D([], [], linestyle='solid', color='g', label='Dew Point') 98 | grey_line = lines.Line2D([], [], color='darkgray', label='MSLP (hPa)') 99 | blue_line = lines.Line2D([], [], color='b',label='Wet Bulb') 100 | pink_line = lines.Line2D([], [], color='fuchsia',label='Surface-Based Parcel Path') 101 | teal_line = lines.Line2D([], [], linestyle='dashed',color='teal',label='HGZ') 102 | green_dot = lines.Line2D([], [], marker='o', color='forestgreen',label='LCL') 103 | black_dot = lines.Line2D([], [], marker='o', color='k',label='Sounding Origin') 104 | 105 | red = mpatches.Patch(color='tab:red',label='CAPE') 106 | blue = mpatches.Patch(color='tab:blue',label='CIN') 107 | 108 | # do the plotting based on user inputs 109 | if cape and wetbulb is True: 110 | print('CAPE + Wetbulb') 111 | for i, plot_elev in enumerate(plot_elevs): 112 | soundlat = sound_lats[i] 113 | 114 | if k<2: 115 | s=1 116 | else: 117 | s=0 118 | 119 | for i in range(s,r): 120 | levs_abv_ground = [] 121 | soundlon = sound_lons[i] 122 | sound_temps = temp.interp(lat=soundlat,lon=soundlon)-273.15 123 | sound_rh = rh.interp(lat=soundlat,lon=soundlon) 124 | sound_pres = temp.lev 125 | spres = sfc_pressure.interp(lat=soundlat,lon=soundlon) 126 | sound_dp = mpcalc.dewpoint_from_relative_humidity(sound_temps.data*units.degC,sound_rh.data*units.percent) 127 | sound_wb = mpcalc.wet_bulb_temperature(sound_pres,sound_temps.data*units.degC,sound_dp) 128 | 129 | #Only want data above the ground 130 | abv_sfc_temp = spt.mask_below_terrain(spres,sound_temps,sound_pres)[0] 131 | abv_sfc_dewp = spt.mask_below_terrain(spres,sound_dp,sound_pres)[0] 132 | abv_sfc_wetb = spt.mask_below_terrain(spres,sound_wb,sound_pres)[0] 133 | pres_abv_ground = spt.mask_below_terrain(spres,sound_temps,sound_pres)[1] 134 | 135 | #sound_wb = sound_wb*units.degC 136 | skew = SkewT(fig=fig,rect=(0.75-(0.15*i),plot_elev,.15,.1)) 137 | 138 | parcel_prof = mpcalc.parcel_profile(pres_abv_ground,abv_sfc_temp[0].data*units.degC,abv_sfc_dewp[0]) 139 | cape = mpcalc.cape_cin(pres_abv_ground,abv_sfc_temp.data*units.degC,abv_sfc_dewp,parcel_prof) 140 | capeout = int(cape[0].m) 141 | cinout = int(cape[1].m) 142 | 143 | #skew.ax.axvspan(-30, -10, color='cyan', alpha=0.4) 144 | 145 | skew.plot(pres_abv_ground,abv_sfc_wetb,'b',linewidth=2) 146 | skew.plot(pres_abv_ground,abv_sfc_dewp,'g',linewidth=3) 147 | skew.plot(pres_abv_ground,abv_sfc_temp,'r',linewidth=3) 148 | 149 | if capeout >100: 150 | # Shade areas of CAPE and CIN 151 | print(pres_abv_ground) 152 | print(abv_sfc_temp.data*units.degC) 153 | print(parcel_prof) 154 | skew.shade_cin(pres_abv_ground, abv_sfc_temp.data*units.degC, parcel_prof) 155 | skew.shade_cape(pres_abv_ground, abv_sfc_temp.data*units.degC, parcel_prof) 156 | skew.plot(pres_abv_ground,parcel_prof,color='fuchsia',linewidth=1) 157 | lcl_pressure, lcl_temperature = mpcalc.lcl(pres_abv_ground[0], abv_sfc_temp.data[0]*units.degC, abv_sfc_dewp[0]) 158 | skew.plot(lcl_pressure, lcl_temperature, 'ko', markerfacecolor='forestgreen') 159 | skew.ax.axvline(-30, color='teal', linestyle='--', linewidth=1) 160 | skew.ax.axvline(-10, color='teal', linestyle='--', linewidth=1) 161 | skew.plot(975,0, 'ko',markerfacecolor='k') 162 | 163 | skew.ax.axvline(0, color='purple', linestyle='--', linewidth=3) 164 | skew.ax.set_ylim((1000,300)) 165 | skew.ax.axis('off') 166 | 167 | leg = ax.legend(handles=[dashed_red_line,dashed_green_line,blue_line,dashed_purple_line,teal_line,green_dot,pink_line,red,blue,black_dot],title='Sounding Legend',loc=4,framealpha=1) 168 | elif cape == True and wetbulb == False: 169 | print('CAPE no wetbulb') 170 | for k in range(len(plot_elevs)): 171 | soundlat = sound_lats[k] 172 | plot_elev = plot_elevs[k] 173 | 174 | if k==0: 175 | s=1 176 | else: 177 | s=0 178 | 179 | for i in range(s,r): 180 | levs_abv_ground = [] 181 | soundlon = sound_lons[i] 182 | sound_temps = temp.interp(lat=soundlat,lon=soundlon)-273.15 183 | sound_rh = rh.interp(lat=soundlat,lon=soundlon) 184 | sound_pres = temp.lev 185 | spres = sfc_pressure.interp(lat=soundlat,lon=soundlon) 186 | sound_dp = mpcalc.dewpoint_from_relative_humidity(sound_temps.data*units.degC,sound_rh.data*units.percent) 187 | 188 | abv_sfc_temp = spt.mask_below_terrain(spres,sound_temps,sound_pres)[0] 189 | abv_sfc_dewp = spt.mask_below_terrain(spres,sound_dp,sound_pres)[0] 190 | pres_abv_ground = spt.mask_below_terrain(spres,sound_temps,sound_pres)[1] 191 | 192 | skew = SkewT(fig=fig,rect=(0.75-(0.15*i),plot_elev,.15,.1)) 193 | 194 | parcel_prof = mpcalc.parcel_profile(pres_abv_ground,abv_sfc_temp[0].data*units.degC,abv_sfc_dewp[0]) 195 | cape = mpcalc.cape_cin(pres_abv_ground,abv_sfc_temp.data*units.degC,abv_sfc_dewp,parcel_prof) 196 | capeout = int(cape[0].m) 197 | cinout = int(cape[1].m) 198 | 199 | skew.plot(pres_abv_ground,abv_sfc_dewp,'g',linewidth=3) 200 | skew.plot(pres_abv_ground,abv_sfc_temp,'r',linewidth=3) 201 | 202 | if capeout >100: 203 | # Shade areas of CAPE and CIN 204 | skew.shade_cin(pres_abv_ground, abv_sfc_temp.data*units.degC, parcel_prof) 205 | skew.shade_cape(pres_abv_ground, abv_sfc_temp.data*units.degC, parcel_prof) 206 | skew.plot(pres_abv_ground,parcel_prof,color='fuchsia',linewidth=1) 207 | print(abv_sfc_temp) 208 | lcl_pressure, lcl_temperature = mpcalc.lcl(pres_abv_ground[0], abv_sfc_temp.data[0]*units.degC, abv_sfc_dewp[0]) 209 | skew.plot(lcl_pressure, lcl_temperature, 'ko', markerfacecolor='forestgreen') 210 | skew.ax.axvline(-30, color='teal', linestyle='--', linewidth=1) 211 | skew.ax.axvline(-10, color='teal', linestyle='--', linewidth=1) 212 | 213 | skew.plot(975,0, 'ko',markerfacecolor='k') 214 | 215 | skew.ax.axvline(0, color='purple', linestyle='--', linewidth=3) 216 | skew.ax.set_ylim((1000,300)) 217 | skew.ax.axis('off') 218 | 219 | leg = ax.legend(handles=[dashed_red_line,dashed_green_line,dashed_purple_line,teal_line,green_dot,pink_line,red,blue,black_dot],title='Sounding Legend',loc=4,framealpha=1) 220 | 221 | elif wetbulb==True and cape == False: 222 | print('Wetbulb no CAPE') 223 | for k in range(len(plot_elevs)): 224 | soundlat = sound_lats[k] 225 | plot_elev = plot_elevs[k] 226 | 227 | if k==0: 228 | s=1 229 | else: 230 | s=0 231 | 232 | for i in range(s,r): 233 | levs_abv_ground = [] 234 | soundlon = sound_lons[i] 235 | sound_temps = temp.interp(lat=soundlat,lon=soundlon)-273.15 236 | sound_rh = rh.interp(lat=soundlat,lon=soundlon) 237 | sound_pres = temp.lev 238 | spres = sfc_pressure.interp(lat=soundlat,lon=soundlon) 239 | 240 | sound_dp = mpcalc.dewpoint_from_relative_humidity(sound_temps.data*units.degC,sound_rh.data*units.percent) 241 | 242 | sound_wb = mpcalc.wet_bulb_temperature(sound_pres,sound_temps.data*units.degC,sound_dp) 243 | 244 | abv_sfc_temp = spt.mask_below_terrain(spres,sound_temps,sound_pres)[0] 245 | abv_sfc_dewp = spt.mask_below_terrain(spres,sound_dp,sound_pres)[0] 246 | abv_sfc_wetb = spt.mask_below_terrain(spres,sound_wb,sound_pres)[0] 247 | pres_abv_ground = spt.mask_below_terrain(spres,sound_temps,sound_pres)[1] 248 | 249 | #sound_wb = sound_wb*units.degC 250 | skew = SkewT(fig=fig,rect=(0.75-(0.15*i),plot_elev,.15,.1)) 251 | 252 | skew.plot(pres_abv_ground,abv_sfc_wetb,'b',linewidth=2) 253 | skew.plot(pres_abv_ground,abv_sfc_dewp,'g',linewidth=3) 254 | skew.plot(pres_abv_ground,abv_sfc_temp,'r',linewidth=3) 255 | 256 | skew.ax.axvline(0, color='purple', linestyle='--', linewidth=3) 257 | skew.ax.set_ylim((1000,300)) 258 | skew.ax.axis('off') 259 | else: 260 | print('No Wetbulb or CAPE') 261 | for k in range(len(plot_elevs)): 262 | soundlat = sound_lats[k] 263 | plot_elev = plot_elevs[k] 264 | 265 | if k==0: 266 | s=1 267 | else: 268 | s=0 269 | 270 | for i in range(s,r): 271 | sound_pres = temp.lev 272 | sound_temps = temp.interp(lat=soundlat,lon=soundlon)-273.15 273 | sound_rh = rh.interp(lat=soundlat,lon=soundlon) 274 | sound_dp = mpcalc.dewpoint_from_relative_humidity(sound_temps.data*units.degC,sound_rh.data*units.percent) 275 | skew = SkewT(fig=fig,rect=(0.75-(0.15*i),plot_elev,.15,.1)) 276 | skew.plot(sound_pres,sound_dp,'g',linewidth=3) 277 | skew.plot(sound_pres,sound_temps,'r',linewidth=3) 278 | skew.plot(1000,0, 'ko',markerfacecolor='k') 279 | 280 | skew.ax.axvline(0, color='purple', linestyle='--', linewidth=3) 281 | skew.ax.set_ylim((1000,300)) 282 | skew.ax.axis('off') 283 | 284 | leg = ax.legend(handles=[dashed_red_line,dashed_green_line,blue_line,dashed_purple_line,black_dot],title='Sounding Legend',loc=4,framealpha=1) 285 | -------------------------------------------------------------------------------- /supplementary_tools.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This script contains tools for use in my various plotting routines, including 3 | the soundingmaps. 4 | ''' 5 | 6 | #### IMPORTS #### 7 | from datetime import datetime 8 | import datetime as dt 9 | import numpy as np 10 | from metpy.units import units 11 | import metpy.calc as mpcalc 12 | import matplotlib.pyplot as plt 13 | 14 | #### DATA FETCH HELPER FUNCTIONS #### 15 | def get_init_time(model): 16 | ''' 17 | This function will return date and run hour information to select the most 18 | current run of a given model. 19 | 20 | Input: model (string) currently supported 'HRRR','NAM','GFS','RTMA','RAP' 21 | 22 | Output: [mdate,init_hr] strings mdate=YYYYMMDD current run init_hr = HH. 23 | ''' 24 | current_time = datetime.utcnow() 25 | year = current_time.year 26 | month = current_time.month 27 | day = current_time.day 28 | hour = current_time.hour 29 | 30 | if model=='HRRR': 31 | if hour <3: 32 | init_time = current_time-dt.timedelta(hours=3) 33 | init_hour = '18' 34 | day = init_time.day 35 | month = init_time.month 36 | year = init_time.year 37 | elif hour<9: 38 | init_hour = '00' 39 | elif hour<14: 40 | init_hour = '06' 41 | elif hour<21: 42 | init_hour = '12' 43 | else: 44 | init_hour = '18' 45 | 46 | elif model=='NAM': 47 | if hour <4: 48 | init_time = current_time-dt.timedelta(hours=3) 49 | init_hour = '18' 50 | day = init_time.day 51 | month = init_time.month 52 | year = init_time.year 53 | elif hour<10: 54 | init_hour = '00' 55 | elif hour<16: 56 | init_hour = '06' 57 | elif hour<22: 58 | init_hour = '12' 59 | else: 60 | init_hour = '18' 61 | 62 | elif model=='GFS': 63 | if hour <5: 64 | init_time = current_time-dt.timedelta(hours=3) 65 | init_hour = '18' 66 | day = init_time.day 67 | month = init_time.month 68 | year = init_time.year 69 | elif hour<11: 70 | init_hour = '00' 71 | elif hour<17: 72 | init_hour = '06' 73 | elif hour<23: 74 | init_hour = '12' 75 | else: 76 | init_hour = '18' 77 | 78 | elif model=='RTMA': 79 | minute = current_time.minute 80 | if minute>50: 81 | init_hour = current_time.hour 82 | if float(init_hour) <10: 83 | init_hour = '0'+str(init_hour) 84 | else: 85 | init_hour = str(init_hour) 86 | else: 87 | time = current_time-dt.timedelta(hours=1) 88 | init_hour = time.hour 89 | if float(init_hour) <10: 90 | init_hour = '0'+str(init_hour) 91 | else: 92 | init_hour = str(init_hour) 93 | 94 | 95 | elif model=='RAP': 96 | minute = current_time.minute 97 | if minute<10: 98 | time = current_time-dt.timedelta(hours=2) 99 | init_hour = str(time.hour) 100 | if float(init_hour) <10: 101 | init_hour = '0'+str(init_hour) 102 | else: 103 | init_hour = str(init_hour) 104 | else: 105 | time = current_time-dt.timedelta(hours=1) 106 | init_hour = str(time.hour) 107 | if float(init_hour) <10: 108 | init_hour = '0'+str(init_hour) 109 | else: 110 | init_hour = str(init_hour) 111 | 112 | # Format the current date and time 113 | mdate = "{:4d}{:02d}{:02d}".format(year, month, day) # Formats the string to YYYYMMDD format 114 | 115 | return mdate, init_hour 116 | 117 | 118 | def get_prev_init_time(model): 119 | ''' 120 | This function will return date and run hour information for the previous 121 | forecast cycle of a given model. This is useful for analysis of model trends. 122 | 123 | Input: model (string) currently supported 'HRRR','NAM','GFS' 124 | 125 | Output: [mdate,init_hr] strings mdate=YYYYMMDD current run init_hr = HH. 126 | ''' 127 | 128 | current_time = datetime.utcnow() 129 | year = current_time.year 130 | month = current_time.month 131 | day = current_time.day 132 | hour = current_time.hour 133 | 134 | if model=='HRRR': 135 | if hour <3: 136 | init_time = current_time-dt.timedelta(hours=3) 137 | init_hour = '18' 138 | prev_init_hour = '12' 139 | day = piday = init_time.day 140 | month = pimonth = init_time.month 141 | year = piyear= init_time.year 142 | elif hour<9: 143 | init_hour = '00' 144 | prev_init_hour = '18' 145 | prev_init_time = current_time-dt.timedelta(hours=9) 146 | piday = prev_init_time.day 147 | pimonth = prev_init_time.month 148 | piyear = prev_init_time.year 149 | elif hour<15: 150 | init_hour = '06' 151 | prev_init_hour = '00' 152 | piday = day 153 | pimonth = month 154 | piyear = year 155 | elif hour<21: 156 | init_hour = '12' 157 | prev_init_hour = '06' 158 | piday = day 159 | pimonth = month 160 | piyear = year 161 | else: 162 | init_hour = '18' 163 | prev_init_hour = '12' 164 | piday = day 165 | pimonth = month 166 | piyear = year 167 | 168 | elif model=='NAM': 169 | if hour <4: 170 | init_time = current_time-dt.timedelta(hours=4) 171 | init_hour = '18' 172 | prev_init_hour = '12' 173 | day = piday = init_time.day 174 | month = pimonth = init_time.month 175 | year = piyear= init_time.year 176 | elif hour<10: 177 | init_hour = '00' 178 | prev_init_hour = '18' 179 | prev_init_time = current_time-dt.timedelta(hours=10) 180 | piday = prev_init_time.day 181 | pimonth = prev_init_time.month 182 | piyear = prev_init_time.year 183 | elif hour<16: 184 | init_hour = '06' 185 | prev_init_hour = '00' 186 | piday = day 187 | pimonth = month 188 | piyear = year 189 | elif hour<22: 190 | init_hour = '12' 191 | prev_init_hour = '06' 192 | piday = day 193 | pimonth = month 194 | piyear = year 195 | else: 196 | init_hour = '18' 197 | prev_init_hour = '12' 198 | piday = day 199 | pimonth = month 200 | piyear = year 201 | elif model=='GFS': 202 | if hour <5: 203 | init_time = current_time-dt.timedelta(hours=5) 204 | init_hour = '18' 205 | prev_init_hour = '12' 206 | day = piday = init_time.day 207 | month = pimonth = init_time.month 208 | year = piyear= init_time.year 209 | elif hour<11: 210 | init_hour = '00' 211 | prev_init_hour = '18' 212 | prev_init_time = current_time-dt.timedelta(hours=11) 213 | piday = prev_init_time.day 214 | pimonth = prev_init_time.month 215 | piyear = prev_init_time.year 216 | elif hour<16: 217 | init_hour = '06' 218 | prev_init_hour = '00' 219 | piday = day 220 | pimonth = month 221 | piyear = year 222 | elif hour<22: 223 | init_hour = '12' 224 | prev_init_hour = '06' 225 | piday = day 226 | pimonth = month 227 | piyear = year 228 | else: 229 | init_hour = '18' 230 | prev_init_hour = '12' 231 | piday = day 232 | pimonth = month 233 | piyear = year 234 | 235 | # Format the current date and time 236 | mdate = "{:4d}{:02d}{:02d}".format(piyear, pimonth, piday) # Formats the string to YYYYMMDD format 237 | output = [mdate,prev_init_hour] 238 | 239 | return output 240 | 241 | def get_url(model): 242 | ''' 243 | Return the NOMADS URL for a model of choice. Currently supported options are 244 | GFS, NAM, HRRR, RAP 245 | ''' 246 | mdate, init_hour = get_init_time(model) 247 | if model == 'HRRR': 248 | url = 'http://nomads.ncep.noaa.gov:80/dods/hrrr/hrrr'+mdate+'/hrrr_sfc.t'+init_hour+'z' 249 | elif model == 'NAM': 250 | url = 'http://nomads.ncep.noaa.gov:80/dods/nam/nam'+mdate+'/nam_'+init_hour+'z' 251 | elif model == 'GFS': 252 | url = 'http://nomads.ncep.noaa.gov:80/dods/gfs_0p25_1hr/gfs'+mdate+'/gfs_0p25_1hr_'+init_hour+'z' 253 | elif model == 'RAP': 254 | url = 'http://nomads.ncep.noaa.gov:80/dods/rap/rap'+mdate+'/rap_'+init_hour+'z' 255 | 256 | return url 257 | 258 | def get_num_timesteps(model): 259 | ''' 260 | Return the number and width of time steps to query for a given model. 261 | Currently supported options are GFS, NAM, HRRR, RAP 262 | ''' 263 | if model =='GFS': 264 | etime = 121 265 | delt = 1 266 | elif model == 'NAM': 267 | etime = 28 268 | delt = 3 269 | elif model == 'HRRR': 270 | etime = 49 271 | delt = 1 272 | elif model == 'RAP': 273 | etime = 37 274 | delt = 1 275 | 276 | return [etime,delt] 277 | 278 | def get_varlist(model): 279 | ''' 280 | Each model has slightly different variable names. This function will return 281 | a dictionary that renames the right variables to the right things depending 282 | on which model you want. Currently supported options are GFS, NAM, HRRR, RAP 283 | ''' 284 | 285 | if model == 'RAP': 286 | vars = { 287 | 'cfrzrsfc':'catice', 288 | 'cicepsfc':'catsleet', 289 | 'crainsfc':'catrain', 290 | 'csnowsfc':'catsnow', 291 | 'tmpprs': 'temperature', 292 | 'mslmamsl':'mslp', 293 | 'tmp2m':'sfc_temp', 294 | 'dpt2m':'sfc_td', 295 | 'refcclm':'radar', 296 | 'rhprs':'rh', 297 | 'capesfc':'cape', 298 | 'ugrd10m':'u', 299 | 'vgrd10m':'v', 300 | 'pressfc':'spres' 301 | } 302 | 303 | elif model == 'HRRR': 304 | vars = { 305 | 'cfrzrsfc':'catice', 306 | 'cicepsfc':'catsleet', 307 | 'crainsfc':'catrain', 308 | 'csnowsfc':'catsnow', 309 | 'tcdcclm':'tcc', 310 | 'tmpprs': 'temperature', 311 | 'ugrd10m': 'u', 312 | 'vgrd10m': 'v', 313 | 'mslmamsl':'mslp', 314 | 'tmp2m':'sfc_temp', 315 | 'dpt2m':'sfc_td', 316 | 'refcclm':'radar', 317 | 'apcpsfc':'qpf', 318 | 'capesfc':'cape', 319 | 'gustsfc':'sfcgust', 320 | 'hcdchcll':'high_cloud', 321 | 'mcdcmcll':'mid_cloud', 322 | 'lcdclcll':'low_cloud', 323 | 'vissfc':'sfcvis', 324 | 'hgt263_k':'hgt_m10c', 325 | 'hgt253_k':'hgt_m20c', 326 | 'ltngclm':'lightning', 327 | 'sbt124toa':'simsat', 328 | 'hgt0c':'0chgt' 329 | } 330 | 331 | elif model == 'NAM': 332 | vars = { 333 | 'cfrzrsfc':'catice', 334 | 'cicepsfc':'catsleet', 335 | 'crainsfc':'catrain', 336 | 'csnowsfc':'catsnow', 337 | 'tcdcclm':'tcc', 338 | 'tmpprs': 'temperature', 339 | 'ugrd10m': 'u', 340 | 'vgrd10m': 'v', 341 | 'hgtprs': 'height', 342 | 'prmslmsl':'mslp', 343 | 'tmp2m':'sfc_temp', 344 | 'dpt2m':'sfc_td', 345 | 'refcclm':'radar', 346 | 'apcpsfc':'qpf', 347 | 'rhprs':'rh', 348 | 'capesfc':'cape', 349 | 'pressfc':'spres' 350 | } 351 | 352 | elif model == 'GFS': 353 | vars = { 354 | 'cfrzrsfc':'catice', 355 | 'cicepsfc':'catsleet', 356 | 'crainsfc':'catrain', 357 | 'csnowsfc':'catsnow', 358 | 'tcdcclm':'tcc', 359 | 'tmpprs': 'temperature', 360 | 'ugrd10m': 'u', 361 | 'vgrd10m': 'v', 362 | 'hgtprs': 'height', 363 | 'prmslmsl':'mslp', 364 | 'tmp2m':'sfc_temp', 365 | 'dpt2m':'sfc_td', 366 | 'refcclm':'radar', 367 | 'apcpsfc':'qpf', 368 | 'rhprs':'rh', 369 | 'capesfc':'cape', 370 | 'pressfc':'spres' 371 | 372 | } 373 | 374 | return vars 375 | 376 | #### CALCULATION OF METEOROLOGICAL VARIABLES #### 377 | def wet_bulb(temp,dewpoint): 378 | ''' 379 | This uses the simple 1/3 rule to compute wet bulb temperatures from temp and 380 | dew point values/arrays. See Knox et. al (2017) in BAMS for more info about 381 | this approximation and when it is most reliable. 382 | 383 | Input: temp, dewpoint either values or arrays 384 | 385 | Output: wet_bulb either values or arrays depending on input 386 | ''' 387 | tdd = temp-dewpoint 388 | wet_bulb = temp-((1/3)*tdd) 389 | return wet_bulb 390 | 391 | def wetbulb_with_nan(pressure,temperature,dewpoint): 392 | ''' 393 | This function uses the MetPy wet_bulb_temperature method to calculate the 394 | actual wet bulb temperature using pressure, temperature, and dew point info. 395 | 396 | Inputs: pressure, temperature, dewpoint pint arrays 397 | 398 | Output: wetbulb_full pint array 399 | 400 | This function was constructed using code graciously suggested by Jon Thielen 401 | ''' 402 | nan_mask = np.isnan(pressure) | np.isnan(temperature) | np.isnan(dewpoint) 403 | idx = np.arange(pressure.size)[~nan_mask] 404 | wetbulb_valid_only = mpcalc.wet_bulb_temperature(pressure[idx], temperature[idx], dewpoint[idx]) 405 | wetbulb_full = np.full(pressure.size, np.nan) * wetbulb_valid_only.units 406 | wetbulb_full[idx] = wetbulb_valid_only 407 | 408 | return wetbulb_full 409 | 410 | def fram(ice,wet_bulb,velocity): 411 | ''' 412 | This function computes ice accretion values using the Freezing Rain Accumulation 413 | Model method outlined in Sanders and Barjenbruch (2016) in WAF. 414 | 415 | Inputs: ice, wet_bulb, velocity which are arrays containing QPF falling as 416 | ZR, wet bulb temperature, and wind speed information. Units are inches per hour, 417 | degrees celsius, and knots respectively. 418 | 419 | Output: ice accretion array in units of inches. 420 | ''' 421 | ilr_p = ice 422 | ilr_t = (-0.0071*(wet_bulb**3))-(0.039*(wet_bulb**2))-(0.3904*wet_bulb)+0.5545 423 | ilr_v = (0.0014*(velocity**2))+(0.0027*velocity)+0.7574 424 | 425 | cond_1 = np.ma.masked_where(wet_bulb>-0.35,ice) 426 | cond_2 = np.ma.masked_where((wet_bulb<-0.35) & (velocity>12.),ice) 427 | cond_3 = np.ma.masked_where((wet_bulb<-0.35) & (velocity<=12.),ice) 428 | 429 | cond_1 = cond_1.filled(0) 430 | cond_2 = cond_2.filled(0) 431 | cond_3 = cond_3.filled(0) 432 | 433 | ilr_1 = (0.7*ilr_p)+(0.29*ilr_t)+(0.01*ilr_v) 434 | ilr_2 = (0.73*ilr_p)+(0.01*ilr_t)+(0.26*ilr_v) 435 | ilr_3 = (0.79*ilr_p)+(0.2*ilr_t)+(0.01*ilr_v) 436 | 437 | accretion_1 = cond_1*ilr_1 438 | accretion_2 = cond_2*ilr_2 439 | accretion_3 = cond_3*ilr_3 440 | 441 | total_accretion=accretion_1+accretion_2+accretion_3 442 | return total_accretion 443 | 444 | #### FIGURE TOOLS #### 445 | def addcapecolorbar(ax,fig,im,clevs): 446 | ''' 447 | This function adds a new colorbar on its own axes for CAPE 448 | 449 | Inputs: ax, fig are matplotlib axis/figure objects, im is the contourf object, 450 | and clevs is the contour levels used in the contourf plot 451 | 452 | Outputs: just call it and it'll put the colorbar in the right place 453 | 454 | This code was adapted from Dr. Kim Wood's Community Tools repo 455 | ''' 456 | axes_bbox = ax.get_position() 457 | left = axes_bbox.x0 458 | bottom = 0.17 459 | width = 0.38 460 | height = 0.01 461 | cax = fig.add_axes([left, bottom, width, height]) 462 | cbar = plt.colorbar(im, cax=cax, ticks=clevs, orientation='horizontal') 463 | #cbar.ax.xaxis.set_ticks_position('top') 464 | 465 | cbar.ax.tick_params(labelsize=8) 466 | cbar.set_label('Surface-Based CAPE (J/kg)', size=8) # MODIFY THIS for other fields!! 467 | 468 | def addrefcolorbar(ax,fig,im,clevs): 469 | ''' 470 | This function adds a new colorbar on its own axes for reflectivity 471 | 472 | Inputs: ax, fig are matplotlib axis/figure objects, im is the contourf object, 473 | and clevs is the contour levels used in the contourf plot 474 | 475 | Outputs: just call it and it'll put the colorbar in the right place 476 | 477 | This code was adapted from Dr. Kim Wood's Community Tools repo 478 | ''' 479 | axes_bbox = ax.get_position() 480 | left = axes_bbox.x0 + 0.39 481 | bottom = 0.17 482 | width = 0.38 483 | height = 0.01 484 | cax = fig.add_axes([left, bottom, width, height]) 485 | cbar = plt.colorbar(im, cax=cax, ticks=clevs, orientation='horizontal') 486 | #cbar.ax.xaxis.set_ticks_position('top') 487 | 488 | cbar.ax.tick_params(labelsize=8) 489 | cbar.set_label('Composite Reflectivity (dBZ)', size=8) # MODIFY THIS for other fields!! 490 | 491 | #### MISC #### 492 | def mkdir_p(mypath): 493 | '''Creates a directory. equivalent to using mkdir -p on the command line''' 494 | 495 | from errno import EEXIST 496 | from os import makedirs,path 497 | 498 | try: 499 | makedirs(mypath) 500 | except OSError as exc: # Python >2.5 501 | if exc.errno == EEXIST and path.isdir(mypath): 502 | pass 503 | else: raise 504 | 505 | def mask_below_terrain(spres,data,levs): 506 | ''' 507 | Given a surface pressure, return data only below that pressure (above ground). 508 | 509 | Needs spres, a surface pressure (float) 510 | Needs data, a pint quantity array of temps/dew point/rh/whatever 511 | Needs levs, a pint quantity array of pressures 512 | ''' 513 | above_ground = [] 514 | for i in range(len(levs)): 515 | diff = levs[i]-spres 516 | if diff <0: 517 | above_ground.append(levs[i]) 518 | pres_abv_ground = above_ground*units.hPa 519 | num_points_abv_ground = len(above_ground) 520 | data_abv_ground = data[-num_points_abv_ground:] 521 | return [data_abv_ground,pres_abv_ground] 522 | 523 | def get_windslice(model,domainsize): 524 | ''' 525 | Given a model and domainsize, return the right wind slice to make the barbs 526 | look good. 527 | 528 | Inputs: model, domainsize (strings). Currently supported models are 'GFS', 529 | 'NAM', and 'RAP'. Currently supported domainsizes are 'regional' and 'local' 530 | 531 | Output: slice object wind_slice 532 | ''' 533 | if model == 'GFS': 534 | if domainsize == 'regional': 535 | wind_slice = slice(2,-2,2) 536 | elif domainsize == 'local': 537 | wind_slice = slice(1,-1,1) 538 | else: 539 | print("Invalid domainsize String. Needs to be 'regional' or 'local'") 540 | elif model == 'NAM': 541 | if domainsize == 'regional': 542 | wind_slice = slice(6,-6,6) 543 | elif domainsize == 'local': 544 | wind_slice = slice(3,-3,3) 545 | else: 546 | print("Invalid domainsize String. Needs to be 'regional' or 'local'") 547 | elif model == 'RAP': 548 | if domainsize == 'regional': 549 | wind_slice = slice(12,-12,12) 550 | elif domainsize == 'local': 551 | wind_slice = slice(8,-8,8) 552 | else: 553 | print("Invalid domainsize String. Needs to be 'regional' or 'local'") 554 | else: 555 | print("Invalid model String. Needs to be 'GFS','NAM',or 'RAP'") 556 | 557 | return wind_slice 558 | -------------------------------------------------------------------------------- /v0.1/gfs_hrly_ptype_soundings.py: -------------------------------------------------------------------------------- 1 | import cartopy.crs as ccrs 2 | import cartopy.feature as cfeature 3 | import numpy as np 4 | import matplotlib.pyplot as plt 5 | import netCDF4 6 | import xarray as xr 7 | import metpy 8 | from datetime import datetime 9 | import datetime as dt 10 | from metpy.units import units 11 | import scipy.ndimage as ndimage 12 | from metpy.plots import USCOUNTIES 13 | import cartopy 14 | from scipy.ndimage.filters import generic_filter as gf 15 | from metpy.plots import USCOUNTIES 16 | from metpy.plots import SkewT 17 | import matplotlib.patches as mpatches 18 | import matplotlib.lines as lines 19 | import metpy.calc as mpcalc 20 | 21 | """ 22 | This program produces soundings derived from GFS model data obtained via 23 | the NOMADS openDAP functionality and overlays these soundings above a 24 | map of precipitation type and MSLP to assist in the assessment of spatial 25 | and temporal changes in thermodynamic and moisture profiles. 26 | 27 | This code was originally written by Jack Sillin. 28 | """ 29 | 30 | # make unique directory to store output 31 | def mkdir_p(mypath): 32 | """Creates a directory. equivalent to using mkdir -p on the command line""" 33 | 34 | from errno import EEXIST 35 | from os import makedirs, path 36 | 37 | try: 38 | makedirs(mypath) 39 | except OSError as exc: # Python >2.5 40 | if exc.errno == EEXIST and path.isdir(mypath): 41 | pass 42 | else: 43 | raise 44 | 45 | 46 | # grabbing data from NOMADS 47 | start_time = datetime.now() 48 | 49 | year = start_time.year 50 | 51 | month = f"{start_time:%m}" 52 | day = f"{start_time.day:%d}" 53 | hour = f"{start_time.hour:%H}" 54 | 55 | mdate = f"{start_time:%Y%m%d}" 56 | 57 | 58 | def get_init_hr(hour): 59 | if int(hour) < 6: 60 | init_hour = "00" 61 | elif int(hour) < 12: 62 | init_hour = "06" 63 | elif int(hour) < 17: 64 | init_hour = "12" 65 | elif int(hour) < 22: 66 | init_hour = "18" 67 | else: 68 | init_hour = "00" 69 | return init_hour 70 | 71 | 72 | init_hour = get_init_hr(hour) 73 | url = f"http://nomads.ncep.noaa.gov:80/dods/gfs_0p25_1hr/gfs{mdate}/gfs_0p25_1hr_{init_hour}z" 74 | 75 | # Create new directory to store output 76 | mkdir_p(f"{year}{month}{day}_{init_hour}00") 77 | mkdir_p(f"{output_dir}/GFS") # create subdirectory to store GFS output like this 78 | 79 | # This actually opens the dataset from NOMADS and parses it with MetPy 80 | ds = xr.open_dataset(url) 81 | init_hr = dt.datetime(year, month, day, init_hour) 82 | times = ds["tmp2m"].metpy.time # Pull out the time dimension 83 | init_time = ds["time"][0] 84 | 85 | # Subset the data to only work with certain lats and lons of interest 86 | lats = np.arange(20, 55, 0.25) 87 | lons = np.arange(240, 310, 0.25) 88 | 89 | ds = ds.sel(lat=lats, lon=lons) 90 | 91 | # Now loop through the 120 forecast hours to make the plots 92 | for i in range(0, 120): 93 | # Get the data for the forecast hour of interest 94 | data = ds.metpy.parse_cf() 95 | data = data.isel(time=i) 96 | 97 | # Rename variables to useful things 98 | data = data.rename( 99 | { 100 | "cfrzrsfc": "catice", 101 | "cicepsfc": "catsleet", 102 | "crainsfc": "catrain", 103 | "csnowsfc": "catsnow", 104 | "tmpprs": "temperature", 105 | "prmslmsl": "mslp", 106 | "tmp2m": "sfc_temp", 107 | "dpt2m": "sfc_td", 108 | "refcclm": "radar", 109 | "rhprs": "rh", 110 | } 111 | ) 112 | 113 | # Pull out the categorical precip type arrays 114 | catrain = data["catrain"].squeeze() 115 | catsnow = data["catsnow"].squeeze() 116 | catsleet = data["catsleet"].squeeze() 117 | catice = data["catice"].squeeze() 118 | 119 | # This extends each ptype one gridpoint outwards to prevent a gap between 120 | # different ptypes 121 | radius = 1 122 | kernel = np.zeros((2 * radius + 1, 2 * radius + 1)) 123 | y1, x1 = np.ogrid[-radius : radius + 1, -radius : radius + 1] 124 | mask = x1 ** 2 + y1 ** 2 <= radius ** 2 125 | kernel[mask] = 1 126 | 127 | # Make the ptype arrays nicer looking 128 | snowc = gf(catsnow, np.max, footprint=kernel) 129 | icec = gf(catice, np.max, footprint=kernel) 130 | sleetc = gf(catsleet, np.max, footprint=kernel) 131 | rainc = gf(catrain, np.max, footprint=kernel) 132 | 133 | # Coordinate stuff 134 | (vertical,) = data["temperature"].metpy.coordinates("vertical") 135 | time = data["temperature"].metpy.time 136 | x, y = data["temperature"].metpy.coordinates("x", "y") 137 | lat, lon = xr.broadcast(y, x) 138 | zH5_crs = data["temperature"].metpy.cartopy_crs 139 | 140 | # Processing surface temperature data 141 | t2m = data["sfc_temp"].squeeze() 142 | t2m = ((t2m - 273.15) * (9.0 / 5.0)) + 32.0 143 | 144 | td2m = data["sfc_td"].squeeze() 145 | td2m = ((td2m - 273.15) * (9.0 / 5.0)) + 32.0 146 | td2ms = ndimage.gaussian_filter(td2m, sigma=5, order=0) 147 | 148 | # Fetch reflectivity data 149 | reflectivity = data["radar"].squeeze() 150 | 151 | # Create masked arrays for each ptype 152 | rain = np.ma.masked_where(rainc == 0, reflectivity) 153 | sleet = np.ma.masked_where(sleetc == 0, reflectivity) 154 | ice = np.ma.masked_where(icec == 0, reflectivity) 155 | snow = np.ma.masked_where(snowc == 0, reflectivity) 156 | 157 | # Process MSLP data 158 | mslp = data["mslp"] / 100.0 159 | mslpc = mslp.squeeze() 160 | mslpc = ndimage.gaussian_filter(mslpc, sigma=1, order=0) 161 | 162 | # This creates a nice-looking datetime label 163 | dtfs = str(time.dt.strftime("%Y-%m-%d_%H%MZ").item()) 164 | 165 | ########## SET UP FIGURE ################################################## 166 | fig = plt.figure(figsize=(15, 15)) 167 | ax1 = fig.add_subplot(111, projection=zH5_crs) 168 | 169 | ax1.coastlines(resolution="10m") 170 | ax1.add_feature(cfeature.BORDERS.with_scale("10m")) 171 | ax1.add_feature(cfeature.STATES.with_scale("10m")) 172 | 173 | ########## PLOTTING ####################################################### 174 | # Plot 2m 32F isotherm 175 | tmp_2m32 = ax1.contour(x, y, t2m, colors="b", alpha=0.8, levels=[32]) 176 | 177 | # Plot labeled MSLP contours 178 | h_contour = ax1.contour( 179 | x, 180 | y, 181 | mslpc, 182 | colors="dimgray", 183 | levels=range(940, 1080, 4), 184 | linewidths=1, 185 | alpha=0.7, 186 | ) 187 | h_contour.clabel( 188 | fontsize=14, 189 | colors="dimgray", 190 | inline=1, 191 | inline_spacing=4, 192 | fmt="%i mb", 193 | rightside_up=True, 194 | use_clabeltext=True, 195 | ) 196 | 197 | # Define levels and colors for plotting precip types 198 | ref_levs = [1, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65] 199 | qr_cols = [ 200 | "#cfffbf", 201 | "#a7ff8a", 202 | "#85ff5c", 203 | "#60ff2b", 204 | "#40ff00", 205 | "#ffff00", 206 | "#e6e600", 207 | "#cccc00", 208 | "#e4cc00", 209 | ] 210 | qs_cols = [ 211 | "#b8ffff", 212 | "#82ffff", 213 | "#00ffff", 214 | "#00cccc", 215 | "#00adad", 216 | "#007575", 217 | "#0087f5", 218 | "#0039f5", 219 | "#1d00f5", 220 | ] 221 | qi_cols = [ 222 | "#eeccff", 223 | "#dd99ff", 224 | "#cc66ff", 225 | "#bb33ff", 226 | "#aa00ff", 227 | "#8800cc", 228 | "#660099", 229 | "#440066", 230 | "#6600cc", 231 | ] 232 | qz_cols = [ 233 | "#ff0066", 234 | "#ff0080", 235 | "#ff33cc", 236 | "#ff00bf", 237 | "#cc0099", 238 | "#990073", 239 | "#66004d", 240 | "#b30000", 241 | "#ff3333", 242 | ] 243 | 244 | # Plot the underlying precip type shadings 245 | # Use try/except so that if no ice/sleet/etc is present, things don't break 246 | try: 247 | ra = ax1.contourf( 248 | x, y, rain, colors=qr_cols, levels=ref_levs, alpha=0.4, extend="max" 249 | ) 250 | except: 251 | print("no rain") 252 | try: 253 | sn = ax1.contourf( 254 | x, y, snow, colors=qs_cols, levels=ref_levs, alpha=0.4, extend="max" 255 | ) 256 | except: 257 | print("no snow") 258 | try: 259 | ip = ax1.contourf( 260 | x, y, sleet, colors=qi_cols, levels=ref_levs, alpha=0.4, extend="max" 261 | ) 262 | except: 263 | print("no sleet") 264 | try: 265 | zr = ax1.contourf( 266 | x, y, ice, colors=qz_cols, levels=ref_levs, alpha=0.4, extend="max" 267 | ) 268 | except: 269 | print("no ice") 270 | 271 | # Set a title and extent for the map 272 | ax1.set_title("Precipitation Type and Selected Soundings", fontsize=16) 273 | ax1.set_title( 274 | "\n Valid: " + time.dt.strftime("%Y-%m-%d %H:%MZ").item(), 275 | fontsize=11, 276 | loc="right", 277 | ) 278 | ax1.set_title( 279 | "\n GFS Init: " + init_time.dt.strftime("%Y-%m-%d %H:%MZ").item(), 280 | fontsize=11, 281 | loc="left", 282 | ) 283 | ax1.set_extent( 284 | (255, 285, 25, 50) 285 | ) # , crs = zH5_crs) # Set a title and show the plot 286 | 287 | #################### SOUNDINGS ################################ 288 | """ 289 | Ok this is the fun part. I admit this is probably the number one ugliest 290 | way of possibly doing this. So sorry for the frustration! 291 | 292 | Basically, we're going to run six for loops, one for each row of soundings. 293 | For each row, we're going to start a little west of the eastern plot bound 294 | and move west by some fixed increment that will depend on the size of your 295 | domain (the regional and local files will have different londelts). 296 | 297 | At each point of interest, we're going to interpolate the temp/rh fields 298 | at each pressure level and stuff that info into an array for plotting. 299 | Because we're using metpy, we also need to tag it with units. I'm told there 300 | are ways of doing this unit tagging that are 1000% better than what's shown 301 | here, but I always seem to find a way to break those. This way works, though 302 | it is very far from elegant. 303 | 304 | I've added a 0C isotherm with a purple dashed line since precip types are 305 | a pressing forecast concern at the time I wrote this (Feb 2021). I've also 306 | truncated the soundings at 300mb since stuff above that doesn't matter for 307 | precip types but you can change this with the ptop variable for severe 308 | applications. 309 | 310 | One other note of some importance is that because MetPy doesn't currently 311 | support data coordinates in the rect function, great care is required to 312 | make sure that the sounding data is actually coming from the bottom of your 313 | skew t plots (I set mine to come from the bottom of the purple line). I've 314 | put in a feature request for this, so hopefully it will be easier in the 315 | future. 316 | """ 317 | 318 | sound_pres = data.lev # Grab pressure data for the y-axis 319 | ptop = 300 # set the top of the sounding at 300mb 320 | startlat = 78 # set the starting latitude of each row 321 | londelt = 5.85 # set the spacing between columns (data-wise, NOT visually!!) 322 | 323 | # make the soundings in rows from the bottom up. Bottom row starts at 1 324 | # to leave room for a legend in the lower right 325 | 326 | for i in range(1, 5): 327 | soundlat = 27.15 328 | soundlon = 360 - (startlat + (londelt * i)) 329 | sound_temps = data["temperature"].interp(lat=soundlat, lon=soundlon) - 273.15 330 | sound_rh = data["rh"].interp(lat=soundlat, lon=soundlon) 331 | sound_dp = mpcalc.dewpoint_from_relative_humidity( 332 | sound_temps.data * units.degC, sound_rh.data * units.percent 333 | ) 334 | skew = SkewT(fig=fig, rect=(0.75 - (0.15 * i), 0.2, 0.15, 0.1)) 335 | skew.plot(sound_pres, sound_dp, "g", linewidth=3) 336 | skew.plot(sound_pres, sound_temps, "r", linewidth=3) 337 | skew.ax.axvline(0, color="purple", linestyle="--", linewidth=3) 338 | skew.ax.set_ylim((1000, ptop)) 339 | skew.ax.axis("off") 340 | 341 | for i in range(0, 5): 342 | soundlat = 31 343 | soundlon = 360 - (startlat + (londelt * i)) 344 | sound_temps = data["temperature"].interp(lat=soundlat, lon=soundlon) - 273.15 345 | sound_rh = data["rh"].interp(lat=soundlat, lon=soundlon) 346 | sound_dp = mpcalc.dewpoint_from_relative_humidity( 347 | sound_temps.data * units.degC, sound_rh.data * units.percent 348 | ) 349 | skew = SkewT(fig=fig, rect=(0.75 - (0.15 * i), 0.3, 0.15, 0.1)) 350 | skew.plot(sound_pres, sound_dp, "g", linewidth=3) 351 | skew.plot(sound_pres, sound_temps, "r", linewidth=3) 352 | skew.ax.axvline(0, color="purple", linestyle="--", linewidth=3) 353 | skew.ax.set_ylim((1000, ptop)) 354 | skew.ax.axis("off") 355 | 356 | for i in range(0, 5): 357 | soundlat = 34.75 358 | soundlon = 360 - (startlat + (londelt * i)) 359 | sound_temps = data["temperature"].interp(lat=soundlat, lon=soundlon) - 273.15 360 | sound_rh = data["rh"].interp(lat=soundlat, lon=soundlon) 361 | sound_dp = mpcalc.dewpoint_from_relative_humidity( 362 | sound_temps.data * units.degC, sound_rh.data * units.percent 363 | ) 364 | skew = SkewT(fig=fig, rect=(0.75 - (0.15 * i), 0.4, 0.15, 0.1)) 365 | skew.plot(sound_pres, sound_dp, "g", linewidth=3) 366 | skew.plot(sound_pres, sound_temps, "r", linewidth=3) 367 | skew.ax.axvline(0, color="purple", linestyle="--", linewidth=3) 368 | skew.ax.set_ylim((1000, ptop)) 369 | skew.ax.axis("off") 370 | 371 | for i in range(0, 5): 372 | soundlat = 38.75 373 | soundlon = 360 - (startlat + (londelt * i)) 374 | sound_temps = data["temperature"].interp(lat=soundlat, lon=soundlon) - 273.15 375 | sound_rh = data["rh"].interp(lat=soundlat, lon=soundlon) 376 | sound_dp = mpcalc.dewpoint_from_relative_humidity( 377 | sound_temps.data * units.degC, sound_rh.data * units.percent 378 | ) 379 | skew = SkewT(fig=fig, rect=(0.75 - (0.15 * i), 0.5, 0.15, 0.1)) 380 | skew.plot(sound_pres, sound_dp, "g", linewidth=3) 381 | skew.plot(sound_pres, sound_temps, "r", linewidth=3) 382 | skew.ax.axvline(0, color="purple", linestyle="--", linewidth=3) 383 | skew.ax.set_ylim((1000, ptop)) 384 | skew.ax.axis("off") 385 | 386 | for i in range(0, 5): 387 | soundlat = 42.65 388 | soundlon = 360 - (startlat + (londelt * i)) 389 | sound_temps = data["temperature"].interp(lat=soundlat, lon=soundlon) - 273.15 390 | sound_rh = data["rh"].interp(lat=soundlat, lon=soundlon) 391 | sound_dp = mpcalc.dewpoint_from_relative_humidity( 392 | sound_temps.data * units.degC, sound_rh.data * units.percent 393 | ) 394 | skew = SkewT(fig=fig, rect=(0.75 - (0.15 * i), 0.6, 0.15, 0.1)) 395 | skew.plot(sound_pres, sound_dp, "g", linewidth=3) 396 | skew.plot(sound_pres, sound_temps, "r", linewidth=3) 397 | skew.ax.axvline(0, color="purple", linestyle="--", linewidth=3) 398 | skew.ax.set_ylim((1000, ptop)) 399 | skew.ax.axis("off") 400 | 401 | for i in range(0, 5): 402 | soundlat = 46.35 403 | soundlon = 360 - (startlat + (londelt * i)) 404 | sound_temps = data["temperature"].interp(lat=soundlat, lon=soundlon) - 273.15 405 | sound_rh = data["rh"].interp(lat=soundlat, lon=soundlon) 406 | sound_dp = mpcalc.dewpoint_from_relative_humidity( 407 | sound_temps.data * units.degC, sound_rh.data * units.percent 408 | ) 409 | skew = SkewT(fig=fig, rect=(0.75 - (0.15 * i), 0.7, 0.15, 0.1)) 410 | skew.plot(sound_pres, sound_dp, "g", linewidth=3) 411 | skew.plot(sound_pres, sound_temps, "r", linewidth=3) 412 | skew.ax.axvline(0, color="purple", linestyle="--", linewidth=3) 413 | skew.ax.axis("off") 414 | skew.ax.set_ylim((1000, ptop)) 415 | 416 | ########## LEGEND ############ 417 | dashed_red_line = lines.Line2D( 418 | [], [], linestyle="solid", color="r", label="Temperature" 419 | ) 420 | dashed_purple_line = lines.Line2D( 421 | [], [], linestyle="dashed", color="purple", label="0C Isotherm" 422 | ) 423 | dashed_green_line = lines.Line2D( 424 | [], [], linestyle="solid", color="g", label="Dew Point" 425 | ) 426 | grey_line = lines.Line2D([], [], color="darkgray", label="MSLP (hPa)") 427 | blue_line = lines.Line2D([], [], color="b", label="2m 0C Isotherm") 428 | leg = ax1.legend( 429 | handles=[dashed_red_line, dashed_green_line, dashed_purple_line], 430 | title="Sounding Legend", 431 | loc=4, 432 | framealpha=1, 433 | ) 434 | leg.set_zorder(100) 435 | 436 | ######## Save the plot 437 | plt.savefig( 438 | output_dir + "/GFS/gfs_hrly_pytpesound_" + dtfs + ".png", 439 | bbox_inches="tight", 440 | pad_inches=0.1, 441 | ) 442 | plt.close() 443 | plt.clf() 444 | -------------------------------------------------------------------------------- /v0.1/gfs_hrly_ptype_soundings_local_gyx.py: -------------------------------------------------------------------------------- 1 | import cartopy.crs as ccrs 2 | import cartopy.feature as cfeature 3 | import numpy as np 4 | import matplotlib.pyplot as plt 5 | import netCDF4 6 | import xarray as xr 7 | import metpy 8 | from datetime import datetime 9 | import datetime as dt 10 | from metpy.units import units 11 | import scipy.ndimage as ndimage 12 | from metpy.plots import USCOUNTIES 13 | import cartopy 14 | from scipy.ndimage.filters import generic_filter as gf 15 | from metpy.plots import USCOUNTIES 16 | from metpy.plots import SkewT 17 | import metpy.calc as mpcalc 18 | import matplotlib.patches as mpatches 19 | import matplotlib.lines as lines 20 | 21 | """ 22 | This program produces soundings derived from GFS model data obtained via 23 | the NOMADS openDAP functionality and overlays these soundings above a 24 | map of precipitation type and MSLP to assist in the assessment of spatial 25 | and temporal changes in thermodynamic and moisture profiles. 26 | 27 | This code was originally written by Jack Sillin. 28 | """ 29 | 30 | # make unique directory to store output 31 | def mkdir_p(mypath): 32 | """Creates a directory. equivalent to using mkdir -p on the command line""" 33 | 34 | from errno import EEXIST 35 | from os import makedirs, path 36 | 37 | try: 38 | makedirs(mypath) 39 | except OSError as exc: # Python >2.5 40 | if exc.errno == EEXIST and path.isdir(mypath): 41 | pass 42 | else: 43 | raise 44 | 45 | 46 | # grabbing data from NOMADS 47 | startTime = datetime.now() 48 | 49 | year = startTime.year 50 | 51 | if startTime.month < 10: 52 | month = "0" + str(startTime.month) 53 | else: 54 | month = str(startTime.month) 55 | 56 | if startTime.day < 10: 57 | day = "0" + str(startTime.day) 58 | else: 59 | day = str(startTime.day) 60 | 61 | if startTime.hour < 10: 62 | hour = "0" + str(startTime.hour) 63 | else: 64 | hour = str(startTime.hour) 65 | 66 | mdate = str(year) + str(month) + str(day) 67 | 68 | 69 | def get_init_hr(hour): 70 | if int(hour) < 6: 71 | init_hour = "00" 72 | elif int(hour) < 12: 73 | init_hour = "06" 74 | elif int(hour) < 17: 75 | init_hour = "12" 76 | elif int(hour) < 22: 77 | init_hour = "18" 78 | else: 79 | init_hour = "00" 80 | return init_hour 81 | 82 | 83 | url = ( 84 | "http://nomads.ncep.noaa.gov:80/dods/gfs_0p25_1hr/gfs" 85 | + mdate 86 | + "/gfs_0p25_1hr_" 87 | + get_init_hr(hour) 88 | + "z" 89 | ) 90 | init_hour = get_init_hr(hour) 91 | 92 | # Create new directory to store output 93 | output_dir = ( 94 | str(year) + str(month) + str(day) + "_" + str(init_hour) + "00" 95 | ) # this string names the output directory 96 | mkdir_p(output_dir) 97 | mkdir_p(output_dir + "/GFS") # create subdirectory to store GFS output like this 98 | 99 | # This actually opens the dataset from NOMADS and parses it with MetPy 100 | ds = xr.open_dataset(url) 101 | init_hr = dt.datetime(year, int(month), int(day), int(init_hour)) 102 | times = ds["tmp2m"].metpy.time # Pull out the time dimension 103 | init_time = ds["time"][0] 104 | 105 | # Subset the data to only work with certain lats and lons of interest 106 | lats = np.arange(20, 55, 0.25) 107 | lons = np.arange(240, 310, 0.25) 108 | 109 | ds = ds.sel(lat=lats, lon=lons) 110 | 111 | # Now loop through the 120 forecast hours to make the plots 112 | for i in range(0, 120): 113 | # Get the data for the forecast hour of interest 114 | data = ds.metpy.parse_cf() 115 | data = data.isel(time=i) 116 | 117 | # Rename variables to useful things 118 | data = data.rename( 119 | { 120 | "cfrzrsfc": "catice", 121 | "cicepsfc": "catsleet", 122 | "crainsfc": "catrain", 123 | "csnowsfc": "catsnow", 124 | "tmpprs": "temperature", 125 | "prmslmsl": "mslp", 126 | "tmp2m": "sfc_temp", 127 | "dpt2m": "sfc_td", 128 | "refcclm": "radar", 129 | "rhprs": "rh", 130 | } 131 | ) 132 | 133 | # Pull out the categorical precip type arrays 134 | catrain = data["catrain"].squeeze() 135 | catsnow = data["catsnow"].squeeze() 136 | catsleet = data["catsleet"].squeeze() 137 | catice = data["catice"].squeeze() 138 | 139 | # This extends each ptype one gridpoint outwards to prevent a gap between 140 | # different ptypes 141 | radius = 1 142 | kernel = np.zeros((2 * radius + 1, 2 * radius + 1)) 143 | y1, x1 = np.ogrid[-radius : radius + 1, -radius : radius + 1] 144 | mask = x1 ** 2 + y1 ** 2 <= radius ** 2 145 | kernel[mask] = 1 146 | 147 | # Make the ptype arrays nicer looking 148 | snowc = gf(catsnow, np.max, footprint=kernel) 149 | icec = gf(catice, np.max, footprint=kernel) 150 | sleetc = gf(catsleet, np.max, footprint=kernel) 151 | rainc = gf(catrain, np.max, footprint=kernel) 152 | 153 | # Coordinate stuff 154 | (vertical,) = data["temperature"].metpy.coordinates("vertical") 155 | time = data["temperature"].metpy.time 156 | x, y = data["temperature"].metpy.coordinates("x", "y") 157 | lat, lon = xr.broadcast(y, x) 158 | zH5_crs = data["temperature"].metpy.cartopy_crs 159 | 160 | # Processing surface temperature data 161 | t2m = data["sfc_temp"].squeeze() 162 | t2m = ((t2m - 273.15) * (9.0 / 5.0)) + 32.0 163 | 164 | td2m = data["sfc_td"].squeeze() 165 | td2m = ((td2m - 273.15) * (9.0 / 5.0)) + 32.0 166 | td2ms = ndimage.gaussian_filter(td2m, sigma=5, order=0) 167 | 168 | # Fetch reflectivity data 169 | reflectivity = data["radar"].squeeze() 170 | 171 | # Create masked arrays for each ptype 172 | rain = np.ma.masked_where(rainc == 0, reflectivity) 173 | sleet = np.ma.masked_where(sleetc == 0, reflectivity) 174 | ice = np.ma.masked_where(icec == 0, reflectivity) 175 | snow = np.ma.masked_where(snowc == 0, reflectivity) 176 | 177 | # Process MSLP data 178 | mslp = data["mslp"] / 100.0 179 | mslpc = mslp.squeeze() 180 | mslpc = ndimage.gaussian_filter(mslpc, sigma=1, order=0) 181 | 182 | # This creates a nice-looking datetime label 183 | dtfs = str(time.dt.strftime("%Y-%m-%d_%H%MZ").item()) 184 | 185 | ########## SET UP FIGURE ################################################## 186 | fig = plt.figure(figsize=(15, 15)) 187 | ax1 = fig.add_subplot(111, projection=zH5_crs) 188 | 189 | ax1.coastlines(resolution="10m") 190 | ax1.add_feature(cfeature.BORDERS.with_scale("10m")) 191 | ax1.add_feature(cfeature.STATES.with_scale("10m")) 192 | 193 | ########## PLOTTING ####################################################### 194 | # Plot 2m 32F isotherm 195 | tmp_2m32 = ax1.contour(x, y, t2m, colors="b", alpha=0.8, levels=[32]) 196 | 197 | # Plot labeled MSLP contours 198 | h_contour = ax1.contour( 199 | x, 200 | y, 201 | mslpc, 202 | colors="dimgray", 203 | levels=range(940, 1080, 4), 204 | linewidths=1, 205 | alpha=0.7, 206 | ) 207 | h_contour.clabel( 208 | fontsize=14, 209 | colors="dimgray", 210 | inline=1, 211 | inline_spacing=4, 212 | fmt="%i mb", 213 | rightside_up=True, 214 | use_clabeltext=True, 215 | ) 216 | 217 | # Define levels and colors for plotting precip types 218 | ref_levs = [1, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65] 219 | qr_cols = [ 220 | "#cfffbf", 221 | "#a7ff8a", 222 | "#85ff5c", 223 | "#60ff2b", 224 | "#40ff00", 225 | "#ffff00", 226 | "#e6e600", 227 | "#cccc00", 228 | "#e4cc00", 229 | ] 230 | qs_cols = [ 231 | "#b8ffff", 232 | "#82ffff", 233 | "#00ffff", 234 | "#00cccc", 235 | "#00adad", 236 | "#007575", 237 | "#0087f5", 238 | "#0039f5", 239 | "#1d00f5", 240 | ] 241 | qi_cols = [ 242 | "#eeccff", 243 | "#dd99ff", 244 | "#cc66ff", 245 | "#bb33ff", 246 | "#aa00ff", 247 | "#8800cc", 248 | "#660099", 249 | "#440066", 250 | "#6600cc", 251 | ] 252 | qz_cols = [ 253 | "#ff0066", 254 | "#ff0080", 255 | "#ff33cc", 256 | "#ff00bf", 257 | "#cc0099", 258 | "#990073", 259 | "#66004d", 260 | "#b30000", 261 | "#ff3333", 262 | ] 263 | 264 | # Plot the underlying precip type shadings 265 | # Use try/except so that if no ice/sleet/etc is present, things don't break 266 | try: 267 | ra = ax1.contourf( 268 | x, y, rain, colors=qr_cols, levels=ref_levs, alpha=0.4, extend="max" 269 | ) 270 | except: 271 | print("no rain") 272 | try: 273 | sn = ax1.contourf( 274 | x, y, snow, colors=qs_cols, levels=ref_levs, alpha=0.4, extend="max" 275 | ) 276 | except: 277 | print("no snow") 278 | try: 279 | ip = ax1.contourf( 280 | x, y, sleet, colors=qi_cols, levels=ref_levs, alpha=0.4, extend="max" 281 | ) 282 | except: 283 | print("no sleet") 284 | try: 285 | zr = ax1.contourf( 286 | x, y, ice, colors=qz_cols, levels=ref_levs, alpha=0.4, extend="max" 287 | ) 288 | except: 289 | print("no ice") 290 | 291 | # Set a title and extent for the map 292 | ax1.set_title("Precipitation Type and Selected Soundings", fontsize=16) 293 | ax1.set_title( 294 | "\n Valid: " + time.dt.strftime("%Y-%m-%d %H:%MZ").item(), 295 | fontsize=11, 296 | loc="right", 297 | ) 298 | ax1.set_title( 299 | "\n GFS Init: " + init_time.dt.strftime("%Y-%m-%d %H:%MZ").item(), 300 | fontsize=11, 301 | loc="left", 302 | ) 303 | ax1.set_extent( 304 | (287.25, 291.25, 42.5, 45.75) 305 | ) # , crs = zH5_crs) # Set a title and show the plot 306 | 307 | #################### SOUNDINGS ################################ 308 | """ 309 | Ok this is the fun part. I admit this is probably the number one ugliest 310 | way of possibly doing this. So sorry for the frustration! 311 | 312 | Basically, we're going to run six for loops, one for each row of soundings. 313 | For each row, we're going to start a little west of the eastern plot bound 314 | and move west by some fixed increment that will depend on the size of your 315 | domain (the regional and local files will have different londelts). 316 | 317 | At each point of interest, we're going to interpolate the temp/rh fields 318 | at each pressure level and stuff that info into an array for plotting. 319 | Because we're using metpy, we also need to tag it with units. I'm told there 320 | are ways of doing this unit tagging that are 1000% better than what's shown 321 | here, but I always seem to find a way to break those. This way works, though 322 | it is very far from elegant. 323 | 324 | I've added a 0C isotherm with a purple dashed line since precip types are 325 | a pressing forecast concern at the time I wrote this (Feb 2021). I've also 326 | truncated the soundings at 300mb since stuff above that doesn't matter for 327 | precip types but you can change this with the ptop variable for severe 328 | applications. 329 | 330 | One other note of some importance is that because MetPy doesn't currently 331 | support data coordinates in the rect function, great care is required to 332 | make sure that the sounding data is actually coming from the bottom of your 333 | skew t plots (I set mine to come from the bottom of the purple line). I've 334 | put in a feature request for this, so hopefully it will be easier in the 335 | future. 336 | """ 337 | 338 | """ 339 | Local-specific stuff... 340 | So this sounding code is similar but a little different than the original. 341 | I've tried to make it a little easier to create different domains because 342 | manually lining up the locations of the plots with the locations from which 343 | data is being pulled is really time-consuming. To make your own regional 344 | domain without checking this, follow the following formulae: 345 | 346 | Step 1: decide on bounds for your area of interest. You have 4 degrees of 347 | longitude and 3.25 degrees of latitude to work with. Changing these numbers 348 | will require re-calibrating the plot locations! In set_extent above, put 349 | your bounds in. 350 | 351 | Step 2: change startlon below to be your eastern bound + 0.45 degrees. Leave 352 | londelt alone unless you want to recalibrate everything! 353 | 354 | Step 3: change startlat to be your southern bound (no addition needed) 355 | """ 356 | 357 | sound_pres = data.lev 358 | ptop = 300 359 | startlon = 69.2 360 | startlat = 42.5 361 | londelt = 0.76 362 | sound_lons = [] 363 | sound_lats = [] 364 | lat_delts = [0.2, 0.7, 1.2, 1.75, 2.25, 2.8] 365 | r = 5 366 | for i in range(0, r): 367 | lon = -startlon - (londelt * i) 368 | sound_lons.append(lon) 369 | 370 | for i in range(0, 6): 371 | lat = startlat + lat_delts[i] 372 | sound_lats.append(lat) 373 | print(sound_lats) 374 | for i in range(1, r): 375 | soundlat = sound_lats[0] 376 | soundlon = 360 - (startlon + (londelt * i)) 377 | sound_temps = data["temperature"].interp(lat=soundlat, lon=soundlon) - 273.15 378 | 379 | sound_rh = data["rh"].interp(lat=soundlat, lon=soundlon) 380 | sound_dp = mpcalc.dewpoint_from_relative_humidity( 381 | sound_temps.data * units.degC, sound_rh.data * units.percent 382 | ) 383 | 384 | skew = SkewT(fig=fig, rect=(0.75 - (0.15 * i), 0.2, 0.15, 0.1)) 385 | skew.plot(sound_pres, sound_dp, "g", linewidth=3) 386 | skew.plot(sound_pres, sound_temps, "r", linewidth=3) 387 | skew.ax.axvline(0, color="purple", linestyle="--", linewidth=3) 388 | skew.ax.set_ylim((1000, ptop)) 389 | skew.ax.axis("off") 390 | 391 | for i in range(0, r): 392 | soundlat = sound_lats[1] 393 | soundlon = 360 - (startlon + (londelt * i)) 394 | sound_temps = data["temperature"].interp(lat=soundlat, lon=soundlon) - 273.15 395 | 396 | sound_rh = data["rh"].interp(lat=soundlat, lon=soundlon) 397 | sound_dp = mpcalc.dewpoint_from_relative_humidity( 398 | sound_temps.data * units.degC, sound_rh.data * units.percent 399 | ) 400 | 401 | skew = SkewT(fig=fig, rect=(0.75 - (0.15 * i), 0.3, 0.15, 0.1)) 402 | skew.plot(sound_pres, sound_dp, "g", linewidth=3) 403 | skew.plot(sound_pres, sound_temps, "r", linewidth=3) 404 | skew.ax.axvline(0, color="purple", linestyle="--", linewidth=3) 405 | skew.ax.set_ylim((1000, ptop)) 406 | skew.ax.axis("off") 407 | 408 | for i in range(0, r): 409 | soundlat = sound_lats[2] 410 | soundlon = 360 - (startlon + (londelt * i)) 411 | sound_temps = data["temperature"].interp(lat=soundlat, lon=soundlon) - 273.15 412 | 413 | sound_rh = data["rh"].interp(lat=soundlat, lon=soundlon) 414 | sound_dp = mpcalc.dewpoint_from_relative_humidity( 415 | sound_temps.data * units.degC, sound_rh.data * units.percent 416 | ) 417 | 418 | skew = SkewT(fig=fig, rect=(0.75 - (0.15 * i), 0.4, 0.15, 0.1)) 419 | skew.plot(sound_pres, sound_dp, "g", linewidth=3) 420 | skew.plot(sound_pres, sound_temps, "r", linewidth=3) 421 | skew.ax.axvline(0, color="purple", linestyle="--", linewidth=3) 422 | skew.ax.set_ylim((1000, ptop)) 423 | skew.ax.axis("off") 424 | 425 | for i in range(0, r): 426 | soundlat = sound_lats[3] 427 | soundlon = 360 - (startlon + (londelt * i)) 428 | sound_temps = data["temperature"].interp(lat=soundlat, lon=soundlon) - 273.15 429 | 430 | sound_rh = data["rh"].interp(lat=soundlat, lon=soundlon) 431 | sound_dp = mpcalc.dewpoint_from_relative_humidity( 432 | sound_temps.data * units.degC, sound_rh.data * units.percent 433 | ) 434 | 435 | skew = SkewT(fig=fig, rect=(0.75 - (0.15 * i), 0.5, 0.15, 0.1)) 436 | skew.plot(sound_pres, sound_dp, "g", linewidth=3) 437 | skew.plot(sound_pres, sound_temps, "r", linewidth=3) 438 | skew.ax.axvline(0, color="purple", linestyle="--", linewidth=3) 439 | skew.ax.set_ylim((1000, ptop)) 440 | skew.ax.axis("off") 441 | 442 | for i in range(0, r): 443 | soundlat = sound_lats[4] 444 | soundlon = 360 - (startlon + (londelt * i)) 445 | sound_temps = data["temperature"].interp(lat=soundlat, lon=soundlon) - 273.15 446 | 447 | sound_rh = data["rh"].interp(lat=soundlat, lon=soundlon) 448 | sound_dp = mpcalc.dewpoint_from_relative_humidity( 449 | sound_temps.data * units.degC, sound_rh.data * units.percent 450 | ) 451 | 452 | skew = SkewT(fig=fig, rect=(0.75 - (0.15 * i), 0.6, 0.15, 0.1)) 453 | skew.plot(sound_pres, sound_dp, "g", linewidth=3) 454 | skew.plot(sound_pres, sound_temps, "r", linewidth=3) 455 | skew.ax.axvline(0, color="purple", linestyle="--", linewidth=3) 456 | skew.ax.set_ylim((1000, ptop)) 457 | skew.ax.axis("off") 458 | 459 | for i in range(0, r): 460 | soundlat = sound_lats[5] 461 | soundlon = 360 - (startlon + (londelt * i)) 462 | sound_temps = data["temperature"].interp(lat=soundlat, lon=soundlon) - 273.15 463 | 464 | sound_rh = data["rh"].interp(lat=soundlat, lon=soundlon) 465 | sound_dp = mpcalc.dewpoint_from_relative_humidity( 466 | sound_temps.data * units.degC, sound_rh.data * units.percent 467 | ) 468 | 469 | skew = SkewT(fig=fig, rect=(0.75 - (0.15 * i), 0.7, 0.15, 0.1)) 470 | skew.plot(sound_pres, sound_dp, "g", linewidth=3) 471 | skew.plot(sound_pres, sound_temps, "r", linewidth=3) 472 | skew.ax.axvline(0, color="purple", linestyle="--", linewidth=3) 473 | skew.ax.set_ylim((1000, ptop)) 474 | skew.ax.axis("off") 475 | 476 | # uncomment the two lines below (rows,cols) to plot gridlines and check that 477 | # your sounding bases line up with the right lats/lons used to pull the data 478 | 479 | # rows = ax1.gridlines(ylocs=sound_lats,linewidth=2, linestyle='--', edgecolor='dimgrey',draw_labels=True) 480 | # cols = ax1.gridlines(xlocs=sound_lons,linewidth=2, linestyle='--', edgecolor='dimgrey',draw_labels=True) 481 | 482 | ########## LEGEND ############ 483 | dashed_red_line = lines.Line2D( 484 | [], [], linestyle="solid", color="r", label="Temperature" 485 | ) 486 | dashed_purple_line = lines.Line2D( 487 | [], [], linestyle="dashed", color="purple", label="0C Isotherm" 488 | ) 489 | dashed_green_line = lines.Line2D( 490 | [], [], linestyle="solid", color="g", label="Dew Point" 491 | ) 492 | grey_line = lines.Line2D([], [], color="darkgray", label="MSLP (hPa)") 493 | blue_line = lines.Line2D([], [], color="b", label="2m 0C Isotherm") 494 | leg = ax1.legend( 495 | handles=[dashed_red_line, dashed_green_line, dashed_purple_line], 496 | title="Sounding Legend", 497 | loc=4, 498 | framealpha=1, 499 | ) 500 | leg.set_zorder(100) 501 | 502 | ######## Save the plot 503 | plt.savefig( 504 | output_dir + "/GFS/gfs_hrly_pytpe_gyx_sound_" + dtfs + ".png", 505 | bbox_inches="tight", 506 | pad_inches=0.1, 507 | ) 508 | fcst_hr = str(0) 509 | plt.close() 510 | plt.clf() 511 | -------------------------------------------------------------------------------- /v0.1/gfs_hrly_ptype_soundings_regional.py: -------------------------------------------------------------------------------- 1 | import cartopy.crs as ccrs 2 | import cartopy.feature as cfeature 3 | import numpy as np 4 | import matplotlib.pyplot as plt 5 | import netCDF4 6 | import xarray as xr 7 | import metpy 8 | from datetime import datetime 9 | import datetime as dt 10 | from metpy.units import units 11 | import scipy.ndimage as ndimage 12 | from metpy.plots import USCOUNTIES 13 | import cartopy 14 | from scipy.ndimage.filters import generic_filter as gf 15 | from metpy.plots import USCOUNTIES 16 | from metpy.plots import SkewT 17 | import metpy.calc as mpcalc 18 | import matplotlib.patches as mpatches 19 | import matplotlib.lines as lines 20 | 21 | """ 22 | This program produces soundings derived from GFS model data obtained via 23 | the NOMADS openDAP functionality and overlays these soundings above a 24 | map of precipitation type and MSLP to assist in the assessment of spatial 25 | and temporal changes in thermodynamic and moisture profiles. 26 | 27 | This code was originally written by Jack Sillin. 28 | """ 29 | 30 | # make unique directory to store output 31 | def mkdir_p(mypath): 32 | """Creates a directory. equivalent to using mkdir -p on the command line""" 33 | 34 | from errno import EEXIST 35 | from os import makedirs, path 36 | 37 | try: 38 | makedirs(mypath) 39 | except OSError as exc: # Python >2.5 40 | if exc.errno == EEXIST and path.isdir(mypath): 41 | pass 42 | else: 43 | raise 44 | 45 | 46 | # grabbing data from NOMADS 47 | startTime = datetime.now() 48 | 49 | year = startTime.year 50 | 51 | if startTime.month < 10: 52 | month = "0" + str(startTime.month) 53 | else: 54 | month = str(startTime.month) 55 | 56 | if startTime.day < 10: 57 | day = "0" + str(startTime.day) 58 | else: 59 | day = str(startTime.day) 60 | 61 | if startTime.hour < 10: 62 | hour = "0" + str(startTime.hour) 63 | else: 64 | hour = str(startTime.hour) 65 | 66 | mdate = str(year) + str(month) + str(day) 67 | 68 | 69 | def get_init_hr(hour): 70 | if int(hour) < 6: 71 | init_hour = "00" 72 | elif int(hour) < 12: 73 | init_hour = "06" 74 | elif int(hour) < 17: 75 | init_hour = "12" 76 | elif int(hour) < 22: 77 | init_hour = "18" 78 | else: 79 | init_hour = "00" 80 | return init_hour 81 | 82 | 83 | url = ( 84 | "http://nomads.ncep.noaa.gov:80/dods/gfs_0p25_1hr/gfs" 85 | + mdate 86 | + "/gfs_0p25_1hr_" 87 | + get_init_hr(hour) 88 | + "z" 89 | ) 90 | init_hour = get_init_hr(hour) 91 | 92 | # Create new directory to store output 93 | output_dir = ( 94 | str(year) + str(month) + str(day) + "_" + str(init_hour) + "00" 95 | ) # this string names the output directory 96 | mkdir_p(output_dir) 97 | mkdir_p(output_dir + "/GFS") # create subdirectory to store GFS output like this 98 | 99 | # This actually opens the dataset from NOMADS and parses it with MetPy 100 | ds = xr.open_dataset(url) 101 | init_hr = dt.datetime(year, int(month), int(day), int(init_hour)) 102 | times = ds["tmp2m"].metpy.time # Pull out the time dimension 103 | init_time = ds["time"][0] 104 | 105 | # Subset the data to only work with certain lats and lons of interest 106 | lats = np.arange(20, 55, 0.25) 107 | lons = np.arange(240, 310, 0.25) 108 | 109 | ds = ds.sel(lat=lats, lon=lons) 110 | 111 | # Now loop through the 120 forecast hours to make the plots 112 | for i in range(0, 120): 113 | # Get the data for the forecast hour of interest 114 | data = ds.metpy.parse_cf() 115 | data = data.isel(time=i) 116 | 117 | # Rename variables to useful things 118 | data = data.rename( 119 | { 120 | "cfrzrsfc": "catice", 121 | "cicepsfc": "catsleet", 122 | "crainsfc": "catrain", 123 | "csnowsfc": "catsnow", 124 | "tmpprs": "temperature", 125 | "prmslmsl": "mslp", 126 | "tmp2m": "sfc_temp", 127 | "dpt2m": "sfc_td", 128 | "refcclm": "radar", 129 | "rhprs": "rh", 130 | } 131 | ) 132 | 133 | # Pull out the categorical precip type arrays 134 | catrain = data["catrain"].squeeze() 135 | catsnow = data["catsnow"].squeeze() 136 | catsleet = data["catsleet"].squeeze() 137 | catice = data["catice"].squeeze() 138 | 139 | # This extends each ptype one gridpoint outwards to prevent a gap between 140 | # different ptypes 141 | radius = 1 142 | kernel = np.zeros((2 * radius + 1, 2 * radius + 1)) 143 | y1, x1 = np.ogrid[-radius : radius + 1, -radius : radius + 1] 144 | mask = x1 ** 2 + y1 ** 2 <= radius ** 2 145 | kernel[mask] = 1 146 | 147 | # Make the ptype arrays nicer looking 148 | snowc = gf(catsnow, np.max, footprint=kernel) 149 | icec = gf(catice, np.max, footprint=kernel) 150 | sleetc = gf(catsleet, np.max, footprint=kernel) 151 | rainc = gf(catrain, np.max, footprint=kernel) 152 | 153 | # Coordinate stuff 154 | (vertical,) = data["temperature"].metpy.coordinates("vertical") 155 | time = data["temperature"].metpy.time 156 | x, y = data["temperature"].metpy.coordinates("x", "y") 157 | lat, lon = xr.broadcast(y, x) 158 | zH5_crs = data["temperature"].metpy.cartopy_crs 159 | 160 | # Processing surface temperature data 161 | t2m = data["sfc_temp"].squeeze() 162 | t2m = ((t2m - 273.15) * (9.0 / 5.0)) + 32.0 163 | 164 | td2m = data["sfc_td"].squeeze() 165 | td2m = ((td2m - 273.15) * (9.0 / 5.0)) + 32.0 166 | td2ms = ndimage.gaussian_filter(td2m, sigma=5, order=0) 167 | 168 | # Fetch reflectivity data 169 | reflectivity = data["radar"].squeeze() 170 | 171 | # Create masked arrays for each ptype 172 | rain = np.ma.masked_where(rainc == 0, reflectivity) 173 | sleet = np.ma.masked_where(sleetc == 0, reflectivity) 174 | ice = np.ma.masked_where(icec == 0, reflectivity) 175 | snow = np.ma.masked_where(snowc == 0, reflectivity) 176 | 177 | # Process MSLP data 178 | mslp = data["mslp"] / 100.0 179 | mslpc = mslp.squeeze() 180 | mslpc = ndimage.gaussian_filter(mslpc, sigma=1, order=0) 181 | 182 | # This creates a nice-looking datetime label 183 | dtfs = str(time.dt.strftime("%Y-%m-%d_%H%MZ").item()) 184 | 185 | ########## SET UP FIGURE ################################################## 186 | fig = plt.figure(figsize=(15, 15)) 187 | ax1 = fig.add_subplot(111, projection=zH5_crs) 188 | 189 | ax1.coastlines(resolution="10m") 190 | ax1.add_feature(cfeature.BORDERS.with_scale("10m")) 191 | ax1.add_feature(cfeature.STATES.with_scale("10m")) 192 | 193 | ########## PLOTTING ####################################################### 194 | # Plot 2m 32F isotherm 195 | tmp_2m32 = ax1.contour(x, y, t2m, colors="b", alpha=0.8, levels=[32]) 196 | 197 | # Plot labeled MSLP contours 198 | h_contour = ax1.contour( 199 | x, 200 | y, 201 | mslpc, 202 | colors="dimgray", 203 | levels=range(940, 1080, 4), 204 | linewidths=1, 205 | alpha=0.7, 206 | ) 207 | h_contour.clabel( 208 | fontsize=14, 209 | colors="dimgray", 210 | inline=1, 211 | inline_spacing=4, 212 | fmt="%i mb", 213 | rightside_up=True, 214 | use_clabeltext=True, 215 | ) 216 | 217 | # Define levels and colors for plotting precip types 218 | ref_levs = [1, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65] 219 | qr_cols = [ 220 | "#cfffbf", 221 | "#a7ff8a", 222 | "#85ff5c", 223 | "#60ff2b", 224 | "#40ff00", 225 | "#ffff00", 226 | "#e6e600", 227 | "#cccc00", 228 | "#e4cc00", 229 | ] 230 | qs_cols = [ 231 | "#b8ffff", 232 | "#82ffff", 233 | "#00ffff", 234 | "#00cccc", 235 | "#00adad", 236 | "#007575", 237 | "#0087f5", 238 | "#0039f5", 239 | "#1d00f5", 240 | ] 241 | qi_cols = [ 242 | "#eeccff", 243 | "#dd99ff", 244 | "#cc66ff", 245 | "#bb33ff", 246 | "#aa00ff", 247 | "#8800cc", 248 | "#660099", 249 | "#440066", 250 | "#6600cc", 251 | ] 252 | qz_cols = [ 253 | "#ff0066", 254 | "#ff0080", 255 | "#ff33cc", 256 | "#ff00bf", 257 | "#cc0099", 258 | "#990073", 259 | "#66004d", 260 | "#b30000", 261 | "#ff3333", 262 | ] 263 | 264 | # Plot the underlying precip type shadings 265 | # Use try/except so that if no ice/sleet/etc is present, things don't break 266 | try: 267 | ra = ax1.contourf( 268 | x, y, rain, colors=qr_cols, levels=ref_levs, alpha=0.4, extend="max" 269 | ) 270 | except: 271 | print("no rain") 272 | try: 273 | sn = ax1.contourf( 274 | x, y, snow, colors=qs_cols, levels=ref_levs, alpha=0.4, extend="max" 275 | ) 276 | except: 277 | print("no snow") 278 | try: 279 | ip = ax1.contourf( 280 | x, y, sleet, colors=qi_cols, levels=ref_levs, alpha=0.4, extend="max" 281 | ) 282 | except: 283 | print("no sleet") 284 | try: 285 | zr = ax1.contourf( 286 | x, y, ice, colors=qz_cols, levels=ref_levs, alpha=0.4, extend="max" 287 | ) 288 | except: 289 | print("no ice") 290 | 291 | # Set a title and extent for the map 292 | ax1.set_title("Precipitation Type and Selected Soundings", fontsize=16) 293 | ax1.set_title( 294 | "\n Valid: " + time.dt.strftime("%Y-%m-%d %H:%MZ").item(), 295 | fontsize=11, 296 | loc="right", 297 | ) 298 | ax1.set_title( 299 | "\n GFS Init: " + init_time.dt.strftime("%Y-%m-%d %H:%MZ").item(), 300 | fontsize=11, 301 | loc="left", 302 | ) 303 | ax1.set_extent( 304 | (255, 270, 29, 41) 305 | ) # , crs = zH5_crs) # Set a title and show the plot 306 | 307 | #################### SOUNDINGS ################################ 308 | """ 309 | Ok this is the fun part. I admit this is probably the number one ugliest 310 | way of possibly doing this. So sorry for the frustration! 311 | 312 | Basically, we're going to run six for loops, one for each row of soundings. 313 | For each row, we're going to start a little west of the eastern plot bound 314 | and move west by some fixed increment that will depend on the size of your 315 | domain (the regional and local files will have different londelts). 316 | 317 | At each point of interest, we're going to interpolate the temp/rh fields 318 | at each pressure level and stuff that info into an array for plotting. 319 | Because we're using metpy, we also need to tag it with units. I'm told there 320 | are ways of doing this unit tagging that are 1000% better than what's shown 321 | here, but I always seem to find a way to break those. This way works, though 322 | it is very far from elegant. 323 | 324 | I've added a 0C isotherm with a purple dashed line since precip types are 325 | a pressing forecast concern at the time I wrote this (Feb 2021). I've also 326 | truncated the soundings at 300mb since stuff above that doesn't matter for 327 | precip types but you can change this with the ptop variable for severe 328 | applications. 329 | 330 | One other note of some importance is that because MetPy doesn't currently 331 | support data coordinates in the rect function, great care is required to 332 | make sure that the sounding data is actually coming from the bottom of your 333 | skew t plots (I set mine to come from the bottom of the purple line). I've 334 | put in a feature request for this, so hopefully it will be easier in the 335 | future. 336 | """ 337 | 338 | """ 339 | Regional-specific stuff... 340 | So this sounding code is similar but a little different than the original. 341 | I've tried to make it a little easier to create different domains because 342 | manually lining up the locations of the plots with the locations from which 343 | data is being pulled is really time-consuming. To make your own regional 344 | domain without checking this, follow the following formulae: 345 | 346 | Step 1: decide on bounds for your area of interest. You have 15 degrees of 347 | longitude and 12 degrees of latitude to work with. Changing these numbers 348 | will require re-calibrating the plot locations! In set_extent above, put 349 | your bounds in. 350 | 351 | Step 2: change startlon below to be your eastern bound + 1.6 degrees. Leave 352 | londelt alone unless you want to recalibrate everything! 353 | 354 | Step 3: change startlat to be your southern bound (no addition needed) 355 | """ 356 | 357 | sound_pres = data.lev 358 | ptop = 300 359 | startlon = 91.6 360 | startlat = 29 361 | 362 | londelt = 2.9 363 | sound_lons = [] 364 | sound_lats = [] 365 | lat_delts = [0.6, 2.5, 4.5, 6.4, 8.4, 10.25] 366 | r = 5 367 | for i in range(0, r): 368 | lon = -startlon - (londelt * i) 369 | sound_lons.append(lon) 370 | 371 | for i in range(0, 6): 372 | lat = startlat + lat_delts[i] 373 | sound_lats.append(lat) 374 | print(sound_lats) 375 | 376 | for i in range(1, r): 377 | soundlat = sound_lats[0] 378 | soundlon = 360 - (startlon + (londelt * i)) 379 | sound_temps = data["temperature"].interp(lat=soundlat, lon=soundlon) - 273.15 380 | 381 | sound_rh = data["rh"].interp(lat=soundlat, lon=soundlon) 382 | sound_dp = mpcalc.dewpoint_from_relative_humidity( 383 | sound_temps.data * units.degC, sound_rh.data * units.percent 384 | ) 385 | 386 | skew = SkewT(fig=fig, rect=(0.75 - (0.15 * i), 0.2, 0.15, 0.1)) 387 | skew.plot(sound_pres, sound_dp, "g", linewidth=3) 388 | skew.plot(sound_pres, sound_temps, "r", linewidth=3) 389 | skew.ax.axvline(0, color="purple", linestyle="--", linewidth=3) 390 | skew.ax.set_ylim((1000, ptop)) 391 | skew.ax.axis("off") 392 | 393 | for i in range(0, r): 394 | soundlat = sound_lats[1] 395 | soundlon = 360 - (startlon + (londelt * i)) 396 | sound_temps = data["temperature"].interp(lat=soundlat, lon=soundlon) - 273.15 397 | 398 | sound_rh = data["rh"].interp(lat=soundlat, lon=soundlon) 399 | sound_dp = mpcalc.dewpoint_from_relative_humidity( 400 | sound_temps.data * units.degC, sound_rh.data * units.percent 401 | ) 402 | 403 | skew = SkewT(fig=fig, rect=(0.75 - (0.15 * i), 0.3, 0.15, 0.1)) 404 | skew.plot(sound_pres, sound_dp, "g", linewidth=3) 405 | skew.plot(sound_pres, sound_temps, "r", linewidth=3) 406 | skew.ax.axvline(0, color="purple", linestyle="--", linewidth=3) 407 | skew.ax.set_ylim((1000, ptop)) 408 | skew.ax.axis("off") 409 | 410 | for i in range(0, r): 411 | soundlat = sound_lats[2] 412 | soundlon = 360 - (startlon + (londelt * i)) 413 | sound_temps = data["temperature"].interp(lat=soundlat, lon=soundlon) - 273.15 414 | 415 | sound_rh = data["rh"].interp(lat=soundlat, lon=soundlon) 416 | sound_dp = mpcalc.dewpoint_from_relative_humidity( 417 | sound_temps.data * units.degC, sound_rh.data * units.percent 418 | ) 419 | 420 | skew = SkewT(fig=fig, rect=(0.75 - (0.15 * i), 0.4, 0.15, 0.1)) 421 | skew.plot(sound_pres, sound_dp, "g", linewidth=3) 422 | skew.plot(sound_pres, sound_temps, "r", linewidth=3) 423 | skew.ax.axvline(0, color="purple", linestyle="--", linewidth=3) 424 | skew.ax.set_ylim((1000, ptop)) 425 | skew.ax.axis("off") 426 | 427 | for i in range(0, r): 428 | soundlat = sound_lats[3] 429 | soundlon = 360 - (startlon + (londelt * i)) 430 | sound_temps = data["temperature"].interp(lat=soundlat, lon=soundlon) - 273.15 431 | 432 | sound_rh = data["rh"].interp(lat=soundlat, lon=soundlon) 433 | sound_dp = mpcalc.dewpoint_from_relative_humidity( 434 | sound_temps.data * units.degC, sound_rh.data * units.percent 435 | ) 436 | 437 | skew = SkewT(fig=fig, rect=(0.75 - (0.15 * i), 0.5, 0.15, 0.1)) 438 | skew.plot(sound_pres, sound_dp, "g", linewidth=3) 439 | skew.plot(sound_pres, sound_temps, "r", linewidth=3) 440 | skew.ax.axvline(0, color="purple", linestyle="--", linewidth=3) 441 | skew.ax.set_ylim((1000, ptop)) 442 | skew.ax.axis("off") 443 | 444 | for i in range(0, r): 445 | soundlat = sound_lats[4] 446 | soundlon = 360 - (startlon + (londelt * i)) 447 | sound_temps = data["temperature"].interp(lat=soundlat, lon=soundlon) - 273.15 448 | 449 | sound_rh = data["rh"].interp(lat=soundlat, lon=soundlon) 450 | sound_dp = mpcalc.dewpoint_from_relative_humidity( 451 | sound_temps.data * units.degC, sound_rh.data * units.percent 452 | ) 453 | 454 | skew = SkewT(fig=fig, rect=(0.75 - (0.15 * i), 0.6, 0.15, 0.1)) 455 | skew.plot(sound_pres, sound_dp, "g", linewidth=3) 456 | skew.plot(sound_pres, sound_temps, "r", linewidth=3) 457 | skew.ax.axvline(0, color="purple", linestyle="--", linewidth=3) 458 | skew.ax.set_ylim((1000, ptop)) 459 | skew.ax.axis("off") 460 | 461 | for i in range(0, r): 462 | soundlat = sound_lats[5] 463 | soundlon = 360 - (startlon + (londelt * i)) 464 | sound_temps = data["temperature"].interp(lat=soundlat, lon=soundlon) - 273.15 465 | 466 | sound_rh = data["rh"].interp(lat=soundlat, lon=soundlon) 467 | sound_dp = mpcalc.dewpoint_from_relative_humidity( 468 | sound_temps.data * units.degC, sound_rh.data * units.percent 469 | ) 470 | 471 | skew = SkewT(fig=fig, rect=(0.75 - (0.15 * i), 0.7, 0.15, 0.1)) 472 | skew.plot(sound_pres, sound_dp, "g", linewidth=3) 473 | skew.plot(sound_pres, sound_temps, "r", linewidth=3) 474 | skew.ax.axvline(0, color="purple", linestyle="--", linewidth=3) 475 | skew.ax.set_ylim((1000, ptop)) 476 | skew.ax.axis("off") 477 | 478 | # uncomment the two lines below (rows,cols) to plot gridlines and check that 479 | # your sounding bases line up with the right lats/lons used to pull the data 480 | 481 | # rows = ax1.gridlines(ylocs=sound_lats,linewidth=2, linestyle='--', edgecolor='dimgrey',draw_labels=True) 482 | # cols = ax1.gridlines(xlocs=sound_lons,linewidth=2, linestyle='--', edgecolor='dimgrey',draw_labels=True) 483 | 484 | ########## LEGEND ############ 485 | dashed_red_line = lines.Line2D( 486 | [], [], linestyle="solid", color="r", label="Temperature" 487 | ) 488 | dashed_purple_line = lines.Line2D( 489 | [], [], linestyle="dashed", color="purple", label="0C Isotherm" 490 | ) 491 | dashed_green_line = lines.Line2D( 492 | [], [], linestyle="solid", color="g", label="Dew Point" 493 | ) 494 | grey_line = lines.Line2D([], [], color="darkgray", label="MSLP (hPa)") 495 | blue_line = lines.Line2D([], [], color="b", label="2m 0C Isotherm") 496 | leg = ax1.legend( 497 | handles=[dashed_red_line, dashed_green_line, dashed_purple_line], 498 | title="Sounding Legend", 499 | loc=4, 500 | framealpha=1, 501 | ) 502 | leg.set_zorder(100) 503 | 504 | ######## Save the plot 505 | plt.savefig( 506 | output_dir + "/GFS/gfs_hrly_pytpe_reg_sound_" + dtfs + ".png", 507 | bbox_inches="tight", 508 | pad_inches=0.1, 509 | ) 510 | fcst_hr = str(0) 511 | plt.close() 512 | plt.clf() 513 | -------------------------------------------------------------------------------- /v0.1/gfs_hrly_ptype_soundings_with_cape.py: -------------------------------------------------------------------------------- 1 | import cartopy.crs as ccrs 2 | import cartopy.feature as cfeature 3 | import numpy as np 4 | import matplotlib.pyplot as plt 5 | import netCDF4 6 | import xarray as xr 7 | import metpy 8 | from datetime import datetime 9 | import datetime as dt 10 | from metpy.units import units 11 | import scipy.ndimage as ndimage 12 | from metpy.plots import USCOUNTIES 13 | import cartopy 14 | from scipy.ndimage.filters import generic_filter as gf 15 | from metpy.plots import USCOUNTIES 16 | from metpy.plots import SkewT 17 | import matplotlib.patches as mpatches 18 | import matplotlib.lines as lines 19 | import metpy.calc as mpcalc 20 | 21 | """ 22 | This program produces soundings derived from GFS model data obtained via 23 | the NOMADS openDAP functionality and overlays these soundings above a 24 | map of precipitation type and MSLP to assist in the assessment of spatial 25 | and temporal changes in thermodynamic and moisture profiles. 26 | 27 | This code was originally written by Jack Sillin. 28 | """ 29 | 30 | # make unique directory to store output 31 | def mkdir_p(mypath): 32 | """Creates a directory. equivalent to using mkdir -p on the command line""" 33 | 34 | from errno import EEXIST 35 | from os import makedirs, path 36 | 37 | try: 38 | makedirs(mypath) 39 | except OSError as exc: # Python >2.5 40 | if exc.errno == EEXIST and path.isdir(mypath): 41 | pass 42 | else: 43 | raise 44 | 45 | 46 | # grabbing data from NOMADS 47 | startTime = datetime.now() 48 | 49 | year = startTime.year 50 | 51 | if startTime.month < 10: 52 | month = "0" + str(startTime.month) 53 | else: 54 | month = str(startTime.month) 55 | 56 | if startTime.day < 10: 57 | day = "0" + str(startTime.day) 58 | else: 59 | day = str(startTime.day) 60 | 61 | if startTime.hour < 10: 62 | hour = "0" + str(startTime.hour) 63 | else: 64 | hour = str(startTime.hour) 65 | 66 | mdate = str(year) + str(month) + str(day) 67 | 68 | 69 | def get_init_hr(hour): 70 | if int(hour) < 6: 71 | init_hour = "00" 72 | elif int(hour) < 12: 73 | init_hour = "06" 74 | elif int(hour) < 17: 75 | init_hour = "12" 76 | elif int(hour) < 22: 77 | init_hour = "18" 78 | else: 79 | init_hour = "00" 80 | return init_hour 81 | 82 | 83 | init_hour = get_init_hr(hour) 84 | url = ( 85 | "http://nomads.ncep.noaa.gov:80/dods/gfs_0p25_1hr/gfs" 86 | + mdate 87 | + "/gfs_0p25_1hr_" 88 | + init_hour 89 | + "z" 90 | ) 91 | 92 | # Create new directory to store output 93 | output_dir = ( 94 | str(year) + str(month) + str(day) + "_" + str(init_hour) + "00" 95 | ) # this string names the output directory 96 | mkdir_p(output_dir) 97 | mkdir_p(output_dir + "/GFS") # create subdirectory to store GFS output like this 98 | 99 | # This actually opens the dataset from NOMADS and parses it with MetPy 100 | ds = xr.open_dataset(url) 101 | init_hr = dt.datetime(year, int(month), int(day), int(init_hour)) 102 | times = ds["tmp2m"].metpy.time # Pull out the time dimension 103 | init_time = ds["time"][0] 104 | 105 | # Subset the data to only work with certain lats and lons of interest 106 | lats = np.arange(20, 55, 0.25) 107 | lons = np.arange(240, 310, 0.25) 108 | 109 | ds = ds.sel(lat=lats, lon=lons) 110 | 111 | # Now loop through the 120 forecast hours to make the plots 112 | for i in range(0, 120): 113 | # Get the data for the forecast hour of interest 114 | data = ds.metpy.parse_cf() 115 | data = data.isel(time=i) 116 | 117 | # Rename variables to useful things 118 | data = data.rename( 119 | { 120 | "cfrzrsfc": "catice", 121 | "cicepsfc": "catsleet", 122 | "crainsfc": "catrain", 123 | "csnowsfc": "catsnow", 124 | "tmpprs": "temperature", 125 | "prmslmsl": "mslp", 126 | "tmp2m": "sfc_temp", 127 | "dpt2m": "sfc_td", 128 | "refcclm": "radar", 129 | "rhprs": "rh", 130 | } 131 | ) 132 | 133 | # Pull out the categorical precip type arrays 134 | catrain = data["catrain"].squeeze() 135 | catsnow = data["catsnow"].squeeze() 136 | catsleet = data["catsleet"].squeeze() 137 | catice = data["catice"].squeeze() 138 | 139 | # This extends each ptype one gridpoint outwards to prevent a gap between 140 | # different ptypes 141 | radius = 1 142 | kernel = np.zeros((2 * radius + 1, 2 * radius + 1)) 143 | y1, x1 = np.ogrid[-radius : radius + 1, -radius : radius + 1] 144 | mask = x1 ** 2 + y1 ** 2 <= radius ** 2 145 | kernel[mask] = 1 146 | 147 | # Make the ptype arrays nicer looking 148 | snowc = gf(catsnow, np.max, footprint=kernel) 149 | icec = gf(catice, np.max, footprint=kernel) 150 | sleetc = gf(catsleet, np.max, footprint=kernel) 151 | rainc = gf(catrain, np.max, footprint=kernel) 152 | 153 | # Coordinate stuff 154 | (vertical,) = data["temperature"].metpy.coordinates("vertical") 155 | time = data["temperature"].metpy.time 156 | x, y = data["temperature"].metpy.coordinates("x", "y") 157 | lat, lon = xr.broadcast(y, x) 158 | zH5_crs = data["temperature"].metpy.cartopy_crs 159 | 160 | # Processing surface temperature data 161 | t2m = data["sfc_temp"].squeeze() 162 | t2m = ((t2m - 273.15) * (9.0 / 5.0)) + 32.0 163 | 164 | td2m = data["sfc_td"].squeeze() 165 | td2m = ((td2m - 273.15) * (9.0 / 5.0)) + 32.0 166 | td2ms = ndimage.gaussian_filter(td2m, sigma=5, order=0) 167 | 168 | # Fetch reflectivity data 169 | reflectivity = data["radar"].squeeze() 170 | 171 | # Create masked arrays for each ptype 172 | rain = np.ma.masked_where(rainc == 0, reflectivity) 173 | sleet = np.ma.masked_where(sleetc == 0, reflectivity) 174 | ice = np.ma.masked_where(icec == 0, reflectivity) 175 | snow = np.ma.masked_where(snowc == 0, reflectivity) 176 | 177 | # Process MSLP data 178 | mslp = data["mslp"] / 100.0 179 | mslpc = mslp.squeeze() 180 | mslpc = ndimage.gaussian_filter(mslpc, sigma=1, order=0) 181 | 182 | # This creates a nice-looking datetime label 183 | dtfs = str(time.dt.strftime("%Y-%m-%d_%H%MZ").item()) 184 | 185 | ########## SET UP FIGURE ################################################## 186 | fig = plt.figure(figsize=(15, 15)) 187 | ax1 = fig.add_subplot(111, projection=zH5_crs) 188 | 189 | ax1.coastlines(resolution="10m") 190 | ax1.add_feature(cfeature.BORDERS.with_scale("10m")) 191 | ax1.add_feature(cfeature.STATES.with_scale("10m")) 192 | 193 | ########## PLOTTING ####################################################### 194 | # Plot 2m 32F isotherm 195 | tmp_2m32 = ax1.contour(x, y, t2m, colors="b", alpha=0.8, levels=[32]) 196 | 197 | # Plot labeled MSLP contours 198 | h_contour = ax1.contour( 199 | x, 200 | y, 201 | mslpc, 202 | colors="dimgray", 203 | levels=range(940, 1080, 4), 204 | linewidths=1, 205 | alpha=0.7, 206 | ) 207 | h_contour.clabel( 208 | fontsize=14, 209 | colors="dimgray", 210 | inline=1, 211 | inline_spacing=4, 212 | fmt="%i mb", 213 | rightside_up=True, 214 | use_clabeltext=True, 215 | ) 216 | 217 | # Define levels and colors for plotting precip types 218 | ref_levs = [1, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65] 219 | qr_cols = [ 220 | "#cfffbf", 221 | "#a7ff8a", 222 | "#85ff5c", 223 | "#60ff2b", 224 | "#40ff00", 225 | "#ffff00", 226 | "#e6e600", 227 | "#cccc00", 228 | "#e4cc00", 229 | ] 230 | qs_cols = [ 231 | "#b8ffff", 232 | "#82ffff", 233 | "#00ffff", 234 | "#00cccc", 235 | "#00adad", 236 | "#007575", 237 | "#0087f5", 238 | "#0039f5", 239 | "#1d00f5", 240 | ] 241 | qi_cols = [ 242 | "#eeccff", 243 | "#dd99ff", 244 | "#cc66ff", 245 | "#bb33ff", 246 | "#aa00ff", 247 | "#8800cc", 248 | "#660099", 249 | "#440066", 250 | "#6600cc", 251 | ] 252 | qz_cols = [ 253 | "#ff0066", 254 | "#ff0080", 255 | "#ff33cc", 256 | "#ff00bf", 257 | "#cc0099", 258 | "#990073", 259 | "#66004d", 260 | "#b30000", 261 | "#ff3333", 262 | ] 263 | 264 | # Plot the underlying precip type shadings 265 | # Use try/except so that if no ice/sleet/etc is present, things don't break 266 | try: 267 | ra = ax1.contourf( 268 | x, y, rain, colors=qr_cols, levels=ref_levs, alpha=0.4, extend="max" 269 | ) 270 | except: 271 | print("no rain") 272 | try: 273 | sn = ax1.contourf( 274 | x, y, snow, colors=qs_cols, levels=ref_levs, alpha=0.4, extend="max" 275 | ) 276 | except: 277 | print("no snow") 278 | try: 279 | ip = ax1.contourf( 280 | x, y, sleet, colors=qi_cols, levels=ref_levs, alpha=0.4, extend="max" 281 | ) 282 | except: 283 | print("no sleet") 284 | try: 285 | zr = ax1.contourf( 286 | x, y, ice, colors=qz_cols, levels=ref_levs, alpha=0.4, extend="max" 287 | ) 288 | except: 289 | print("no ice") 290 | 291 | # Set a title and extent for the map 292 | ax1.set_title("Precipitation Type and Selected Soundings", fontsize=16) 293 | ax1.set_title( 294 | "\n Valid: " + time.dt.strftime("%Y-%m-%d %H:%MZ").item(), 295 | fontsize=11, 296 | loc="right", 297 | ) 298 | ax1.set_title( 299 | "\n GFS Init: " + init_time.dt.strftime("%Y-%m-%d %H:%MZ").item(), 300 | fontsize=11, 301 | loc="left", 302 | ) 303 | ax1.set_extent( 304 | (255, 285, 25, 50) 305 | ) # , crs = zH5_crs) # Set a title and show the plot 306 | 307 | #################### SOUNDINGS ################################ 308 | """ 309 | Ok this is the fun part. I admit this is probably the number one ugliest 310 | way of possibly doing this. So sorry for the frustration! 311 | 312 | Basically, we're going to run six for loops, one for each row of soundings. 313 | For each row, we're going to start a little west of the eastern plot bound 314 | and move west by some fixed increment that will depend on the size of your 315 | domain (the regional and local files will have different londelts). 316 | 317 | At each point of interest, we're going to interpolate the temp/rh fields 318 | at each pressure level and stuff that info into an array for plotting. 319 | Because we're using metpy, we also need to tag it with units. I'm told there 320 | are ways of doing this unit tagging that are 1000% better than what's shown 321 | here, but I always seem to find a way to break those. This way works, though 322 | it is very far from elegant. 323 | 324 | I've added a 0C isotherm with a purple dashed line since precip types are 325 | a pressing forecast concern at the time I wrote this (Feb 2021). I've also 326 | truncated the soundings at 300mb since stuff above that doesn't matter for 327 | precip types but you can change this with the ptop variable for severe 328 | applications. 329 | 330 | In this version, I also plot CAPE and CIN shadings and surface-based parcel 331 | paths for sounding locations with CAPE >100J/kg. Change this value using 332 | the capemin variable to plot more or fewer parcel paths. 333 | 334 | One other note of some importance is that because MetPy doesn't currently 335 | support data coordinates in the rect function, great care is required to 336 | make sure that the sounding data is actually coming from the bottom of your 337 | skew t plots (I set mine to come from the bottom of the purple line). I've 338 | put in a feature request for this, so hopefully it will be easier in the 339 | future. 340 | """ 341 | 342 | sound_pres = data.lev # Grab pressure data for the y-axis 343 | ptop = 300 # set the top of the sounding at 300mb 344 | startlat = 78 # set the starting latitude of each row 345 | londelt = 5.85 # set the spacing between columns (data-wise, NOT visually!!) 346 | capemin = 100 # set the minimum value of CAPE for which CAPE/CIN/parcel paths plot 347 | 348 | # make the soundings in rows from the bottom up. Bottom row starts at 1 349 | # to leave room for a legend in the lower right 350 | 351 | for i in range(1, 5): 352 | soundlat = 27.15 353 | soundlon = 360 - (startlat + (londelt * i)) 354 | sound_temps = data["temperature"].interp(lat=soundlat, lon=soundlon) - 273.15 355 | sound_rh = data["rh"].interp(lat=soundlat, lon=soundlon) 356 | sound_dp = mpcalc.dewpoint_from_relative_humidity( 357 | sound_temps.data * units.degC, sound_rh.data * units.percent 358 | ) 359 | skew = SkewT(fig=fig, rect=(0.75 - (0.15 * i), 0.2, 0.15, 0.1)) 360 | 361 | parcel_prof = mpcalc.parcel_profile( 362 | sound_pres, sound_temps[0].data * units.degC, sound_dp[0] 363 | ) 364 | cape = mpcalc.cape_cin( 365 | sound_pres, sound_temps.data * units.degC, sound_dp, parcel_prof 366 | ) 367 | capeout = int(cape[0].m) 368 | cinout = int(cape[1].m) 369 | 370 | skew.plot(sound_pres, sound_dp, "g", linewidth=3) 371 | skew.plot(sound_pres, sound_temps, "r", linewidth=3) 372 | 373 | if capeout > capemin: 374 | # Shade areas of CAPE and CIN 375 | skew.shade_cin(sound_pres, sound_temps.data * units.degC, parcel_prof) 376 | skew.shade_cape(sound_pres, sound_temps.data * units.degC, parcel_prof) 377 | skew.plot(sound_pres, parcel_prof, color="fuchsia", linewidth=1) 378 | 379 | skew.ax.axvline(0, color="purple", linestyle="--", linewidth=3) 380 | skew.ax.set_ylim((1000, ptop)) 381 | skew.ax.axis("off") 382 | # skew.ax.text(0.1,0.1,str(soundlat)+' '+str(soundlon)) 383 | 384 | for i in range(0, 5): 385 | soundlat = 31 386 | soundlon = 360 - (startlat + (londelt * i)) 387 | sound_temps = data["temperature"].interp(lat=soundlat, lon=soundlon) - 273.15 388 | sound_rh = data["rh"].interp(lat=soundlat, lon=soundlon) 389 | sound_dp = mpcalc.dewpoint_from_relative_humidity( 390 | sound_temps.data * units.degC, sound_rh.data * units.percent 391 | ) 392 | skew = SkewT(fig=fig, rect=(0.75 - (0.15 * i), 0.3, 0.15, 0.1)) 393 | 394 | parcel_prof = mpcalc.parcel_profile( 395 | sound_pres, sound_temps[0].data * units.degC, sound_dp[0] 396 | ) 397 | cape = mpcalc.cape_cin( 398 | sound_pres, sound_temps.data * units.degC, sound_dp, parcel_prof 399 | ) 400 | capeout = int(cape[0].m) 401 | cinout = int(cape[1].m) 402 | 403 | skew.plot(sound_pres, sound_dp, "g", linewidth=3) 404 | skew.plot(sound_pres, sound_temps, "r", linewidth=3) 405 | 406 | if capeout > capemin: 407 | # Shade areas of CAPE and CIN 408 | skew.shade_cin(sound_pres, sound_temps.data * units.degC, parcel_prof) 409 | skew.shade_cape(sound_pres, sound_temps.data * units.degC, parcel_prof) 410 | skew.plot(sound_pres, parcel_prof, color="fuchsia", linewidth=1) 411 | skew.ax.axvline(0, color="purple", linestyle="--", linewidth=3) 412 | skew.ax.set_ylim((1000, ptop)) 413 | skew.ax.axis("off") 414 | 415 | for i in range(0, 5): 416 | soundlat = 34.75 417 | soundlon = 360 - (startlat + (londelt * i)) 418 | sound_temps = data["temperature"].interp(lat=soundlat, lon=soundlon) - 273.15 419 | sound_rh = data["rh"].interp(lat=soundlat, lon=soundlon) 420 | sound_dp = mpcalc.dewpoint_from_relative_humidity( 421 | sound_temps.data * units.degC, sound_rh.data * units.percent 422 | ) 423 | skew = SkewT(fig=fig, rect=(0.75 - (0.15 * i), 0.4, 0.15, 0.1)) 424 | parcel_prof = mpcalc.parcel_profile( 425 | sound_pres, sound_temps[0].data * units.degC, sound_dp[0] 426 | ) 427 | cape = mpcalc.cape_cin( 428 | sound_pres, sound_temps.data * units.degC, sound_dp, parcel_prof 429 | ) 430 | capeout = int(cape[0].m) 431 | cinout = int(cape[1].m) 432 | 433 | skew.plot(sound_pres, sound_dp, "g", linewidth=3) 434 | skew.plot(sound_pres, sound_temps, "r", linewidth=3) 435 | 436 | if capeout > capemin: 437 | # Shade areas of CAPE and CIN 438 | skew.shade_cin(sound_pres, sound_temps.data * units.degC, parcel_prof) 439 | skew.shade_cape(sound_pres, sound_temps.data * units.degC, parcel_prof) 440 | skew.plot(sound_pres, parcel_prof, color="fuchsia", linewidth=1) 441 | skew.ax.axvline(0, color="purple", linestyle="--", linewidth=3) 442 | skew.ax.set_ylim((1000, ptop)) 443 | skew.ax.axis("off") 444 | 445 | for i in range(0, 5): 446 | soundlat = 38.75 447 | soundlon = 360 - (startlat + (londelt * i)) 448 | sound_temps = data["temperature"].interp(lat=soundlat, lon=soundlon) - 273.15 449 | sound_rh = data["rh"].interp(lat=soundlat, lon=soundlon) 450 | sound_dp = mpcalc.dewpoint_from_relative_humidity( 451 | sound_temps.data * units.degC, sound_rh.data * units.percent 452 | ) 453 | skew = SkewT(fig=fig, rect=(0.75 - (0.15 * i), 0.5, 0.15, 0.1)) 454 | parcel_prof = mpcalc.parcel_profile( 455 | sound_pres, sound_temps[0].data * units.degC, sound_dp[0] 456 | ) 457 | cape = mpcalc.cape_cin( 458 | sound_pres, sound_temps.data * units.degC, sound_dp, parcel_prof 459 | ) 460 | capeout = int(cape[0].m) 461 | cinout = int(cape[1].m) 462 | 463 | skew.plot(sound_pres, sound_dp, "g", linewidth=3) 464 | skew.plot(sound_pres, sound_temps, "r", linewidth=3) 465 | 466 | if capeout > capemin: 467 | # Shade areas of CAPE and CIN 468 | skew.shade_cin(sound_pres, sound_temps.data * units.degC, parcel_prof) 469 | skew.shade_cape(sound_pres, sound_temps.data * units.degC, parcel_prof) 470 | skew.plot(sound_pres, parcel_prof, color="fuchsia", linewidth=1) 471 | skew.ax.axvline(0, color="purple", linestyle="--", linewidth=3) 472 | skew.ax.set_ylim((1000, ptop)) 473 | skew.ax.axis("off") 474 | # skew.ax.text(0.1,0.1,str(soundlat)+' '+str(soundlon)) 475 | 476 | for i in range(0, 5): 477 | soundlat = 42.65 478 | soundlon = 360 - (startlat + (londelt * i)) 479 | sound_temps = data["temperature"].interp(lat=soundlat, lon=soundlon) - 273.15 480 | sound_rh = data["rh"].interp(lat=soundlat, lon=soundlon) 481 | sound_dp = mpcalc.dewpoint_from_relative_humidity( 482 | sound_temps.data * units.degC, sound_rh.data * units.percent 483 | ) 484 | skew = SkewT(fig=fig, rect=(0.75 - (0.15 * i), 0.6, 0.15, 0.1)) 485 | parcel_prof = mpcalc.parcel_profile( 486 | sound_pres, sound_temps[0].data * units.degC, sound_dp[0] 487 | ) 488 | cape = mpcalc.cape_cin( 489 | sound_pres, sound_temps.data * units.degC, sound_dp, parcel_prof 490 | ) 491 | capeout = int(cape[0].m) 492 | cinout = int(cape[1].m) 493 | 494 | skew.plot(sound_pres, sound_dp, "g", linewidth=3) 495 | skew.plot(sound_pres, sound_temps, "r", linewidth=3) 496 | 497 | if capeout > capemin: 498 | # Shade areas of CAPE and CIN 499 | skew.shade_cin(sound_pres, sound_temps.data * units.degC, parcel_prof) 500 | skew.shade_cape(sound_pres, sound_temps.data * units.degC, parcel_prof) 501 | skew.plot(sound_pres, parcel_prof, color="fuchsia", linewidth=1) 502 | skew.ax.axvline(0, color="purple", linestyle="--", linewidth=3) 503 | skew.ax.set_ylim((1000, ptop)) 504 | skew.ax.axis("off") 505 | # skew.ax.text(0.1,0.1,str(soundlat)+' '+str(soundlon)) 506 | 507 | for i in range(0, 5): 508 | soundlat = 46.35 509 | soundlon = 360 - (startlat + (londelt * i)) 510 | sound_temps = data["temperature"].interp(lat=soundlat, lon=soundlon) - 273.15 511 | sound_rh = data["rh"].interp(lat=soundlat, lon=soundlon) 512 | sound_dp = mpcalc.dewpoint_from_relative_humidity( 513 | sound_temps.data * units.degC, sound_rh.data * units.percent 514 | ) 515 | skew = SkewT(fig=fig, rect=(0.75 - (0.15 * i), 0.7, 0.15, 0.1)) 516 | parcel_prof = mpcalc.parcel_profile( 517 | sound_pres, sound_temps[0].data * units.degC, sound_dp[0] 518 | ) 519 | cape = mpcalc.cape_cin( 520 | sound_pres, sound_temps.data * units.degC, sound_dp, parcel_prof 521 | ) 522 | capeout = int(cape[0].m) 523 | cinout = int(cape[1].m) 524 | 525 | skew.plot(sound_pres, sound_dp, "g", linewidth=3) 526 | skew.plot(sound_pres, sound_temps, "r", linewidth=3) 527 | 528 | if capeout > capemin: 529 | # Shade areas of CAPE and CIN 530 | skew.shade_cin(sound_pres, sound_temps.data * units.degC, parcel_prof) 531 | skew.shade_cape(sound_pres, sound_temps.data * units.degC, parcel_prof) 532 | skew.plot(sound_pres, parcel_prof, color="fuchsia", linewidth=1) 533 | skew.ax.axvline(0, color="purple", linestyle="--", linewidth=3) 534 | skew.ax.axis("off") 535 | skew.ax.set_ylim((1000, ptop)) 536 | 537 | ################ LEGEND ################# 538 | dashed_red_line = lines.Line2D( 539 | [], [], linestyle="solid", color="r", label="Temperature" 540 | ) 541 | dashed_purple_line = lines.Line2D( 542 | [], [], linestyle="dashed", color="purple", label="0C Isotherm" 543 | ) 544 | dashed_green_line = lines.Line2D( 545 | [], [], linestyle="solid", color="g", label="Dew Point" 546 | ) 547 | grey_line = lines.Line2D([], [], color="darkgray", label="MSLP (hPa)") 548 | blue_line = lines.Line2D([], [], color="b", label="2m 0C Isotherm") 549 | pink_line = lines.Line2D([], [], color="fuchsia", label="Surface-Based Parcel Path") 550 | red = mpatches.Patch(color="tab:red", label="CAPE") 551 | blue = mpatches.Patch(color="tab:blue", label="CIN") 552 | leg = ax1.legend( 553 | handles=[ 554 | dashed_red_line, 555 | dashed_green_line, 556 | dashed_purple_line, 557 | pink_line, 558 | red, 559 | blue, 560 | ], 561 | title="Sounding Legend", 562 | loc=4, 563 | framealpha=1, 564 | ) 565 | leg.set_zorder(100) 566 | 567 | #######SAVE FIGURE ########### 568 | plt.savefig( 569 | output_dir + "/GFS/gfs_hrly_pytpesound_cape_" + dtfs + ".png", 570 | bbox_inches="tight", 571 | pad_inches=0.1, 572 | ) 573 | fcst_hr = str(0) 574 | plt.close() 575 | plt.clf() 576 | -------------------------------------------------------------------------------- /v0.1/soundingmaps.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | import xarray as xr 4 | import metpy 5 | from datetime import datetime 6 | import datetime as dt 7 | from metpy.units import units 8 | from metpy.plots import SkewT 9 | import matplotlib.patches as mpatches 10 | import matplotlib.lines as lines 11 | import metpy.calc as mpcalc 12 | 13 | def plot_soundings(fig,ax,temp,rh,centerlat,centerlon,domainsize,cape): 14 | ''' 15 | This function will plot a bunch of little soundings onto a matplotlib fig,ax. 16 | 17 | temp is an xarray dataarray with temperature data on pressure levels at least 18 | between 1000 and 300mb (you can change the ylimits for other datasets) 19 | 20 | rh is an xarray dataarray with temperature data on pressure levels at least 21 | between 1000 and 300mb (you can change ) 22 | 23 | centerlat and centerlon are the coordinates around which you want your map 24 | to be centered. both are floats or integers and are in degrees of latitude 25 | and degrees of longitude west (i.e. 70W would be input as positive 70 here) 26 | 27 | domainsize is a string either 'local' for ~WFO-size domains or 'regional' for 28 | NE/SE/Mid-Atlantic-size domains (12 deg lat by 15 deg lon). More will be added soon. 29 | 30 | cape is a boolean to indicate whether you want to overlay parcel paths and 31 | shade CAPE/CIN on soundings with >100 J/kg of CAPE (this value can be changed) 32 | 33 | note that this function doesn't "return" anything but if you just call it and 34 | provide the right arguments, it works. 35 | 36 | for example: 37 | import soundingmaps as smap 38 | ... 39 | smap.plot_soundings(fig,ax1,data['temperature'],data['rh'],30.5,87.5,'local',cape=True) 40 | 41 | ''' 42 | r=5 43 | if domainsize=='local': 44 | init_lat_delt = 1.625 45 | init_lon_delt = 0.45 46 | lat_delts = [.2,.7,1.2,1.75,2.25,2.8] 47 | londelt = 0.76 48 | startlon = centerlon-2+0.45 49 | 50 | elif domainsize=='regional': 51 | init_lat_delt = 6 52 | init_lon_delt = 1.6 53 | lat_delts = [0.6,2.5,4.5,6.4,8.4,10.25] 54 | londelt = 2.9 55 | startlon = centerlon-7.5+1.6 56 | 57 | startlat = centerlat-init_lat_delt 58 | startlon = centerlon-2+0.45 59 | 60 | sound_lats=[] 61 | sound_lons=[] 62 | for i in range(0,6): 63 | lats = startlat+lat_delts[i] 64 | sound_lats.append(lats) 65 | 66 | for i in range(0,r): 67 | lons = -startlon-(londelt*i) 68 | sound_lons.append(lons) 69 | 70 | plot_elevs=[0.2,0.3,0.4,0.5,0.6,0.7] 71 | 72 | dashed_red_line = lines.Line2D([], [], linestyle='solid', color='r', label='Temperature') 73 | dashed_purple_line = lines.Line2D([],[],linestyle='dashed',color='purple',label='0C Isotherm') 74 | dashed_green_line = lines.Line2D([], [], linestyle='solid', color='g', label='Dew Point') 75 | grey_line = lines.Line2D([], [], color='darkgray', label='MSLP (hPa)') 76 | blue_line = lines.Line2D([], [], color='b',label='2m 0C Isotherm') 77 | pink_line = lines.Line2D([], [], color='fuchsia',label='Surface-Based Parcel Path') 78 | red = mpatches.Patch(color='tab:red',label='CAPE') 79 | blue = mpatches.Patch(color='tab:blue',label='CIN') 80 | 81 | if cape==True: 82 | for k in range(len(plot_elevs)): 83 | soundlat = sound_lats[k] 84 | plot_elev = plot_elevs[k] 85 | 86 | if k==0: 87 | s=1 88 | else: 89 | s=0 90 | 91 | for i in range(s,r): 92 | sound_pres = temp.lev 93 | soundlon = -(startlon+(londelt*i)) 94 | sound_temps = temp.interp(lat=soundlat,lon=soundlon)-273.15 95 | sound_rh = rh.interp(lat=soundlat,lon=soundlon) 96 | sound_dp = mpcalc.dewpoint_from_relative_humidity(sound_temps.data*units.degC,sound_rh.data*units.percent) 97 | skew = SkewT(fig=fig,rect=(0.75-(0.15*i),plot_elev,.15,.1)) 98 | 99 | parcel_prof = mpcalc.parcel_profile(sound_pres,sound_temps[0].data*units.degC,sound_dp[0]) 100 | cape = mpcalc.cape_cin(sound_pres,sound_temps.data*units.degC,sound_dp,parcel_prof) 101 | capeout = int(cape[0].m) 102 | cinout = int(cape[1].m) 103 | 104 | skew.plot(sound_pres,sound_dp,'g',linewidth=3) 105 | skew.plot(sound_pres,sound_temps,'r',linewidth=3) 106 | 107 | if capeout >100: 108 | # Shade areas of CAPE and CIN 109 | print(sound_temps) 110 | print(parcel_prof) 111 | skew.shade_cin(sound_pres, sound_temps.data*units.degC, parcel_prof) 112 | skew.shade_cape(sound_pres, sound_temps.data*units.degC, parcel_prof) 113 | skew.plot(sound_pres,parcel_prof,color='fuchsia',linewidth=1) 114 | 115 | skew.ax.axvline(0, color='purple', linestyle='--', linewidth=3) 116 | skew.ax.set_ylim((1000,300)) 117 | skew.ax.axis('off') 118 | 119 | leg = ax.legend(handles=[dashed_red_line,dashed_green_line,dashed_purple_line,pink_line,red,blue],title='Sounding Legend',loc=4,framealpha=1) 120 | 121 | else: 122 | for k in range(len(plot_elevs)): 123 | soundlat = sound_lats[k] 124 | plot_elev = plot_elevs[k] 125 | 126 | if k==0: 127 | s=1 128 | else: 129 | s=0 130 | 131 | for i in range(s,r): 132 | soundlon = -(startlon+(londelt*i)) 133 | sound_pres = temp.lev 134 | sound_temps = temp.interp(lat=soundlat,lon=soundlon)-273.15 135 | sound_rh = rh.interp(lat=soundlat,lon=soundlon) 136 | sound_dp = mpcalc.dewpoint_from_relative_humidity(sound_temps.data*units.degC,sound_rh.data*units.percent) 137 | skew = SkewT(fig=fig,rect=(0.75-(0.15*i),plot_elev,.15,.1)) 138 | skew.plot(sound_pres,sound_dp,'g',linewidth=3) 139 | skew.plot(sound_pres,sound_temps,'r',linewidth=3) 140 | skew.ax.axvline(0, color='purple', linestyle='--', linewidth=3) 141 | skew.ax.set_ylim((1000,300)) 142 | skew.ax.axis('off') 143 | leg = ax.legend(handles=[dashed_red_line,dashed_green_line,dashed_purple_line],title='Sounding Legend',loc=4,framealpha=1) 144 | --------------------------------------------------------------------------------