From 4ed448c1b26255b34568fb119901ea2a7351b730 Mon Sep 17 00:00:00 2001 From: David Ahijevych Date: Wed, 24 Jan 2018 11:29:04 -0700 Subject: [PATCH 01/68] first commit --- webplot.py | 761 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 761 insertions(+) create mode 100755 webplot.py diff --git a/webplot.py b/webplot.py new file mode 100755 index 0000000..8ce6c4d --- /dev/null +++ b/webplot.py @@ -0,0 +1,761 @@ +import matplotlib.colors as colors +import matplotlib.pyplot as plt +from mpl_toolkits.basemap import * +from datetime import * +import cPickle as pickle +import os, sys, time, argparse +import scipy.ndimage as ndimage +from scipy import interpolate +import subprocess +from fieldinfo import * +from netCDF4 import Dataset, MFDataset + +class webPlot: + '''A class to plot data from NCAR ensemble''' + def __init__(self): + self.opts = parseargs() + self.initdate = datetime.strptime(self.opts['date'], '%Y%m%d%H') + self.title = self.opts['title'] + self.debug = self.opts['debug'] + self.autolevels = self.opts['autolevels'] + self.domain = self.opts['domain'] + if ',' in self.opts['timerange']: self.shr, self.ehr = map(int, self.opts['timerange'].split(',')) + else: self.shr, self.ehr = int(self.opts['timerange']), int(self.opts['timerange']) + self.createFilename() + self.ENS_SIZE = int(os.getenv('ENS_SIZE', 10)) + + def createFilename(self): + for f in ['fill', 'contour','barb']: # CSS added this for loop and everything in it + if 'name' in self.opts[f]: + if 'thresh' in self.opts[f]: + prefx = self.opts[f]['name']+'_'+self.opts[f]['ensprod']+'_'+str(self.opts[f]['thresh']) # CSS + else: + prefx = self.opts[f]['name']+'_'+self.opts[f]['ensprod'] # CSS + break + + if self.shr == self.ehr: # CSS + self.outfile = prefx+'_f'+'%03d'%self.shr+'_'+self.domain+'.png' # 'test.png' # CSS + else: # CSS + self.outfile = prefx+'_f'+'%03d'%self.shr+'-f'+'%03d'%self.ehr+'_'+self.domain+'.png' # 'test.png' # CSS + + def loadMap(self): + PYTHON_SCRIPTS_DIR = os.getenv('PYTHON_SCRIPTS_DIR', '/glade/u/home/wrfrt/rt_ensemble/python_scripts') + self.fig, self.ax, self.m = pickle.load(open('%s/rt2015_%s.pk'%(PYTHON_SCRIPTS_DIR,self.domain), 'r')) + lats, lons = readGrid(PYTHON_SCRIPTS_DIR) + self.x, self.y = self.m(lons,lats) + + def readEnsemble(self): + self.data, self.missing_members = readEnsemble(self.initdate, timerange=[self.shr,self.ehr], fields=self.opts, debug=self.debug, ENS_SIZE=self.ENS_SIZE) + + def plotDepartures(self): + from collections import OrderedDict + hly_inventory, hly_forecast = OrderedDict(), OrderedDict() + + fieldname = self.opts['fill']['name'] + if fieldname == 't2depart': normals = open('/glade/u/home/sobash/RT2015_gpx/hly-temp-normal.txt').readlines() + elif fieldname == 'td2depart': normals = open('/glade/u/home/sobash/RT2015_gpx/hly-dewp-normal.txt').readlines() + + # figure out local times for this UTC time because NCDC is stupid + utcoffset = (time.mktime(time.gmtime()) - time.mktime(time.localtime()))/3600.0 #utc to local time offset for mountain time + monthday = (self.initdate + timedelta(hours=self.shr)).strftime(' %m %d ') #search for this string in normals file (see below) + hr_pt = (self.initdate + timedelta(hours=self.shr) - timedelta(hours=utcoffset+1)).hour # hour in pacific time + hr_mt = (self.initdate + timedelta(hours=self.shr) - timedelta(hours=utcoffset)).hour # hour in mountain time + hr_ct = (self.initdate + timedelta(hours=self.shr) - timedelta(hours=utcoffset-1)).hour # hour in central time + hr_et = (self.initdate + timedelta(hours=self.shr) - timedelta(hours=utcoffset-2)).hour # hour in eastern time + + # get interpolated forecast at climate station locations (in F) + fi = interpolate.RegularGridInterpolator((self.y[:,0], self.x[0,:]), self.data['fill'][0], fill_value=-9999, bounds_error=False, method='linear') + with open('/glade/u/home/sobash/RT2015_gpx/hly-inventory.txt') as f: + for line in f: + stn = line.split() + lonlat = map(float, stn[1:3][::-1]) + x_ob, y_ob = self.m(*zip(lonlat)) + hly_forecast[stn[0]] = fi((y_ob,x_ob))[0] + hly_inventory[stn[0]] = (y_ob[0], x_ob[0], lonlat[0], lonlat[1]) + + # read in file of normals for this hour and plot departures + if fieldname == 't2depart': cmap = plt.get_cmap('RdBu_r') + elif fieldname == 'td2depart': cmap = plt.get_cmap('BrBG') + norm = colors.BoundaryNorm([-50,-19.999,-14.999,-9.999,-4.999,0,5,10,15,20,50], cmap.N) #want to have bins so they both dont include bndrys + self.m.set_axes_limits(ax=self.ax) # need to do this if no other basemap functions have been called + + for line in normals: + if monthday in line: + stn = line.split() + try: y_ob, x_ob, lon, lat = hly_inventory[stn[0]] + except: continue + + if lon < -114: localhr = hr_pt #PT + elif lon > -102 and lon <= -85: localhr = hr_ct #CT + elif lon > -85: localhr = hr_et #ET + else: localhr = hr_mt #MT + + forecast = hly_forecast[stn[0]] + normal = float(stn[localhr+3][:-1])/10.0 + departure = float(forecast - normal) + + if x_ob < self.m.xmax and x_ob > self.m.xmin and y_ob < self.m.ymax and y_ob > self.m.ymin and normal > -50: + if abs(departure) < 5: size = 5**2 + elif abs(departure) < 10: size = 8**2 + elif abs(departure) < 15: size = 11**2 + elif abs(departure) < 20: size = 13**2 + else: size = 15**2 + cs = self.m.scatter(x_ob, y_ob, s=size, c=departure, cmap=cmap, norm=norm, linewidths=1.25, ax=self.ax) + + # make custom legend + fontdict = {'family':'monospace', 'size':9 } + x0, y0 = self.ax.transAxes.transform((0,0)) + x, y = self.fig.transFigure.inverted().transform((x0+10,y0+10)) + w, h = self.fig.get_size_inches()*self.fig.dpi + cax = self.fig.add_axes([x,y,130/float(w),114/float(h)]) + cax.set_axis_bgcolor('#dddddd') + cax.set_xticks([]) + cax.set_yticks([]) + for i in cax.spines.itervalues(): i.set_linewidth(0.5) + + sizes, start_y, start_c, labels = [5,8,11,13,15], 0.84, 1, ['0-5F', '5-10F', '10-15F', '15-20F', '>= 20F'] + cax.text(0.2,0.94,'Below', va='center', ha='center', fontdict=fontdict, transform=cax.transAxes) + cax.text(0.8,0.94,'Above', va='center', ha='center', fontdict=fontdict, transform=cax.transAxes) + for i in range(len(sizes)): + cax.scatter(0.2,start_y-i*0.18,s=sizes[i]**2,c=(-start_c-i*5), cmap=cmap, norm=norm, transform=cax.transAxes) + cax.text(0.5,start_y-i*0.18,labels[i], va='center', ha='center', fontdict=fontdict, transform=cax.transAxes) + cax.scatter(0.8,start_y-i*0.18,s=sizes[i]**2,c=(start_c+i*5), cmap=cmap, norm=norm, transform=cax.transAxes) + + def interpolateData(self): + with open('stations.txt') as f: content = f.readlines() + latlons = [ [ float(num[:-1]) for num in line.split()[-2:] ] for line in content ] + latlons = np.array(latlons)[:,::-1] + latlons[:,0] = -1*latlons[:,0] + + #lats, lons = readGrid() + #latlons = zip(lons[10::40,10::40].flatten(), lats[10::40,10::40].flatten()) + + f = interpolate.RegularGridInterpolator((self.y[:,0], self.x[0,:]), self.data['fill'][0], fill_value=-9999, bounds_error=False, method='linear') + x_ob, y_ob = self.m(*zip(*latlons)) + fcst_val = f((y_ob,x_ob)) + + fontdict = {'family':'monospace', 'size':9 } + self.ax.cla() + self.ax.axis('off') + for i in range(fcst_val.size): + if x_ob[i] < self.m.xmax and x_ob[i] > self.m.xmin and y_ob[i] < self.m.ymax and y_ob[i] > self.m.ymin and fcst_val[i] != -9999: + self.ax.text(x_ob[i], y_ob[i], int(round(fcst_val[i])), fontdict=fontdict, ha='center', va='center') + + def plotReports(self): + import csv, re, urllib2 + + url = 'http://www.spc.noaa.gov/climo/reports/%s_rpts_raw.csv'%(self.initdate.strftime('%y%m%d')) + #url = 'http://www.spc.noaa.gov/climo/reports/yesterday.csv' + print url + response = urllib2.urlopen(url) + cr = csv.reader(response) + + lats, lons, type = [], [], [] + report_type = 'hail' + for row in cr: + if re.search('Hail', row[0]): report_type = 'hail' + if re.search('Wind', row[0]): report_type = 'wind' + if re.search('Tornado', row[0]): report_type = 'torn' + + if re.search('^\d{4}', row[0]): + lats.append(float(row[5])) + lons.append(float(row[6])) + type.append(report_type) + + x_ob, y_ob = self.m(lons, lats) + self.m.scatter(x_ob, y_ob, color='k', edgecolor='k', s=3, ax=self.ax) + + def plotWarnings(self): + self.m.readshapefile('/glade/u/home/sobash/SHARPpy/OBS/sbw_shp/%s'%self.initdate.strftime('%Y%m%d12'),'warnings',drawbounds=True, linewidth=1, color='black', ax=self.ax) + + def plotTitleTimes(self): + fontdict = {'family':'monospace', 'size':12, 'weight':'bold'} + + # place title and times above corners of map + x0, y1 = self.ax.transAxes.transform((0,1)) + x0, y0 = self.ax.transAxes.transform((0,0)) + x1, y1 = self.ax.transAxes.transform((1,1)) + self.ax.text(x0, y1+10, self.title, fontdict=fontdict, transform=None) + + initstr = self.initdate.strftime('Init: %a %Y-%m-%d %H UTC') + if ((self.ehr - self.shr) == 0): + validstr = (self.initdate+timedelta(hours=self.shr)).strftime('Valid: %a %Y-%m-%d %H UTC') + else: + validstr1 = (self.initdate+timedelta(hours=(self.shr-1))).strftime('%a %Y-%m-%d %H UTC') + validstr2 = (self.initdate+timedelta(hours=self.ehr)).strftime('%a %Y-%m-%d %H UTC') + validstr = "Valid: %s - %s"%(validstr1, validstr2) + + self.ax.text(x1, y1+20, initstr, horizontalalignment='right', transform=None) + self.ax.text(x1, y1+5, validstr, horizontalalignment='right', transform=None) + + # Plot missing members (use wrfout count here, if upp missing this wont show that) + if len(self.missing_members['wrfout']) > 0: + missing_members = sorted(set([ (x%10)+1 for x in self.missing_members['wrfout'] ])) #get member number from missing indices + missing_members_string = ', '.join(str(x) for x in missing_members) + self.ax.text(x1-5, y0+5, 'Missing member #s: %s'%missing_members_string, horizontalalignment='right', transform=None) + + def plotFields(self): + if 'fill' in self.data: + if self.opts['fill']['ensprod'] == 'paintball': self.plotPaintball() + elif self.opts['fill']['ensprod'] == 'stamp': self.plotStamp() + else: self.plotFill() + + if 'contour' in self.data: + if self.opts['contour']['ensprod'] == 'spaghetti': self.plotSpaghetti() + elif self.opts['contour']['ensprod'] == 'stamp': self.plotStamp() + else: self.plotContour() + + if 'barb' in self.data: + #self.plotStreamlines() + self.plotBarbs() + + if self.opts['interp']: self.interpolateData() + if self.opts['fill']['name'] in ['t2depart', 'td2depart']: self.plotDepartures() + + def plotFill(self): + if self.opts['fill']['name'] == 'ptype': self.plotFill_ptype(); return + elif self.opts['fill']['name'] == 'crefuh': self.plotReflectivityUH(); return + + if self.autolevels: + min, max = self.data['fill'][0].min(), self.data['fill'][0].max() + levels = np.linspace(min, max, num=10) + cmap = colors.ListedColormap(self.opts['fill']['colors']) + norm = colors.BoundaryNorm(levels, cmap.N) + tick_labels = levels[:-1] + else: + levels = self.opts['fill']['levels'] + cmap = colors.ListedColormap(self.opts['fill']['colors']) + extend, extendfrac = 'neither', 0.0 + tick_labels = levels[:-1] + if self.opts['fill']['ensprod'] in ['prob', 'neprob', 'problt', 'probgt', 'neprobgt', 'neproblt']: + cmap = colors.ListedColormap(self.opts['fill']['colors'][:9]) + cmap.set_over(self.opts['fill']['colors'][-1]) + extend, extendfrac = 'max', 0.02 + tick_labels = levels + norm = colors.BoundaryNorm(levels, cmap.N) + + # smooth some of the fill fields + if self.opts['fill']['name'] == 'avo500': self.data['fill'][0] = ndimage.gaussian_filter(self.data['fill'][0], sigma=4) + if self.opts['fill']['name'] == 'pbmin': self.data['fill'][0] = ndimage.gaussian_filter(self.data['fill'][0], sigma=2) + + cs1 = self.m.contourf(self.x, self.y, self.data['fill'][0], levels=levels, cmap=cmap, norm=norm, extend='max', ax=self.ax) + + self.plotColorbar(cs1, levels, tick_labels, extend, extendfrac) + + def plotFill_ptype(self): + ml_type = np.zeros(self.data['fill'][0].shape) + ml_type_prob = np.zeros(self.data['fill'][0].shape) + + for i in [1,2,3,4]: + pts = (self.data['fill'][i-1] > ml_type_prob+0.001) + ml_type_prob[pts] = self.data['fill'][i-1][pts] + ml_type[pts] = i+0.001 + + cmap = colors.ListedColormap(['#7BBF6A', 'red', 'orange', 'blue']) + norm = colors.BoundaryNorm([1,2,3,4,5], cmap.N) + + x = (self.x[1:,1:] + self.x[:-1,:-1])/2.0 + y = (self.y[1:,1:] + self.y[:-1,:-1])/2.0 + cs1 = self.m.pcolormesh(x, y, np.ma.masked_equal(ml_type[1:,1:], 0), cmap=cmap, norm=norm, edgecolors='None', ax=self.ax) + + # make axes for colorbar, 175px to left and 30px down from bottom of map + x0, y0 = self.ax.transAxes.transform((0,0)) + x, y = self.fig.transFigure.inverted().transform((x0+175,y0-29.5)) + cax = self.fig.add_axes([x,y,0.985-x,y/3.0]) + cb = plt.colorbar(cs1, cax=cax, orientation='horizontal') + cb.outline.set_linewidth(0.5) + cb.set_ticks([0.5,1.5,2.5,3.5,4.5,5.5]) + cb.set_ticklabels(['Rain', 'Freezing Rain', 'Sleet', 'Snow']) + cb.ax.tick_params(length=0) + + def plotReflectivityUH(self): + levels = self.opts['fill']['levels'] + cmap = colors.ListedColormap(self.opts['fill']['colors']) + norm = colors.BoundaryNorm(levels, cmap.N) + tick_labels = levels[:-1] + + cs1 = self.m.contourf(self.x, self.y, self.data['fill'][0], levels=levels, cmap=cmap, norm=norm, extend='max', ax=self.ax) + self.m.contourf(self.x, self.y, self.data['fill'][1], levels=[50,1000], colors='black', ax=self.ax, alpha=0.3) + self.m.contour(self.x, self.y, self.data['fill'][1], levels=[50], colors='k', linewidth=0.5, ax=self.ax) + + #maxuh = self.data['fill'][1].max() + #self.ax.text(0.03,0.03,'Domain-wide UH max %0.f'%maxuh ,ha="left",va="top",bbox=dict(boxstyle="square",lw=0.5,fc="white"), transform=self.ax.transAxes) + + self.plotColorbar(cs1, levels, tick_labels) + + def plotColorbar(self, cs, levels, tick_labels, extend='neither', extendfrac=0.0): + # make axes for colorbar, 175px to left and 30px down from bottom of map + x0, y0 = self.ax.transAxes.transform((0,0)) + x, y = self.fig.transFigure.inverted().transform((x0+175,y0-29.5)) + cax = self.fig.add_axes([x,y,0.985-x,y/3.0]) + cb = plt.colorbar(cs, cax=cax, orientation='horizontal', extend=extend, extendfrac=extendfrac, ticks=tick_labels) + cb.outline.set_linewidth(0.5) + + def plotContour(self): + data = ndimage.gaussian_filter(self.data['contour'][0], sigma=10) + if self.opts['contour']['name'] in ['sbcinh','mlcinh']: linewidth, alpha = 0.5, 0.75 + else: linewidth, alpha = 1.5, 1.0 + cs2 = self.m.contour(self.x, self.y, data, levels=self.opts['contour']['levels'], colors='k', linewidths=linewidth, ax=self.ax, alpha=alpha) + plt.clabel(cs2, fontsize='small', fmt='%i') + + def plotBarbs(self): + skip = self.opts['barb']['skip'] + if self.domain != 'CONUS': skip = 20 + + if self.opts['fill']['name'] == 'crefuh': alpha=0.5 + else: alpha=1.0 + + cs2 = self.m.barbs(self.x[::skip,::skip], self.y[::skip,::skip], self.data['barb'][0][::skip,::skip], self.data['barb'][1][::skip,::skip], \ + color='black', alpha=alpha, length=5.5, linewidth=0.25, sizes={'emptybarb':0.05}, ax=self.ax) + + def plotStreamlines(self): + speed = np.sqrt(self.data['barb'][0]**2 + self.data['barb'][1]**2) + lw = 5*speed/speed.max() + cs2 = self.m.streamplot(self.x[0,:], self.y[:,0], self.data['barb'][0], self.data['barb'][1], color='k', density=3, linewidth=lw, ax=self.ax) + cs2.lines.set_alpha(0.5) + cs2.arrows.set_alpha(0.5) #apparently this doesn't work? + + def plotPaintball(self): + rects, labels = [], [] + colorlist = self.opts['fill']['colors'] + levels = self.opts['fill']['levels'] + for i in range(self.data['fill'][0].shape[0]): + cs = self.m.contourf(self.x, self.y, self.data['fill'][0][i,:], levels=levels, colors=[colorlist[i]], ax=self.ax, alpha=0.5) + rects.append(plt.Rectangle((0,0),1,1,fc=colorlist[i])) + labels.append("member %d"%(i+1)) + + plt.legend(rects, labels, ncol=5, loc='right', bbox_to_anchor=(1.0,-0.05), fontsize=11, \ + frameon=False, borderpad=0.25, borderaxespad=0.25, handletextpad=0.2) + + def plotSpaghetti(self): + proxy = [] + colorlist = self.opts['contour']['colors'] + levels = self.opts['contour']['levels'] + data = ndimage.gaussian_filter(self.data['contour'][0], sigma=[0,10,10]) + for i in range(data.shape[0]): + #cs = self.m.contour(self.x, self.y, data[i,:], levels=levels, colors=[colorlist[i]], linewidths=2, linestyles='solid', ax=self.ax) + cs = self.m.contour(self.x, self.y, data[i,:], levels=levels, colors='k', linewidths=1, linestyles='solid', ax=self.ax) + proxy.append(plt.Rectangle((0,0),1,1,fc=colorlist[i])) + #plt.legend(proxy, ["member %d"%i for i in range(1,11)], ncol=5, loc='right', bbox_to_anchor=(1.0,-0.05), fontsize=11, \ + # frameon=False, borderpad=0.25, borderaxespad=0.25, handletextpad=0.2) + + def plotStamp(self): + fig_width_px, dpi = 1280, 90 + fig = plt.figure(dpi=dpi) + + num_rows, num_columns = 3, 4 + fig_width = fig_width_px/dpi + width_per_panel = fig_width/float(num_columns) + height_per_panel = width_per_panel*self.m.aspect + fig_height = height_per_panel*num_rows + fig_height_px = fig_height*dpi + fig.set_size_inches((fig_width, fig_height)) + + levels = self.opts['fill']['levels'] + cmap = colors.ListedColormap(self.opts['fill']['colors']) + norm = colors.BoundaryNorm(levels, cmap.N) + filename = self.opts['fill']['filename'] + + memberidx = 0 + for j in range(0,num_rows): + for i in range(0,num_columns): + member = num_columns*j+i + if member > 9: break + spacing_w, spacing_h = 5/float(fig_width_px), 5/float(fig_height_px) + spacing_w = 10/float(fig_width_px) + x, y = i*width_per_panel/float(fig_width), 1.0 - (j+1)*height_per_panel/float(fig_height) + w, h = (width_per_panel/float(fig_width))-spacing_w, (height_per_panel/float(fig_height))-spacing_h + if member == 9: y = 0 + + #print 'member', member, 'creating axes at', x, y + thisax = fig.add_axes([x,y,w,h]) + + thisax.axis('on') + for axis in ['top','bottom','left','right']: thisax.spines[axis].set_linewidth(0.5) + self.m.drawcoastlines(ax=thisax, linewidth=0.3) + self.m.drawstates(linewidth=0.15, ax=thisax) + self.m.drawcountries(ax=thisax, linewidth=0.3) + thisax.text(0.03,0.97,member+1,ha="left",va="top",bbox=dict(boxstyle="square",lw=0.5,fc="white"), transform=thisax.transAxes) + + # plot, unless file that has fill field is missing, then skip + if member not in self.missing_members[filename] and member < self.ENS_SIZE: + cs1 = self.m.contourf(self.x, self.y, self.data['fill'][0][memberidx,:], levels=levels, cmap=cmap, norm=norm, extend='max', ax=thisax) + memberidx += 1 + + # use every other tick for large colortables, remove last tick label for both + if self.opts['fill']['name'] in ['goesch3', 'goesch4', 't2', 'precipacc' ]: ticks = levels[:-1][::2] # CSS added precipacc + else: ticks = levels[:-1] + + # add colorbar to figure + cax = fig.add_axes([0.51,0.3,0.48,0.02]) + cb = plt.colorbar(cs1, cax=cax, orientation='horizontal', ticks=ticks, extendfrac=0.0) + cb.outline.set_linewidth(0.5) + cb.ax.tick_params(labelsize=9) + + # add init/valid text + fontdict = {'family':'monospace', 'size':13, 'weight':'bold'} + initstr = self.initdate.strftime(' Init: %a %Y-%m-%d %H UTC') + if ((self.ehr - self.shr) == 0): + validstr = (self.initdate+timedelta(hours=self.shr)).strftime('Valid: %a %Y-%m-%d %H UTC') + else: + validstr1 = (self.initdate+timedelta(hours=(self.shr-1))).strftime('%a %Y-%m-%d %H UTC') + validstr2 = (self.initdate+timedelta(hours=self.ehr)).strftime('%a %Y-%m-%d %H UTC') + validstr = "Valid: %s - %s"%(validstr1, validstr2) + + fig.text(0.51, 0.22, self.title, fontdict=fontdict, transform=fig.transFigure) + fig.text(0.51, 0.22 - 25/float(fig_height_px), initstr, transform=fig.transFigure) + fig.text(0.51, 0.22 - 40/float(fig_height_px), validstr, transform=fig.transFigure) + + # add NCAR logo and text below logo + x, y = fig.transFigure.transform((0.51,0)) + fig.figimage(plt.imread('ncar.png'), xo=x, yo=y+15, zorder=1000) + plt.text(x+10, y+5, 'ensemble.ucar.edu', fontdict={'size':9, 'color':'#505050'}, transform=None) + + def saveFigure(self, trans=False): + # place NCAR logo 57 pixels below bottom of map, then save image + if 'ensprod' in self.opts['fill']: # CSS needed incase not a fill plot + if not trans and self.opts['fill']['ensprod'] != 'stamp': + x, y = self.ax.transAxes.transform((0,0)) + self.fig.figimage(plt.imread('ncar.png'), xo=x, yo=(y-44), zorder=1000) + plt.text(x+10, y-54, 'ensemble.ucar.edu', fontdict={'size':9, 'color':'#505050'}, transform=None) + + plt.savefig(self.outfile, dpi=90, transparent=trans) + + if self.opts['convert']: + #command = 'convert -colors 255 %s %s'%(self.outfile, self.outfile) + if not self.opts['fill']: ncolors = 48 + elif self.opts['fill']['ensprod'] in ['prob', 'neprob', 'probgt', 'problt', 'neprobgt', 'neproblt']: ncolors = 48 + else: ncolors = 255 + command = '/glade/u/home/sobash/pngquant/pngquant %d %s --ext=.png --force'%(ncolors,self.outfile) + ret = subprocess.check_call(command.split()) + plt.clf() + +def parseargs(): + '''Parse arguments and return dictionary of fill, contour and barb field parameters''' + + parser = argparse.ArgumentParser(description='Web plotting script for NCAR ensemble') + parser.add_argument('-d', '--date', default=datetime.utcnow().strftime('%Y%m%d00'), help='initialization datetime (YYYYMMDDHH)') + parser.add_argument('-tr', '--timerange', required=True, help='time range of forecasts (START,END)') + parser.add_argument('-f', '--fill', help='fill field (FIELD_PRODUCT_THRESH), field keys:'+','.join(fieldinfo.keys())) + parser.add_argument('-c', '--contour', help='contour field (FIELD_PRODUCT_THRESH)') + parser.add_argument('-b', '--barb', help='barb field (FIELD_PRODUCT_THRESH)') + parser.add_argument('-bs', '--barbskip', help='barb skip interval') + parser.add_argument('-t', '--title', help='title for plot') + parser.add_argument('-dom', '--domain', default='CONUS', help='domain to plot') + parser.add_argument('-al', '--autolevels', action='store_true', help='use min/max to determine levels for plot') + parser.add_argument('-con', '--convert', default=True, action='store_false', help='run final image through imagemagick') + parser.add_argument('-i', '--interp', action='store_true', help='plot interpolated station values') + parser.add_argument('-sig', '--sigma', default=2, help='smooth probabilities using gaussian smoother') + parser.add_argument('--debug', action='store_true', help='turn on debugging') + + opts = vars(parser.parse_args()) + # opts = { 'date':date, 'timerange':timerange, 'fill':'sbcape_prob_25', 'ensprod':'mean' ... } + + # now, convert underscore delimited fill, contour, and barb args into dicts + for f in ['contour','barb','fill']: + thisdict = {} + if opts[f] is not None: + input = opts[f].lower().split('_') + + thisdict['name'] = input[0] + thisdict['ensprod'] = input[1] + thisdict['arrayname'] = fieldinfo[input[0]]['fname'] + + # assign contour levels and colors + if (input[1] in ['prob', 'neprob', 'problt', 'probgt', 'neprobgt', 'neproblt']): + thisdict['thresh'] = float(input[2]) + thisdict['levels'] = np.arange(0.1,1.1,0.1) + thisdict['colors'] = readNCLcm('perc2_9lev') + elif (input[1] in ['paintball', 'spaghetti']): + thisdict['thresh'] = float(input[2]) + thisdict['levels'] = [float(input[2]), 1000] + thisdict['colors'] = readNCLcm('GMT_paired') + elif (input[1] == 'var'): + if (input[0][0:3] == 'hgt'): + thisdict['levels'] = [2,4,6,8,10,15,20,25,30,35,40,45,50,55,60,65,70,75] #hgt + thisdict['colors'] = readNCLcm('wind_17lev') + elif (input[0][0:3] == 'spe'): + thisdict['levels'] = [1,2,3,4,5,6,7,8,9,10,12.5,15,20,25,30,35,40,45] #iso + thisdict['colors'] = readNCLcm('wind_17lev') + else: + thisdict['levels'] = [0.5,1,1.5,2,3,4,5,6,7,8,10] #tmp/td + thisdict['colors'] = readNCLcm('perc2_9lev') + elif 'levels' in fieldinfo[input[0]]: + thisdict['levels'] = fieldinfo[input[0]]['levels'] + thisdict['colors'] = fieldinfo[input[0]]['cmap'] + + # get vertical array index for 3D array fields + if 'arraylevel' in fieldinfo[input[0]]: + thisdict['arraylevel'] = fieldinfo[input[0]]['arraylevel'] + + # get barb-skip for barb fields + if opts['barbskip'] is not None: thisdict['skip'] = int(opts['barbskip']) + elif 'skip' in fieldinfo[input[0]]: thisdict['skip'] = fieldinfo[input[0]]['skip'] + + # get filename + if 'filename' in fieldinfo[input[0]]: thisdict['filename'] = fieldinfo[input[0]]['filename'] + else: thisdict['filename'] = 'wrfout' + + opts[f] = thisdict + return opts + +def makeEnsembleList(wrfinit, timerange, ENS_SIZE): + # create lists of files (and missing file indices) for various file types + shr, ehr = timerange + file_list = { 'wrfout':[], 'upp': [], 'diag':[] } + missing_list = { 'wrfout':[], 'upp': [], 'diag':[] } + + EXP_DIR = os.getenv('EXP_DIR', '/glade/scratch/wrfrt/realtime_ensemble/ensf') + + missing_index = 0 + for hr in range(shr,ehr+1): + wrfvalidstr = (wrfinit + timedelta(hours=hr)).strftime('%Y-%m-%d_%H:%M:%S') + yyyymmddhh = wrfinit.strftime('%Y%m%d%H') + for mem in range(1,ENS_SIZE+1): + wrfout = '%s/%s/wrf_rundir/ens_%d/wrfout_d02_%s'%(EXP_DIR,yyyymmddhh,mem,wrfvalidstr) + diag = '%s/%s/wrf_rundir/ens_%d/diags_d02.%s.nc'%(EXP_DIR,yyyymmddhh,mem,wrfvalidstr) + upp = '%s/%s/post_rundir/mem_%d/fhr_%d/WRFTWO%02d.nc'%(EXP_DIR,yyyymmddhh,mem,hr,hr) + if os.path.exists(wrfout): file_list['wrfout'].append(wrfout) + else: missing_list['wrfout'].append(missing_index) + if os.path.exists(diag): file_list['diag'].append(diag) + else: missing_list['diag'].append(missing_index) + if os.path.exists(upp): file_list['upp'].append(upp) + else: missing_list['upp'].append(missing_index) + missing_index += 1 + return (file_list, missing_list) + +def readEnsemble(wrfinit, timerange=None, fields=None, debug=False, ENS_SIZE=10): + ''' Reads in desired fields and returns 2-D arrays of data for each field (barb/contour/field) ''' + if debug: print fields + + datadict = {} + file_list, missing_list = makeEnsembleList(wrfinit, timerange, ENS_SIZE) #construct list of files + + # loop through fill field, contour field, barb field and retrieve required data + for f in ['fill', 'contour', 'barb']: + if not fields[f].keys(): continue + if debug: print 'Reading field:', fields[f]['name'], 'from', fields[f]['filename'] + + # save some variables for use in this function + filename = fields[f]['filename'] + arrays = fields[f]['arrayname'] + fieldtype = fields[f]['ensprod'] + fieldname = fields[f]['name'] + if fieldtype in ['prob', 'neprob', 'problt', 'probgt', 'neprobgt', 'neproblt']: thresh = fields[f]['thresh'] + if fieldtype[0:3]=='mem': member = int(fieldtype[3:]) + + # open Multi-file netcdf dataset + if debug: print file_list[filename] + fh = MFDataset(file_list[filename]) + + # loop through each field, wind fields will have two fields that need to be read + datalist = [] + for n,array in enumerate(arrays): + if debug: print 'Reading', array + + #read in 3D array (times*members,ny,nx) from file object + if 'arraylevel' in fields[f]: + if isinstance(fields[f]['arraylevel'], list): level = fields[f]['arraylevel'][n] + else: level = fields[f]['arraylevel'] + else: level = None + + if level == 'max': data = np.amax(fh.variables[array][:,:,:,:], axis=1) + elif level is None: data = fh.variables[array][:,:,:] + else: data = fh.variables[array][:,level,:,:] + + # change units for certain fields + if array in ['U_PL', 'V_PL', 'UBSHR6','VBSHR6','UBSHR1','VBSHR1','U10','V10', 'U_COMP_STM', 'V_COMP_STM','S_PL','U_COMP_STM_6KM','V_COMP_STM_6KM']: data = data*1.93 # m/s > kt + elif array in ['DEWPOINT_2M', 'T2', 'AFWA_WCHILL', 'AFWA_HEATIDX']: data = (data - 273.15)*1.8 + 32.0 # K > F + elif array in ['PREC_ACC_NC', 'PREC_ACC_C', 'AFWA_PWAT', 'PWAT', 'AFWA_SNOWFALL', 'AFWA_SNOW', 'AFWA_ICE', 'AFWA_FZRA','AFWA_RAIN_HRLY', 'AFWA_ICE_HRLY','AFWA_SNOWFALL_HRLY', 'AFWA_FZRA_HRLY']: data = data*0.0393701 # mm > in + elif array in ['RAINNC', 'GRPL_MAX', 'SNOW_ACC_NC', 'AFWA_HAIL', 'HAILCAST_DIAM_MEAN', 'HAILCAST_DIAM_STD', 'HAILCAST_DIAM_MAX']: data = data*0.0393701 # mm > in + elif array in ['T_PL', 'TD_PL', 'SFC_LI']: data = data - 273.15 # K > C + elif array in ['AFWA_MSLP', 'MSLP']: data = data*0.01 # Pa > hPa + elif array in ['ECHOTOP']: data = data*3.28084# m > ft + elif array in ['AFWA_VIS', 'VISIBILITY']: data = (data*0.001)/1.61 # m > mi + elif array in ['SBCINH', 'MLCINH', 'W_DN_MAX', 'UP_HELI_MIN']: data = data*-1.0 # make cin positive + elif array in ['PVORT_320K']: data = data*1000000 # multiply by 1e6 + elif array in ['SBT123_GDS3_NTAT','SBT124_GDS3_NTAT','GOESE_WV','GOESE_IR']: data = data -273.15 # K -> C + elif array in ['HAIL_MAXK1', 'HAIL_MAX2D']: data = data*39.3701 # m -> inches + elif array in ['PBMIN', 'PBMIN_SFC', 'BESTPBMIN', 'MLPBMIN', 'MUPBMIN']: data = data*0.01 # Pa -> hPa +# elif array in ['LTG1_MAX1', 'LTG2_MAX', 'LTG3_MAX']: data = data*0.20 # scale down excess values + + datalist.append(data) + + # these are derived fields, we don't have in any of the input files but we can compute + if 'name' in fields[f]: + if fieldname in ['shr06mag', 'shr01mag', 'bunkmag','speed10m']: datalist = [np.sqrt(datalist[0]**2 + datalist[1]**2)] + elif fieldname == 'stp': datalist = [computestp(datalist)] + # GSR in fields are T(K), mixing ratio (kg/kg), and surface pressure (Pa) + elif fieldname == 'thetae': datalist = [compute_thetae(datalist)] + elif fieldname == 'rh2m': datalist = [compute_rh(datalist)] + #elif fieldname == 'pbmin': datalist = [ datalist[1] - datalist[0][:,0,:] ] + elif fieldname == 'pbmin': datalist = [ datalist[1] - datalist[0] ] # CSS changed above line for GRIB2 + elif fieldname in ['thck1000-500', 'thck1000-850'] : datalist = [ datalist[1]*0.1 - datalist[0]*0.1 ] # CSS added for thicknesses + elif fieldname == 'winter': datalist = [datalist[1] + datalist[2] + datalist[3]] + + datadict[f] = [] + for data in datalist: + # perform mean/max/variance/etc to reduce 3D array to 2D + if (fieldtype == 'mean'): data = np.mean(data, axis=0) + elif (fieldtype == 'pmm'): data = compute_pmm(data) + elif (fieldtype == 'max'): data = np.amax(data, axis=0) + elif (fieldtype == 'min'): data = np.amin(data, axis=0) + elif (fieldtype == 'var'): data = np.std(data, axis=0) + elif (fieldtype == 'summean'): + for i in missing_list[filename]: data = np.insert(data, i, np.nan, axis=0) #insert nan for missing files + data = np.reshape(data, (data.shape[0]/ENS_SIZE,ENS_SIZE,data.shape[1],data.shape[2])) + data = np.nansum(data, axis=0) + data = np.nanmean(data, axis=0) + elif (fieldtype == 'summax'): + for i in missing_list[filename]: data = np.insert(data, i, np.nan, axis=0) #insert nan for missing files + data = np.reshape(data, (data.shape[0]/ENS_SIZE,ENS_SIZE,data.shape[1],data.shape[2])) + data = np.nansum(data, axis=0) + data = np.nanmax(data, axis=0) + elif (fieldtype == 'summin'): + for i in missing_list[filename]: data = np.insert(data, i, np.nan, axis=0) #insert nan for missing files + data = np.reshape(data, (data.shape[0]/ENS_SIZE,ENS_SIZE,data.shape[1],data.shape[2])) + data = np.nansum(data, axis=0) + data = np.nanmin(data, axis=0) + elif (fieldtype[0:3] == 'mem'): + for i in missing_list[filename]: data = np.insert(data, i, np.nan, axis=0) #insert nan for missing files + data = np.reshape(data, (data.shape[0]/ENS_SIZE,ENS_SIZE,data.shape[1],data.shape[2])) + if fieldname in ['precip', 'precipacc']: data = np.nansum(data, axis=0) + else: data = np.nanmax(data, axis=0) + data = data[member-1,:] + elif (fieldtype in ['prob', 'neprob', 'problt', 'probgt', 'neprobgt', 'neproblt']): + if fieldtype in ['prob', 'neprob', 'probgt', 'neprobgt']: data = (data>=thresh).astype('float') + elif fieldtype in ['problt', 'neproblt']: data = (data 0, temp, 0.0) + return temp.reshape((dy,dx)) + +def compute_neprob(ensemble, roi=0, sigma=0.0, type='gaussian'): + y,x = np.ogrid[-roi:roi+1, -roi:roi+1] + kernel = x**2 + y**2 <= roi**2 + ens_roi = ndimage.filters.maximum_filter(ensemble, footprint=kernel[np.newaxis,:]) + + ens_mean = np.nanmean(ens_roi, axis=0) + #ens_mean = np.nanmean(ensemble, axis=0) + + if (type == 'uniform'): + y,x = np.ogrid[-sigma:sigma+1, -sigma:sigma+1] + kernel = x**2 + y**2 <= sigma**2 + ens_mean = ndimage.filters.convolve(ens_mean, kernel/float(kernel.sum())) + elif (type == 'gaussian'): + ens_mean = ndimage.filters.gaussian_filter(ens_mean, sigma) + return ens_mean + +def computestp(data): + '''Compute STP with data array of [sbcape,sblcl,0-1srh,ushr06,vshr06]''' + sbcape_term = (data[0]/1500.0) + + lcl_term = ((2000.0 - data[1])/1000.0) + lcl_term = np.where(data[1] < 1000.0, 1.0, lcl_term) + lcl_term = np.where(data[1] > 2000.0, 0.0, lcl_term) + + srh_term = (data[2]/150.0) + + shear06 = np.sqrt(data[3]**2 + data[4]**2) #this will be in knots (converted prior to fn) + shear_term = (shear06/38.87) + shear_term = np.where(shear06 > 58.32, 1.5, shear_term) + shear_term = np.where(shear06 < 24.3, 0.0, shear_term) + + stp = (sbcape_term * lcl_term * srh_term * shear_term) + # RS: this stopped working on 24 June 2016 - apparently stp not a masked array? replace with similar call, but may not be needed + #stp = stp.filled(0.0) #fill missing values with 0s (apparently lcl_height missing along boundaries?) + stp = np.ma.filled(stp, 0.0) + return stp + +def compute_thetae(data): + # GSR constants for theta E calc + P0 = 100000.0 # (Pa) + Rd = 287.04 # (J/Kg) + Cp = 1005.7 # (J/Kg) + Lv = 2501000.0 # (J/Kg) + LvoCp = Lv/Cp + RdoCp = Rd/Cp + return (((data[0]-32.0)/1.8)+273.15+LvoCp*data[1]) * ((P0/data[2])**RdoCp) + +def compute_rh(data): + t2 = (data[0]-32.0)/1.8 + 273.15 #temp in K + psfc = data[1] #pres in Pa? + q2 = data[2] #qvapor mixing ratio + L_over_Rv = 5418.12 + + es = 611.0 * np.exp(L_over_Rv*(1.0/273.0 - 1.0/t2)) + qsat = 0.622 * es / (psfc - es) + rh = q2 / qsat + return 100*rh + +def showKeys(): + print fieldinfo.keys() + sys.exit() From 330c3f3633a4cde087eeb2595480826389cb37ff Mon Sep 17 00:00:00 2001 From: David Ahijevych Date: Wed, 24 Jan 2018 11:30:03 -0700 Subject: [PATCH 02/68] first commit --- fieldinfo.py | 179 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100755 fieldinfo.py diff --git a/fieldinfo.py b/fieldinfo.py new file mode 100755 index 0000000..1ace351 --- /dev/null +++ b/fieldinfo.py @@ -0,0 +1,179 @@ +import os + +def readcm(name): + '''Read colormap from file formatted as 0-1 RGB CSV''' + rgb = [] + fh = open(name, 'r') + for line in fh.read().splitlines(): rgb.append(map(float,line.split())) + return rgb + +def readNCLcm(name): + '''Read in NCL colormap for use in matplotlib''' + rgb, appending = [], False +# fh = open('/glade/apps/opt/ncl/6.2.0/intel/12.1.5/lib/ncarg/colormaps/%s.rgb'%name, 'r') + fh = open(os.getenv('NCARG_ROOT','/glade/apps/opt/ncl/6.2.0/intel/12.1.5')+'/lib/ncarg/colormaps/%s.rgb'%name, 'r') # CSS made variable, commented out previous line + for line in fh.read().splitlines(): + if appending: rgb.append(map(float,line.split())) + if ''.join(line.split()) in ['#rgb',';RGB']: appending = True + maxrgb = max([ x for y in rgb for x in y ]) + if maxrgb > 1: rgb = [ [ x/255.0 for x in a ] for a in rgb ] + return rgb + +fieldinfo = { + # surface and convection-related entries + 'precip' :{ 'levels' : [0,0.01,0.05,0.1,0.2,0.3,0.4,0.5,0.75,1,1.5,2,2.5,3.0], 'cmap': [readNCLcm('precip2_17lev')[i] for i in (0,1,2,4,5,6,7,8,10,12,13,14,15)], 'fname':['PREC_ACC_NC'] }, +# 'precip-24hr' :{ 'levels' : [0,0.01,0.05,0.1,0.25,0.5,0.75,1.0,1.25,1.5,2.0,3.0,4.0,5.0,6.0,7.0], 'cmap': [readNCLcm('precip2_17lev')[i] for i in (0,1,2,4,5,6,7,8,10,12,13,14,15,16,17)], 'fname':['PREC_ACC_NC'] }, + 'precip-24hr' :{ 'levels' : [0,0.01,0.05,0.1,0.25,0.5,0.75,1.0,1.25,1.5,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0,12.0,13.0], 'cmap': [readNCLcm('precip2_17lev')[i] for i in (0,1,2,4,5,6,7,8,10,12,13,14,15,16,17)]+['#777777', '#AAAAAA', '#CCCCCC', '#EEEEEE']+[readNCLcm('sunshine_diff_12lev')[i] for i in (4,2,1)], 'fname':['PREC_ACC_NC'] }, +# 'precipacc' :{ 'levels' : [0,0.01,0.05,0.1,0.25,0.5,0.75,1.0,1.25,1.5,2.0,3.0,4.0,5.0,6.0,7.0], 'cmap': [readNCLcm('precip2_17lev')[i] for i in (0,1,2,4,5,6,7,8,10,12,13,14,15,16,17)], 'fname':['RAINNC'] }, + 'precipacc':{ 'levels' : [0,0.01,0.05,0.1,0.25,0.5,0.75,1.0,1.25,1.5,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0,12.0,13.0], 'cmap': [readNCLcm('precip2_17lev')[i] for i in (0,1,2,4,5,6,7,8,10,12,13,14,15,16,17)]+['#777777', '#AAAAAA', '#CCCCCC', '#EEEEEE']+[readNCLcm('sunshine_diff_12lev')[i] for i in (4,2,1)], 'fname':['RAINNC'] }, + 'sbcape' :{ 'levels' : [100,250,500,750,1000,1250,1500,1750,2000,2500,3000,3500,4000,4500,5000,5500,6000], + 'cmap' : ['#eeeeee', '#dddddd', '#cccccc', '#aaaaaa']+readNCLcm('precip2_17lev')[3:-1], 'fname': ['SBCAPE'], 'filename':'upp' }, + 'mlcape' :{ 'levels' : [100,250,500,750,1000,1250,1500,1750,2000,2500,3000,3500,4000,4500,5000,5500,6000], + 'cmap' : ['#eeeeee', '#dddddd', '#cccccc', '#aaaaaa']+readNCLcm('precip2_17lev')[3:-1], 'fname': ['MLCAPE'], 'filename':'upp' }, + 'mucape' :{ 'levels' : [100,250,500,750,1000,1250,1500,1750,2000,2500,3000,3500,4000,4500,5000,5500,6000], + 'cmap' : ['#eeeeee', '#dddddd', '#cccccc', '#aaaaaa']+readNCLcm('precip2_17lev')[3:-1], 'fname': ['MUCAPE'], 'filename':'upp' }, + 'sbcinh' :{ 'levels' : [50,75,100,150,200,250,500], 'cmap': readNCLcm('topo_15lev')[1:], 'fname': ['SBCINH'], 'filename':'upp' }, + 'mlcinh' :{ 'levels' : [50,75,100,150,200,250,500], 'cmap': readNCLcm('topo_15lev')[1:], 'fname': ['MLCINH'], 'filename':'upp' }, + 'pwat' :{ 'levels' : [0.25,0.5,0.75,1.0,1.25,1.5,1.75,2.0,2.5,3.0,3.5,4.0], + 'cmap' : ['#dddddd', '#cccccc', '#e1e1d3', '#e1d5b1', '#ffffd5', '#e5ffa7', '#addd8e', '#41ab5d', '#007837', '#005529', '#0029b1'], + 'fname' : ['PWAT'], 'filename':'upp'}, + 'hailk1' :{ 'levels' : [0.25,0.5,0.75,1.0,1.25,1.5,1.75,2.0,2.5,3.0,3.5,4.0], 'cmap' : ['#eeeeee', '#dddddd', '#cccccc', '#aaaaaa']+readNCLcm('precip2_17lev')[3:-1], 'fname':['HAIL_MAXK1'], 'filename': 'diag' }, + 'hail2d' :{ 'levels' : [0.25,0.5,0.75,1.0,1.25,1.5,1.75,2.0,2.5,3.0,3.5,4.0], 'cmap' : ['#eeeeee', '#dddddd', '#cccccc', '#aaaaaa']+readNCLcm('precip2_17lev')[3:-1], 'fname':['HAIL_MAX2D'], 'filename': 'diag' }, + 'afhail' :{ 'levels' : [0.25,0.5,0.75,1.0,1.25,1.5,1.75,2.0,2.5,3.0,3.5,4.0], 'cmap' : ['#eeeeee', '#dddddd', '#cccccc', '#aaaaaa']+readNCLcm('precip2_17lev')[3:-1], 'fname': ['HAILCAST_DIAM_MAX'], 'filename':'diag' }, + 't2' :{ 'levels' : [-35,-30,-25,-20,-15,-10,-5,0,5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100,105,110,115,120], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['T2'] }, + 't2depart' :{ 'levels' : [-35,-30,-25,-20,-15,-10,-5,0,5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100,105,110,115,120], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['T2'] }, + 'mslp' :{ 'levels' : [960,964,968,972,976,980,984,988,992,996,1000,1004,1008,1012,1016,1020,1024,1028,1032,1036,1040,1044,1048,1052], 'cmap':readNCLcm('nice_gfdl')[3:193], 'fname':['MSLP'], 'filename': 'upp' }, + 'mslp-tc' :{ 'levels' : [908,912,916,920,924,928,932,936,940,944,948,952,956,960,964,968,972,976,980,984,988,992,996,1000,1004,1008,1012,1016,1020,1024,1028,1032,1036,1040,1044,1048,1052], 'cmap':readNCLcm('nice_gfdl')[3:193], 'fname':['MSLP'], 'filename': 'upp' }, + 'td2' :{ 'levels' : [-20,-15,-10,-5,0,5,10,15,20,25,30,35,40,45,50,55,60,64,68,72,76,80,84], + 'cmap':['#f3e7e8','#d0a0a4','#ad5960', '#8b131d', '#8b4513','#ad7c59', '#c5a289','#dcc7b8','#eeeeee', '#dddddd', '#bbbbbb', '#e1e1d3', '#e1d5b1','#ccb77a','#ffffe5','#f7fcb9', '#addd8e', '#41ab5d', '#006837', '#004529', '#195257', '#4c787c'], + 'fname' : ['DEWPOINT_2M'], 'filename':'upp'}, + 'td2depart' :{ 'levels' : [-20,-15,-10,-5,0,5,10,15,20,25,30,35,40,45,50,55,60,64,68,72,76,80,84], + 'cmap':['#f3e7e8','#d0a0a4','#ad5960', '#8b131d', '#8b4513','#ad7c59', '#c5a289','#dcc7b8','#eeeeee', '#dddddd', '#bbbbbb', '#e1e1d3', '#e1d5b1','#ccb77a','#ffffe5','#f7fcb9', '#addd8e', '#41ab5d', '#006837', '#004529', '#195257', '#4c787c'], + 'fname' : ['DEWPOINT_2M'], 'filename':'upp'}, + 'thetae' :{ 'levels' : [300,305,310,315,320,325,330,335,340,345,350,355,360], 'cmap': ['#eeeeee', '#dddddd', '#cccccc', '#aaaaaa']+readNCLcm('precip2_17lev')[3:-1], 'fname' : ['T2', 'Q2', 'PSFC'], 'filename': 'diag'}, + 'rh2m' :{ 'levels' : [0,10,20,30,40,50,60,70,80,90,100,110], 'cmap': readNCLcm('precip2_17lev')[:17][::-1], 'fname': ['T2', 'PSFC', 'Q2'], 'filename': 'diag'}, + 'heatindex' :{ 'levels' : [65,70,75,80,85,90,95,100,105,110,115,120,125,130], 'cmap': readNCLcm('MPL_hot')[::-1], 'fname': ['AFWA_HEATIDX'], 'filename':'diag' }, +# 'pblh' :{ 'levels' : [0,250,500,750,1000,1250,1500,1750,2000,2250,2500,2750,3000], 'cmap': readNCLcm('precip2_17lev')[3:-1], 'fname': ['C_PBLH'], 'filename':'diag' }, + 'pblh' :{ 'levels' : [0,250,500,750,1000,1250,1500,1750,2000,2500,3000,3500,4000], + 'cmap': ['#eeeeee', '#dddddd', '#cccccc', '#bbbbbb', '#44aaee','#88bbff', '#aaccff', '#bbddff', '#efd6c1', '#e5c1a1', '#eebb32', '#bb9918'], 'fname': ['PBLH'] }, + 'hmuh' :{ 'levels' : [10,25,50,75,100,125,150,175,200,250,300,400,500], 'cmap': readNCLcm('prcp_1')[1:15], 'fname': ['UP_HELI_MAX'], 'filename':'diag'}, + 'hmuh-neg' :{ 'levels' : [10,25,50,75,100,125,150,175,200,250,300,400,500], 'cmap': readNCLcm('prcp_1')[1:15], 'fname': ['UP_HELI_MIN'], 'filename':'diag'}, + 'hmuh03' :{ 'levels' : [10,25,50,75,100,125,150,175,200,250,300,400,500], 'cmap': readNCLcm('prcp_1')[1:15], 'fname': ['UP_HELI_MAX03'], 'filename':'diag'}, + 'rvort1' :{ 'levels' : [0.005,0.006,0.007,0.008,0.009,0.01,0.011,0.012,0.013,0.014,0.015], 'cmap': readNCLcm('prcp_1')[1:15], 'fname': ['RVORT1_MAX'], 'filename':'diag'}, + 'hmup' :{ 'levels' : [4,6,8,10,12,14,16,18,20,24,28,32,36,40,44,48], 'cmap': readNCLcm('prcp_1')[1:16], 'fname': ['W_UP_MAX'], 'filename':'diag' }, + #'hmdn' :{ 'levels' : [-19,-17,-15,-13,-11,-9,-7,-5,-3,-1,0], 'cmap': readNCLcm('prcp_1')[16:1:-1]+['#ffffff'], 'fname': ['W_DN_MAX'], 'filename':'diag' }, + 'hmdn' :{ 'levels' : [2,3,4,6,8,10,12,14,16,18,20,22,24,26,28,30], 'cmap': readNCLcm('prcp_1')[1:16], 'fname': ['W_DN_MAX'], 'filename':'diag' }, + 'hmwind' :{ 'levels' : [10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42], 'cmap': readNCLcm('prcp_1')[:16], 'fname': ['WSPD10MAX'], 'filename':'diag' }, + #'hmwind' :{ 'levels' : [20,25,30,35,40,45,50,55,60,65,70,75,80,85], 'cmap': readNCLcm('prcp_1')[1:16], 'fname': ['WSPD10MAX'], 'filename':'diag' }, + 'hmgrp' :{ 'levels' : [0.01,0.1,0.25,0.5,0.75,1.0,1.5,2.0,2.5,3.0,4.0,5.0], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['GRPL_MAX'], 'filename':'diag' }, +# 'cref' :{ 'levels' : [5,10,15,20,25,30,35,40,45,50,55,60,65,70], 'cmap': readcm('cmap_rad.rgb')[2:16], 'fname': ['REFL_10CM'], 'arraylevel':'max' }, + 'cref' :{ 'levels' : [5,10,15,20,25,30,35,40,45,50,55,60,65,70], 'cmap': readcm('cmap_rad.rgb')[1:14], 'fname': ['REFL_MAX_COL'], 'filename':'upp' }, + 'lmlref' :{ 'levels' : [5,10,15,20,25,30,35,40,45,50,55,60,65,70], 'cmap': readcm('cmap_rad.rgb')[1:14], 'fname': ['REFL_10CM'], 'arraylevel':0 }, + 'ref1km' :{ 'levels' : [5,10,15,20,25,30,35,40,45,50,55,60,65,70], 'cmap': readcm('cmap_rad.rgb')[1:14], 'fname': ['REFL_1KM_AGL'], 'filename':'upp' }, + 'echotop' :{ 'levels' : [1000,5000,10000,15000,20000,25000,30000,35000,40000,45000,50000,55000,60000,65000], 'cmap': readNCLcm('precip3_16lev')[1::], 'fname': ['ECHOTOP'], 'filename':'diag' }, + 'srh3' :{ 'levels' : [50,100,150,200,250,300,400,500], 'cmap': readNCLcm('perc2_9lev'), 'fname': ['SR_HELICITY_3KM'], 'filename' : 'upp' }, + 'srh1' :{ 'levels' : [50,100,150,200,250,300,400,500], 'cmap': readNCLcm('perc2_9lev'), 'fname': ['SR_HELICITY_1KM'], 'filename' : 'upp' }, + 'shr06mag' :{ 'levels' : [30,35,40,45,50,55,60,65,70,75,80], 'cmap': readNCLcm('perc2_9lev'), 'fname': ['UBSHR6', 'VBSHR6'], 'filename':'upp' }, + 'shr01mag' :{ 'levels' : [10,15,20,25,30,35,40,45,50,55], 'cmap': readNCLcm('perc2_9lev'), 'fname': ['UBSHR1', 'VBSHR1'], 'filename':'upp' }, + 'zlfc' :{ 'levels' : [0,250,500,750,1000,1250,1500,2000,2500,3000,3500,4000,5000], 'cmap': [readNCLcm('nice_gfdl')[i] for i in [3,20,37,54,72,89,106,123,141,158,175,193]], 'fname': ['AFWA_ZLFC'], 'filename':'diag' }, + 'zlcl' :{ 'levels' : [0,250,500,750,1000,1250,1500,2000,2500,3000,3500,4000,5000], 'cmap': [readNCLcm('nice_gfdl')[i] for i in [3,20,37,54,72,89,106,123,141,158,175,193]], 'fname': ['LCL_HEIGHT'], 'filename':'upp' }, + 'ltg1' :{ 'levels' : [0.1,0.5,1,1.5,2,2.5,3,4,5,6,7,8,10,12], 'cmap': readNCLcm('prcp_1')[:15], 'fname': ['LTG1_MAX'], 'filename':'diag' }, + 'ltg2' :{ 'levels' : [0.1,0.5,1,1.5,2,2.5,3,4,5,6,7,8,10,12], 'cmap': readNCLcm('prcp_1')[:15], 'fname': ['LTG2_MAX'], 'filename':'diag' }, + 'ltg3' :{ 'levels' : [0.1,0.5,1,1.5,2,2.5,3,4,5,6,7,8,10,12], 'cmap': readNCLcm('prcp_1')[:15], 'fname': ['LTG3_MAX'], 'filename':'diag' }, + 'liftidx' :{ 'levels' : [-14,-12,-10,-8,-6,-4,-2,0,2,4,6,8], 'cmap': readNCLcm('nice_gfdl')[193:3:-1]+['#ffffff'], 'fname': ['SFC_LI'], 'filename':'upp'}, + 'bmin' :{ 'levels' : [-20,-16,-12,-10,-8,-6,-4,-2,-1,-0.5,0,0.5], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['MLBMIN'], 'filename':'upp' }, + 'pbmin' :{ 'levels' : [0,30,60,90,120,150,180],'cmap': ['#dddddd', '#aaaaaa']+readNCLcm('precip2_17lev')[3:-1], 'fname': ['MLPBMIN','PBMIN_SFC'], 'filename':'upp' }, + 'goesch3' :{ 'levels' : [-80,-78,-76,-74,-72,-70,-68,-66,-64,-62,-60,-58,-56,-54,-52,-50,-48,-46,-44,-42,-40,-38,-36,-34,-32,-30,-28,-26,-24,-22,-20,-18,-16,-14,-12,-10], 'cmap': readcm('cmap_sat2.rgb')[38:1:-1], 'fname': ['GOESE_WV'], 'filename':'upp' }, + 'goesch4' :{ 'levels' : [-80,-76,-72,-68,-64,-60,-56,-52,-48,-44,-40,-36,-32,-28,-24,-20,-16,-12,-8,-4,0,4,8,12,16,20,24,28,32,36,40], 'cmap': readcm('cmap_satir.rgb')[32:1:-1], 'fname': ['GOESE_IR'], 'filename':'upp' }, + #'afwavis' :{ 'levels' : [0.0,0.1,0.25,0.5,1.0,2.0,3.0,4.0,5.0,6.0,8.0,10.0,12.0], 'cmap': readNCLcm('nice_gfdl')[3:175]+['#ffffff'], 'fname': ['AFWA_VIS'], 'filename':'diag' }, + 'afwavis' :{ 'levels' : [0.0,0.1,0.25,0.5,1.0,2.0,3.0,4.0,5.0,6.0,8.0,10.0,12.0], 'cmap': readNCLcm('nice_gfdl')[3:175]+['#ffffff'], 'fname': ['VISIBILITY'], 'filename':'upp' }, + + # winter fields + 'snow' :{ 'levels' : [0.01,0.1,0.25,0.5,0.75,1,1.5,2,2.5,3,4,5], 'cmap':['#dddddd','#aaaaaa']+[readNCLcm('precip3_16lev')[i] for i in [1,3,5,8,10,12,14,16]]+['#FF99FF'], 'fname':['AFWA_SNOWFALL_HRLY'] }, # CSS mod + 'snow-6hr' :{ 'levels' : [0.25,0.5,0.75,1,2,3,4,5,6,8,10,12], 'cmap':['#dddddd','#aaaaaa']+[readNCLcm('precip3_16lev')[i] for i in [1,3,5,8,10,12,14,16]]+['#FF99FF'], 'fname':['AFWA_SNOWFALL_HRLY'] }, # CSS mod + 'snow-12hr' :{ 'levels' : [0.5,1,2,3,6,8,10,12,14,16,18,20], 'cmap':['#dddddd','#aaaaaa']+[readNCLcm('precip3_16lev')[i] for i in [1,3,5,8,10,12,14,16]]+['#FF99FF'], 'fname':['AFWA_SNOWFALL_HRLY'] }, # CSS mod + 'snow-24hr' :{ 'levels' : [1,3,6,8,10,12,15,18,21,24,30,36], 'cmap':['#dddddd','#aaaaaa']+[readNCLcm('precip3_16lev')[i] for i in [1,3,5,8,10,12,14,16]]+['#FF99FF'], 'fname':['AFWA_SNOWFALL_HRLY'] }, # CSS mod + 'snowacc' :{ 'levels' : [0.01,0.1,0.5,1,2,3,4,5,6,8,10,12,18,24,36,48,60], 'cmap':['#dddddd','#aaaaaa']+[readNCLcm('precip3_16lev')[i] for i in [1,2,3,4,5,6,8,10,11,12,13,15,16]]+['#FF99FF'], 'fname':['AFWA_SNOWFALL'], 'filename':'diag'}, # CSS mod colortable +# 'snowliq' :{ 'levels' : [0.1,0.5,1,1.5,2,2.5,3,4,5,6], 'cmap':readNCLcm('precip3_16lev')[1:], 'fname':['AFWA_SNOW'], 'filename':'diag'}, + 'iceacc' :{ 'levels' : [0.01,0.05,0.1,0.15,0.2,0.25,0.3,0.4,0.5,0.75,1,1.25], 'cmap':[readNCLcm('precip3_16lev')[i] for i in [1,2,3,4,5,6,8,10,11,12,13,15,16]], 'fname':['AFWA_ICE'], 'filename':'diag'}, + 'fzra' :{ 'levels' : [0.01,0.05,0.1,0.15,0.2,0.25,0.3,0.4,0.5,0.75,1,1.25], 'cmap':[readNCLcm('precip3_16lev')[i] for i in [1,2,3,4,5,6,8,10,11,12,13,15,16]], 'fname':['AFWA_FZRA_HRLY'] }, # CSS added, hrly + 'fzraacc' :{ 'levels' : [0.01,0.05,0.1,0.15,0.2,0.25,0.3,0.4,0.5,0.75,1,1.25], 'cmap':[readNCLcm('precip3_16lev')[i] for i in [1,2,3,4,5,6,8,10,11,12,13,15,16]], 'fname':['AFWA_FZRA'], 'filename':'diag'}, + 'windchill' :{ 'levels' : [-40,-35,-30,-25,-20,-15,-10,-5,0,5,10,15,20,25,30,35,40,45], 'cmap':readNCLcm('GMT_ocean')[20:], 'fname':['AFWA_WCHILL'], 'filename':'diag'}, + 'freezelev' :{ 'levels' : [0, 500, 1000, 1500, 2000, 2500, 3000, 3500, 4000, 4500, 5000], 'cmap':readNCLcm('nice_gfdl')[3:193], 'fname':['FZLEV'], 'filename':'diag'}, + 'thck1000-500' :{ 'levels' : [480,486,492,498,504,510,516,522,528,534,540,546,552,558,564,570,576,582,588,592,600], 'cmap':readNCLcm('perc2_9lev'), 'fname':['GHT_PL', 'GHT_PL'], 'arraylevel':[0,5], 'filename':'diag'}, # CSS mod + 'thck1000-850' :{ 'levels' : [82,85,88,91,94,97,100,103,106,109,112,115,118,121,124,127,130,133,136,139,142,145,148,151,154,157,160], 'cmap':readNCLcm('perc2_9lev'), 'fname':['GHT_PL', 'GHT_PL'], 'arraylevel':[0,2], 'filename':'diag'}, # CSS mod + + # pressure level entries + 'hgt250' :{ 'levels' : [9700,9760,9820,9880,9940,10000,10060,10120,10180,10240,10300,10360,10420,10480,10540,10600,10660,10720,10780,10840,10900,10960,11020], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['GHT_PL'], 'filename':'diag', 'arraylevel':8 }, + 'hgt300' :{ 'levels' : [8400,8460,8520,8580,8640,8700,8760,8820,8880,8940,9000,9060,9120,9180,9240,9300,9360,9420,9480,9540,9600,9660,9720,9780,9840,9900,9960,10020], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['GHT_PL'], 'filename':'diag', 'arraylevel':7 }, + 'hgt500' :{ 'levels' : [4800,4860,4920,4980,5040,5100,5160,5220,5280,5340,5400,5460,5520,5580,5640,5700,5760,5820,5880,5940,6000], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['GHT_PL'], 'filename':'diag', 'arraylevel':5 }, + 'hgt700' :{ 'levels' : [2700,2730,2760,2790,2820,2850,2880,2910,2940,2970,3000,3030,3060,3090,3120,3150,3180,3210,3240,3270,3300], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['GHT_PL'], 'filename':'diag', 'arraylevel':3 }, + 'hgt850' :{ 'levels' : [1200,1230,1260,1290,1320,1350,1380,1410,1440,1470,1500,1530,1560,1590,1620,1650], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['GHT_PL'], 'filename':'diag', 'arraylevel':2 }, + 'hgt925' :{ 'levels' : [550,580,610,640,670,700,730,760,790,820,850,880,910,940,970,1000,1030], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['GHT_PL'], 'filename':'diag', 'arraylevel':1 }, + 'speed250' :{ 'levels' : [10,20,30,40,50,60,70,80,90,100,110,120,130,140,150,160,170], 'cmap': readNCLcm('wind_17lev'), 'fname': ['S_PL'], 'filename':'diag', 'arraylevel':8 }, + 'speed300' :{ 'levels' : [10,20,30,40,50,60,70,80,90,100,110,120,130,140,150,160,170], 'cmap': readNCLcm('wind_17lev'), 'fname': ['S_PL'], 'filename':'diag', 'arraylevel':7 }, + # RAS: adjusted these ranges - need to capture higher wind speeds + #'speed500' :{ 'levels' : [5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85], 'cmap': readNCLcm('wind_17lev'), 'fname': ['S_PL'], 'filename':'diag', 'arraylevel':5 }, + 'speed500' :{ 'levels' : [15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100], 'cmap': readNCLcm('wind_17lev'), 'fname': ['S_PL'], 'filename':'diag', 'arraylevel':5 }, + #'speed700' :{ 'levels' : [4,8,12,16,20,24,28,32,36,40,44,48,52,56,60,64,68], 'cmap': readNCLcm('wind_17lev'), 'fname': ['S_PL'], 'filename':'diag', 'arraylevel':3 }, + 'speed700' :{ 'levels' : [5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85], 'cmap': readNCLcm('wind_17lev'), 'fname': ['S_PL'], 'filename':'diag', 'arraylevel':3 }, + #'speed850' :{ 'levels' : [3,6,9,12,15,18,21,24,27,30,33,36,39,42,45,48,51], 'cmap': readNCLcm('wind_17lev'), 'fname': ['S_PL'], 'filename':'diag', 'arraylevel':2 }, + 'speed850' :{ 'levels' : [6,10,14,18,22,26,30,34,38,42,46,50,54,58,62,66,70,74], 'cmap': readNCLcm('wind_17lev'), 'fname': ['S_PL'], 'filename':'diag', 'arraylevel':2 }, + #'speed925' :{ 'levels' : [3,6,9,12,15,18,21,24,27,30,33,36,39,42,45,48,51], 'cmap': readNCLcm('wind_17lev'), 'fname': ['S_PL'], 'filename':'diag', 'arraylevel':1 }, + 'speed925' :{ 'levels' : [6,10,14,18,22,26,30,34,38,42,46,50,54,58,62,66,70,74], 'cmap': readNCLcm('wind_17lev'), 'fname': ['S_PL'], 'filename':'diag', 'arraylevel':1 }, + 'temp250' :{ 'levels' : [-65,-63,-61,-59,-57,-55,-53,-51,-49,-47,-45,-43,-41,-39,-37,-35,-33,-31,-29], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['T_PL'], 'filename':'diag', 'arraylevel':8 }, + 'temp300' :{ 'levels' : [-65,-63,-61,-59,-57,-55,-53,-51,-49,-47,-45,-43,-41,-39,-37,-35,-33,-31,-29], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['T_PL'], 'filename':'diag', 'arraylevel':7 }, + 'temp500' :{ 'levels' : [-41,-39,-37,-35,-33,-31,-29,-26,-23,-20,-17,-14,-11,-8,-5,-2], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['T_PL'], 'filename':'diag', 'arraylevel':5 }, + 'temp700' :{ 'levels' : [-36,-33,-30,-27,-24,-21,-18,-15,-12,-9,-6,-3,0,3,6,9,12,15,18,21], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['T_PL'], 'filename':'diag', 'arraylevel':3 }, + 'temp850' :{ 'levels' : [-30,-27,-24,-21,-18,-15,-12,-9,-6,-3,0,3,6,9,12,15,18,21,24,27,30], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['T_PL'], 'filename':'diag', 'arraylevel':2 }, + 'temp925' :{ 'levels' : [-24,-21,-18,-15,-12,-9,-6,-3,0,3,6,9,12,15,18,21,24,27,30,33], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['T_PL'], 'filename':'diag', 'arraylevel':1 }, + #'td300' :{ 'levels' : [-65,-60,-55,-50,-45,-40,-35,-30], 'cmap' : readNCLcm('nice_gfdl'), 'fname': ['TD_PL'], 'filename':'diag', 'arraylevel':7 }, + #'td500' :{ 'levels' : [-50,-45,-40,-35,-30,-25,-20,-15,-10], 'cmap' : readNCLcm('nice_gfdl'), 'fname': ['TD_PL'], 'filename':'diag', 'arraylevel':5 }, + 'td700' :{ 'levels' : [-30,-25,-20,-15,-10,-5,0,5,10], 'cmap' : readNCLcm('nice_gfdl')[3:193], 'fname': ['TD_PL'], 'filename':'diag', 'arraylevel':3 }, + 'td850' :{ 'levels' : [-40,-35,-30,-25,-20,-15,-10,-5,0,5,10,15,20,25,30], 'cmap' : readNCLcm('nice_gfdl')[3:193], 'fname': ['TD_PL'], 'filename':'diag', 'arraylevel':2 }, + 'td925' :{ 'levels' : [-30,-25,-20,-15,-10,-5,0,5,10,15,20,25,30], 'cmap' : readNCLcm('nice_gfdl')[3:193], 'fname': ['TD_PL'], 'filename':'diag', 'arraylevel':1 }, + 'rh300' :{ 'levels' : [0,10,20,30,40,50,60,70,80,90,100], 'cmap' : [readNCLcm('MPL_PuOr')[i] for i in (2,18,34,50)]+[readNCLcm('MPL_Greens')[j] for j in (2,17,50,75,106,125)], 'fname': ['RH_PL'], 'filename':'diag', 'arraylevel':7 }, + 'rh500' :{ 'levels' : [0,10,20,30,40,50,60,70,80,90,100], 'cmap' : [readNCLcm('MPL_PuOr')[i] for i in (2,18,34,50)]+[readNCLcm('MPL_Greens')[j] for j in (2,17,50,75,106,125)], 'fname': ['RH_PL'], 'filename':'diag', 'arraylevel':5 }, +# 'rh700' :{ 'levels' : [0,10,20,30,40,50,60,70,80,90,100], 'cmap' : [readNCLcm('MPL_PuOr')[i] for i in (2,18,34,50)]+[readNCLcm('MPL_Greens')[j] for j in (2,17,50,75,106,125)], 'fname': ['RH_PL'], 'filename':'diag', 'arraylevel':3 }, + 'rh700' :{ 'levels' : [0,10,20,30,40,50,60,70,80,90,100], 'cmap' : readNCLcm('CBR_drywet'), 'fname': ['RH_PL'], 'filename':'diag', 'arraylevel':3 }, + 'rh850' :{ 'levels' : [0,10,20,30,40,50,60,70,80,90,100], 'cmap' : readNCLcm('CBR_drywet'), 'fname': ['RH_PL'], 'filename':'diag', 'arraylevel':2 }, +# 'rh850' :{ 'levels' : [0,10,20,30,40,50,60,70,80,90,100], 'cmap' : [readNCLcm('MPL_PuOr')[i] for i in (2,18,34,50)]+[readNCLcm('MPL_Greens')[j] for j in (2,17,50,75,106,125)], 'fname': ['RH_PL'], 'filename':'diag', 'arraylevel':2 }, + 'rh925' :{ 'levels' : [0,10,20,30,40,50,60,70,80,90,100], 'cmap' : readNCLcm('CBR_drywet'), 'fname': ['RH_PL'], 'filename':'diag', 'arraylevel':1 }, +# 'rh925' :{ 'levels' : [0,10,20,30,40,50,60,70,80,90,100], 'cmap' : [readNCLcm('MPL_PuOr')[i] for i in (2,18,34,50)]+[readNCLcm('MPL_Greens')[j] for j in (2,17,50,75,106,125)], 'fname': ['RH_PL'], 'filename':'diag', 'arraylevel':1 }, + 'avo500' :{ 'levels' : [0,9,12,15,18,21,24,27,30,33], 'cmap': readNCLcm('prcp_1'), 'fname': ['AVORT_PL'], 'filename':'diag', 'arraylevel':5 }, +'pvort320k' :{ 'levels' : [0,0.1,0.2,0.3,0.4,0.5,0.75,1,1.5,2,3,4,5,7,10], + 'cmap' : ['#ffffff','#eeeeee','#dddddd','#cccccc','#bbbbbb','#d1c5b1','#e1d5b9','#f1ead3','#003399','#0033FF','#0099FF','#00CCFF','#8866FF','#9933FF','#660099'], + 'fname': ['PVORT_320K'], 'filename':'upp' }, + 'bunkmag' :{ 'levels' : [20,25,30,35,40,45,50,55,60], 'cmap':readNCLcm('wind_17lev')[1:], 'fname':['U_COMP_STM_6KM', 'V_COMP_STM_6KM'], 'filename':'upp' }, + 'speed10m' :{ 'levels' : [3,6,9,12,15,18,21,24,27,30,33,36,39,42,45,48,51], 'cmap': readNCLcm('wind_17lev')[1:],'fname' : ['U10', 'V10'], 'filename':'diag'}, + 'speed10m-tc' :{ 'levels' : [6,12,18,24,30,36,42,48,54,60,66,72,78,84,90,96,102], 'cmap': readNCLcm('wind_17lev')[1:],'fname' : ['U10', 'V10'], 'filename':'diag'}, + 'stp' :{ 'levels' : [0.5,0.75,1.0,1.5,2.0,3.0,4.0,5.0,6.0,7.0,8.0], 'cmap':readNCLcm('perc2_9lev'), 'fname':['SBCAPE','LCL_HEIGHT','SR_HELICITY_1KM','UBSHR6','VBSHR6'], 'arraylevel':[None,None,None,None,None], 'filename':'upp'}, + #'sigsvr :{ 'levels' : [1e5,2e5,3e5,4e5,5e5,6e5,8e5,10e5], 'cmap':readNCLcm('prcp_1'), 'fname':['MLCAPE','UHSHR], 'filename':'upp'} + 'ptype' :{ 'levels' : [0.01,0.1,0.25,0.5,0.75,1,1.5,2,2.5,3,3.5,4], 'cmap':['#dddddd','#aaaaaa']+readNCLcm('precip3_16lev')[1:], 'fname':['AFWA_RAIN_HRLY', 'AFWA_FZRA_HRLY', 'AFWA_ICE_HRLY', 'AFWA_SNOWFALL_HRLY'], 'filename':'wrfout'}, + 'winter' :{ 'levels' : [0.01,0.1,0.25,0.5,0.75,1,1.5,2,2.5,3,3.5,4], 'cmap':['#dddddd','#aaaaaa']+readNCLcm('precip3_16lev')[1:], 'fname':['AFWA_RAIN_HRLY', 'AFWA_FZRA_HRLY', 'AFWA_ICE_HRLY', 'AFWA_SNOWFALL_HRLY'], 'filename':'wrfout'}, + 'crefuh' :{ 'levels' : [5,10,15,20,25,30,35,40,45,50,55,60,65,70], 'cmap': readcm('cmap_rad.rgb')[0:13], 'fname': ['REFL_MAX_COL', 'MAX_UPDRAFT_HELICITY'], 'filename':'upp' }, + + # wind barb entries + 'wind10m' :{ 'fname' : ['U10', 'V10'], 'filename':'diag', 'skip':40 }, + 'wind250' :{ 'fname' : ['U_PL', 'V_PL'], 'filename':'diag', 'arraylevel':8, 'skip':40 }, + 'wind300' :{ 'fname' : ['U_PL', 'V_PL'], 'filename':'diag', 'arraylevel':7, 'skip':40 }, + 'wind500' :{ 'fname' : ['U_PL', 'V_PL'], 'filename':'diag', 'arraylevel':5, 'skip':40 }, + 'wind700' :{ 'fname' : ['U_PL', 'V_PL'], 'filename':'diag', 'arraylevel':3, 'skip':40 }, + 'wind850' :{ 'fname' : ['U_PL', 'V_PL'], 'filename':'diag', 'arraylevel':2, 'skip':40 }, + 'wind925' :{ 'fname' : ['U_PL', 'V_PL'], 'filename':'diag', 'arraylevel':1, 'skip':40 }, + 'shr06' :{ 'fname' : ['UBSHR6','VBSHR6'], 'filename': 'upp', 'skip':40 }, + 'shr01' :{ 'fname' : ['UBSHR1', 'VBSHR1'], 'filename': 'upp', 'skip':40 }, + 'bunkers' :{ 'fname' : ['U_COMP_STM_6KM', 'V_COMP_STM_6KM'], 'filename': 'upp', 'skip':40 }, +} + +# domains = { 'domainname': { 'corners':[ll_lat,ll_lon,ur_lat,ur_lon], 'figsize':[w,h] } } +domains = { 'CONUS' :{ 'corners': [23.1593,-120.811,46.8857,-65.0212], 'fig_width': 1080 }, + 'SGP' :{ 'corners': [25.3,-107.00,36.00,-88.70], 'fig_width':1080 }, + 'NGP' :{ 'corners': [40.00,-105.0,50.30,-82.00], 'fig_width':1080 }, + 'CGP' :{ 'corners': [33.00,-107.50,45.00,-86.60], 'fig_width':1080 }, + 'SW' :{ 'corners': [28.00,-121.50,44.39,-102.10], 'fig_width':1080 }, + 'NW' :{ 'corners': [37.00,-124.40,51.60,-102.10], 'fig_width':1080 }, + 'SE' :{ 'corners': [26.10,-92.75,36.00,-71.00], 'fig_width':1080 }, + 'NE' :{ 'corners': [38.00,-91.00,46.80,-65.30], 'fig_width':1080 }, + 'MATL':{ 'corners': [33.50,-92.25,41.50,-68.50], 'fig_width':1080 }, +} From f5de45166110a7344b60e0d87e520b5db35151fa Mon Sep 17 00:00:00 2001 From: David Ahijevych Date: Wed, 24 Jan 2018 11:31:29 -0700 Subject: [PATCH 03/68] Added ptype-prob. Translucent filled contours of rain, snow, ice, and freezing rain --- fieldinfo.py | 5 +++++ webplot.py | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/fieldinfo.py b/fieldinfo.py index 1ace351..936e7f3 100755 --- a/fieldinfo.py +++ b/fieldinfo.py @@ -95,6 +95,7 @@ def readNCLcm(name): 'snow-24hr' :{ 'levels' : [1,3,6,8,10,12,15,18,21,24,30,36], 'cmap':['#dddddd','#aaaaaa']+[readNCLcm('precip3_16lev')[i] for i in [1,3,5,8,10,12,14,16]]+['#FF99FF'], 'fname':['AFWA_SNOWFALL_HRLY'] }, # CSS mod 'snowacc' :{ 'levels' : [0.01,0.1,0.5,1,2,3,4,5,6,8,10,12,18,24,36,48,60], 'cmap':['#dddddd','#aaaaaa']+[readNCLcm('precip3_16lev')[i] for i in [1,2,3,4,5,6,8,10,11,12,13,15,16]]+['#FF99FF'], 'fname':['AFWA_SNOWFALL'], 'filename':'diag'}, # CSS mod colortable # 'snowliq' :{ 'levels' : [0.1,0.5,1,1.5,2,2.5,3,4,5,6], 'cmap':readNCLcm('precip3_16lev')[1:], 'fname':['AFWA_SNOW'], 'filename':'diag'}, + 'ice' :{ 'levels' : [0.01,0.05,0.1,0.15,0.2,0.25,0.3,0.4,0.5,0.75,1,1.25], 'cmap':[readNCLcm('precip3_16lev')[i] for i in [1,2,3,4,5,6,8,10,11,12,13,15,16]], 'fname':['AFWA_ICE_HRLY'], 'filename':'wrfout'}, 'iceacc' :{ 'levels' : [0.01,0.05,0.1,0.15,0.2,0.25,0.3,0.4,0.5,0.75,1,1.25], 'cmap':[readNCLcm('precip3_16lev')[i] for i in [1,2,3,4,5,6,8,10,11,12,13,15,16]], 'fname':['AFWA_ICE'], 'filename':'diag'}, 'fzra' :{ 'levels' : [0.01,0.05,0.1,0.15,0.2,0.25,0.3,0.4,0.5,0.75,1,1.25], 'cmap':[readNCLcm('precip3_16lev')[i] for i in [1,2,3,4,5,6,8,10,11,12,13,15,16]], 'fname':['AFWA_FZRA_HRLY'] }, # CSS added, hrly 'fzraacc' :{ 'levels' : [0.01,0.05,0.1,0.15,0.2,0.25,0.3,0.4,0.5,0.75,1,1.25], 'cmap':[readNCLcm('precip3_16lev')[i] for i in [1,2,3,4,5,6,8,10,11,12,13,15,16]], 'fname':['AFWA_FZRA'], 'filename':'diag'}, @@ -150,6 +151,7 @@ def readNCLcm(name): 'stp' :{ 'levels' : [0.5,0.75,1.0,1.5,2.0,3.0,4.0,5.0,6.0,7.0,8.0], 'cmap':readNCLcm('perc2_9lev'), 'fname':['SBCAPE','LCL_HEIGHT','SR_HELICITY_1KM','UBSHR6','VBSHR6'], 'arraylevel':[None,None,None,None,None], 'filename':'upp'}, #'sigsvr :{ 'levels' : [1e5,2e5,3e5,4e5,5e5,6e5,8e5,10e5], 'cmap':readNCLcm('prcp_1'), 'fname':['MLCAPE','UHSHR], 'filename':'upp'} 'ptype' :{ 'levels' : [0.01,0.1,0.25,0.5,0.75,1,1.5,2,2.5,3,3.5,4], 'cmap':['#dddddd','#aaaaaa']+readNCLcm('precip3_16lev')[1:], 'fname':['AFWA_RAIN_HRLY', 'AFWA_FZRA_HRLY', 'AFWA_ICE_HRLY', 'AFWA_SNOWFALL_HRLY'], 'filename':'wrfout'}, + 'ptype-prob' :{ 'cmap':['green','blue','orange','red'], 'fname':['AFWA_RAIN_HRLY', 'AFWA_SNOWFALL_HRLY', 'AFWA_ICE_HRLY','AFWA_FZRA_HRLY'], 'filename':'wrfout'}, 'winter' :{ 'levels' : [0.01,0.1,0.25,0.5,0.75,1,1.5,2,2.5,3,3.5,4], 'cmap':['#dddddd','#aaaaaa']+readNCLcm('precip3_16lev')[1:], 'fname':['AFWA_RAIN_HRLY', 'AFWA_FZRA_HRLY', 'AFWA_ICE_HRLY', 'AFWA_SNOWFALL_HRLY'], 'filename':'wrfout'}, 'crefuh' :{ 'levels' : [5,10,15,20,25,30,35,40,45,50,55,60,65,70], 'cmap': readcm('cmap_rad.rgb')[0:13], 'fname': ['REFL_MAX_COL', 'MAX_UPDRAFT_HELICITY'], 'filename':'upp' }, @@ -166,6 +168,9 @@ def readNCLcm(name): 'bunkers' :{ 'fname' : ['U_COMP_STM_6KM', 'V_COMP_STM_6KM'], 'filename': 'upp', 'skip':40 }, } +# Combine levels from RAIN, FZRA, ICE, and SNOW for plotting winter precip type. Ahijevych added this +fieldinfo['ptype-prob']['levels'] = [fieldinfo['precip']['levels'][1:],fieldinfo['snow']['levels'],fieldinfo['ice']['levels'],fieldinfo['fzra']['levels']] + # domains = { 'domainname': { 'corners':[ll_lat,ll_lon,ur_lat,ur_lon], 'figsize':[w,h] } } domains = { 'CONUS' :{ 'corners': [23.1593,-120.811,46.8857,-65.0212], 'fig_width': 1080 }, 'SGP' :{ 'corners': [25.3,-107.00,36.00,-88.70], 'fig_width':1080 }, diff --git a/webplot.py b/webplot.py index 8ce6c4d..449dcc6 100755 --- a/webplot.py +++ b/webplot.py @@ -214,6 +214,7 @@ def plotFields(self): def plotFill(self): if self.opts['fill']['name'] == 'ptype': self.plotFill_ptype(); return + if self.opts['fill']['name'] == 'ptype-prob': self.plotFill_ptype_prob(); return elif self.opts['fill']['name'] == 'crefuh': self.plotReflectivityUH(); return if self.autolevels: @@ -268,6 +269,44 @@ def plotFill_ptype(self): cb.set_ticklabels(['Rain', 'Freezing Rain', 'Sleet', 'Snow']) cb.ax.tick_params(length=0) + def plotFill_ptype_prob(self): + types = self.data['fill'] + ntypes = len(types) + + # Plot where hourly precip of each type is greater than zero. + # Colors match the regular ptype plot, but are translucent, so you + # can see them underneath each other. + # Data arrays, colors, and levels defined in fieldinfo.py + alpha = 0.25 + colors = self.opts['fill']['colors'] + threshes = self.opts['fill']['levels'] + type_labels= self.opts['fill']['arrayname'] # Get label directly from fieldinfo arrayname. + type_labels = ['Rain', 'Snow', 'Sleet', 'Freezing Rain'] # Hard-coded + if any(len(lst) != ntypes for lst in [colors, threshes, type_labels]): + print "data, colors, threshes, and type_labels must all be same length" + sys.exit(1) + # make axes for colorbar, 175px to left and 35px down from bottom of map + x0, y0 = self.ax.transAxes.transform((0,0)) + x, y = self.fig.transFigure.inverted().transform((x0+175,y0-35)) + # Width of space where colorbar will go. + cbwidth = (0.985-x)/ntypes + for i in range(ntypes): + levels = self.opts['fill']['levels'][i] + # Mask pixels less than threshold + type = np.ma.masked_less(types[i],levels[0]) + cs = self.m.contourf(self.x, self.y, type, levels=[0,np.max(type)], colors=colors[i], ax=self.ax) + cax = self.fig.add_axes([x+(i*cbwidth),y,0.85*cbwidth,y/3.0]) + cs.set_alpha(alpha) + cb = plt.colorbar(cs, cax=cax, orientation='horizontal', ticks=[]) + cb.outline.set_visible(False) + cb.ax.set_title(type_labels[i]) + + data = ndimage.gaussian_filter(types[i], sigma=10) + cs2 = self.m.contour(self.x, self.y, data, colors=colors[i], levels=levels, linewidths=1.5, ax=self.ax) + cb = plt.colorbar(cs2, cax=cax, orientation='horizontal') + cb.ax.tick_params(labelsize=9,length=0) + cb.outline.set_edgecolor(colors[i]) + def plotReflectivityUH(self): levels = self.opts['fill']['levels'] cmap = colors.ListedColormap(self.opts['fill']['colors']) From b57cef9db71ccf108ac5bc3e6e54d7106807c076 Mon Sep 17 00:00:00 2001 From: David Ahijevych Date: Wed, 24 Jan 2018 11:53:01 -0700 Subject: [PATCH 04/68] Renamed 'ptype-prob' -> 'ptypes'. There already is a ptype field, which can be called with 'ptype_prob' in web_plot.py. --- fieldinfo.py | 4 ++-- webplot.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/fieldinfo.py b/fieldinfo.py index 936e7f3..51b5ccb 100755 --- a/fieldinfo.py +++ b/fieldinfo.py @@ -151,7 +151,7 @@ def readNCLcm(name): 'stp' :{ 'levels' : [0.5,0.75,1.0,1.5,2.0,3.0,4.0,5.0,6.0,7.0,8.0], 'cmap':readNCLcm('perc2_9lev'), 'fname':['SBCAPE','LCL_HEIGHT','SR_HELICITY_1KM','UBSHR6','VBSHR6'], 'arraylevel':[None,None,None,None,None], 'filename':'upp'}, #'sigsvr :{ 'levels' : [1e5,2e5,3e5,4e5,5e5,6e5,8e5,10e5], 'cmap':readNCLcm('prcp_1'), 'fname':['MLCAPE','UHSHR], 'filename':'upp'} 'ptype' :{ 'levels' : [0.01,0.1,0.25,0.5,0.75,1,1.5,2,2.5,3,3.5,4], 'cmap':['#dddddd','#aaaaaa']+readNCLcm('precip3_16lev')[1:], 'fname':['AFWA_RAIN_HRLY', 'AFWA_FZRA_HRLY', 'AFWA_ICE_HRLY', 'AFWA_SNOWFALL_HRLY'], 'filename':'wrfout'}, - 'ptype-prob' :{ 'cmap':['green','blue','orange','red'], 'fname':['AFWA_RAIN_HRLY', 'AFWA_SNOWFALL_HRLY', 'AFWA_ICE_HRLY','AFWA_FZRA_HRLY'], 'filename':'wrfout'}, + 'ptypes' :{ 'cmap':['green','blue','orange','red'], 'fname':['AFWA_RAIN_HRLY', 'AFWA_SNOWFALL_HRLY', 'AFWA_ICE_HRLY','AFWA_FZRA_HRLY'], 'filename':'wrfout'}, 'winter' :{ 'levels' : [0.01,0.1,0.25,0.5,0.75,1,1.5,2,2.5,3,3.5,4], 'cmap':['#dddddd','#aaaaaa']+readNCLcm('precip3_16lev')[1:], 'fname':['AFWA_RAIN_HRLY', 'AFWA_FZRA_HRLY', 'AFWA_ICE_HRLY', 'AFWA_SNOWFALL_HRLY'], 'filename':'wrfout'}, 'crefuh' :{ 'levels' : [5,10,15,20,25,30,35,40,45,50,55,60,65,70], 'cmap': readcm('cmap_rad.rgb')[0:13], 'fname': ['REFL_MAX_COL', 'MAX_UPDRAFT_HELICITY'], 'filename':'upp' }, @@ -169,7 +169,7 @@ def readNCLcm(name): } # Combine levels from RAIN, FZRA, ICE, and SNOW for plotting winter precip type. Ahijevych added this -fieldinfo['ptype-prob']['levels'] = [fieldinfo['precip']['levels'][1:],fieldinfo['snow']['levels'],fieldinfo['ice']['levels'],fieldinfo['fzra']['levels']] +fieldinfo['ptypes']['levels'] = [fieldinfo['precip']['levels'][1:],fieldinfo['snow']['levels'],fieldinfo['ice']['levels'],fieldinfo['fzra']['levels']] # domains = { 'domainname': { 'corners':[ll_lat,ll_lon,ur_lat,ur_lon], 'figsize':[w,h] } } domains = { 'CONUS' :{ 'corners': [23.1593,-120.811,46.8857,-65.0212], 'fig_width': 1080 }, diff --git a/webplot.py b/webplot.py index 449dcc6..634d7c9 100755 --- a/webplot.py +++ b/webplot.py @@ -214,7 +214,7 @@ def plotFields(self): def plotFill(self): if self.opts['fill']['name'] == 'ptype': self.plotFill_ptype(); return - if self.opts['fill']['name'] == 'ptype-prob': self.plotFill_ptype_prob(); return + if self.opts['fill']['name'] == 'ptypes': self.plotFill_ptypes(); return elif self.opts['fill']['name'] == 'crefuh': self.plotReflectivityUH(); return if self.autolevels: @@ -269,7 +269,7 @@ def plotFill_ptype(self): cb.set_ticklabels(['Rain', 'Freezing Rain', 'Sleet', 'Snow']) cb.ax.tick_params(length=0) - def plotFill_ptype_prob(self): + def plotFill_ptypes(self): types = self.data['fill'] ntypes = len(types) From 25732e8ef43308aedc553ad8edcffb256f41f882 Mon Sep 17 00:00:00 2001 From: David Ahijevych Date: Thu, 1 Feb 2018 13:08:43 -0700 Subject: [PATCH 05/68] add UPP and GSD precipitation type probability --- fieldinfo.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/fieldinfo.py b/fieldinfo.py index 51b5ccb..ab2bd38 100755 --- a/fieldinfo.py +++ b/fieldinfo.py @@ -1,4 +1,7 @@ import os +import numpy as np +tenths = np.arange(0.1,1.1,0.1) +fifths = np.arange(0.2,1.2,0.2) def readcm(name): '''Read colormap from file formatted as 0-1 RGB CSV''' @@ -151,7 +154,9 @@ def readNCLcm(name): 'stp' :{ 'levels' : [0.5,0.75,1.0,1.5,2.0,3.0,4.0,5.0,6.0,7.0,8.0], 'cmap':readNCLcm('perc2_9lev'), 'fname':['SBCAPE','LCL_HEIGHT','SR_HELICITY_1KM','UBSHR6','VBSHR6'], 'arraylevel':[None,None,None,None,None], 'filename':'upp'}, #'sigsvr :{ 'levels' : [1e5,2e5,3e5,4e5,5e5,6e5,8e5,10e5], 'cmap':readNCLcm('prcp_1'), 'fname':['MLCAPE','UHSHR], 'filename':'upp'} 'ptype' :{ 'levels' : [0.01,0.1,0.25,0.5,0.75,1,1.5,2,2.5,3,3.5,4], 'cmap':['#dddddd','#aaaaaa']+readNCLcm('precip3_16lev')[1:], 'fname':['AFWA_RAIN_HRLY', 'AFWA_FZRA_HRLY', 'AFWA_ICE_HRLY', 'AFWA_SNOWFALL_HRLY'], 'filename':'wrfout'}, - 'ptypes' :{ 'cmap':['green','blue','orange','red'], 'fname':['AFWA_RAIN_HRLY', 'AFWA_SNOWFALL_HRLY', 'AFWA_ICE_HRLY','AFWA_FZRA_HRLY'], 'filename':'wrfout'}, + 'ptypes' :{ 'levels' : [fifths,fifths,fifths,fifths], 'cmap':['green','blue','orange','red'], 'fname':['AFWA_RAIN_HRLY', 'AFWA_SNOWFALL_HRLY', 'AFWA_ICE_HRLY','AFWA_FZRA_HRLY'], 'filename':'wrfout'}, + 'ptypes-upp' :{ 'levels' : [fifths,fifths,fifths,fifths], 'cmap':['green','blue','orange','red'], 'fname':['UPP_CRAIN', 'UPP_CSNOW', 'UPP_CICEP', 'UPP_CFRZR'], 'filename':'upp'}, + 'ptypes-gsd' :{ 'levels' : [fifths,fifths,fifths,fifths], 'cmap':['green','blue','orange','red'], 'fname':[ 'CRAIN', 'CSNOW', 'CICEP', 'CFRZR'], 'filename':'upp'}, 'winter' :{ 'levels' : [0.01,0.1,0.25,0.5,0.75,1,1.5,2,2.5,3,3.5,4], 'cmap':['#dddddd','#aaaaaa']+readNCLcm('precip3_16lev')[1:], 'fname':['AFWA_RAIN_HRLY', 'AFWA_FZRA_HRLY', 'AFWA_ICE_HRLY', 'AFWA_SNOWFALL_HRLY'], 'filename':'wrfout'}, 'crefuh' :{ 'levels' : [5,10,15,20,25,30,35,40,45,50,55,60,65,70], 'cmap': readcm('cmap_rad.rgb')[0:13], 'fname': ['REFL_MAX_COL', 'MAX_UPDRAFT_HELICITY'], 'filename':'upp' }, @@ -168,8 +173,8 @@ def readNCLcm(name): 'bunkers' :{ 'fname' : ['U_COMP_STM_6KM', 'V_COMP_STM_6KM'], 'filename': 'upp', 'skip':40 }, } -# Combine levels from RAIN, FZRA, ICE, and SNOW for plotting winter precip type. Ahijevych added this -fieldinfo['ptypes']['levels'] = [fieldinfo['precip']['levels'][1:],fieldinfo['snow']['levels'],fieldinfo['ice']['levels'],fieldinfo['fzra']['levels']] +# Combine levels from RAIN, FZRA, ICE, and SNOW for plotting 1-hr accumulated precip for each type. Ahijevych added this +#fieldinfo['ptypes']['levels'] = [fieldinfo['precip']['levels'][1:],fieldinfo['snow']['levels'],fieldinfo['ice']['levels'],fieldinfo['fzra']['levels']] # domains = { 'domainname': { 'corners':[ll_lat,ll_lon,ur_lat,ur_lon], 'figsize':[w,h] } } domains = { 'CONUS' :{ 'corners': [23.1593,-120.811,46.8857,-65.0212], 'fig_width': 1080 }, From d107bdcd2f31dc7c7d44b1801d750683b4dd7001 Mon Sep 17 00:00:00 2001 From: David Ahijevych Date: Thu, 1 Feb 2018 13:09:35 -0700 Subject: [PATCH 06/68] Allow ptypes-gsd and ptypes-upp to branch off into ptypes_fill subroutine. --- webplot.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/webplot.py b/webplot.py index 634d7c9..e3a1b34 100755 --- a/webplot.py +++ b/webplot.py @@ -214,7 +214,7 @@ def plotFields(self): def plotFill(self): if self.opts['fill']['name'] == 'ptype': self.plotFill_ptype(); return - if self.opts['fill']['name'] == 'ptypes': self.plotFill_ptypes(); return + if self.opts['fill']['name'][0:6] == 'ptypes': self.plotFill_ptypes(); return elif self.opts['fill']['name'] == 'crefuh': self.plotReflectivityUH(); return if self.autolevels: @@ -284,6 +284,7 @@ def plotFill_ptypes(self): type_labels = ['Rain', 'Snow', 'Sleet', 'Freezing Rain'] # Hard-coded if any(len(lst) != ntypes for lst in [colors, threshes, type_labels]): print "data, colors, threshes, and type_labels must all be same length" + print ntypes, colors, threshes, type_labels sys.exit(1) # make axes for colorbar, 175px to left and 35px down from bottom of map x0, y0 = self.ax.transAxes.transform((0,0)) From 687761e790151056cebac7c7f2080e18ac1cae70 Mon Sep 17 00:00:00 2001 From: David Ahijevych Date: Thu, 1 Feb 2018 13:10:15 -0700 Subject: [PATCH 07/68] First commit --- make_webplot.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100755 make_webplot.py diff --git a/make_webplot.py b/make_webplot.py new file mode 100755 index 0000000..8047ccc --- /dev/null +++ b/make_webplot.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python + +import sys, time, os +from webplot import webPlot, readGrid, saveNewMap + +def log(msg): print time.ctime(time.time()),':', msg + +log('Begin Script') +stime = time.time() + +newPlot = webPlot() + +log('Reading Data') +newPlot.readEnsemble() + +#for dom in ['CONUS', 'NGP', 'SGP', 'CGP']: +for dom in ['CONUS', 'NGP', 'SGP', 'CGP','SW','NW','SE','NE','MATL']: + file_not_created, num_attempts = True, 0 + while file_not_created and num_attempts <= 3: + newPlot.domain = dom + + newPlot.createFilename() + fname = newPlot.outfile + + log('Loading Map for %s'%newPlot.domain) + newPlot.loadMap() + + log('Plotting Data') + newPlot.plotFields() + newPlot.plotTitleTimes() + + log('Writing Image') + newPlot.saveFigure() + + if os.path.exists(fname): + file_not_created = False + log('Created %s, %.1f KB'%(fname,os.stat(fname).st_size/1000.0)) + + num_attempts += 1 + +etime = time.time() +log('End Plotting (took %.2f sec)'%(etime-stime)) + From 923f2b329e0ecbad1b6db96607e02084fc9a6d17 Mon Sep 17 00:00:00 2001 From: ahijevyc Date: Tue, 19 Mar 2019 14:52:04 -0600 Subject: [PATCH 08/68] first commit. mostly copied from python2 library --- Fanglin.py | 19 ++ atcf.py | 617 ++++++++++++++++++++++++++++++++++++++++++++++ cache.py | 80 ++++++ fieldinfo.py | 224 +++++++++++++++++ get_model_time.py | 62 +++++ mpas.py | 72 ++++++ mysavfig.py | 16 ++ myskewt.py | 294 ++++++++++++++++++++++ spc.py | 249 +++++++++++++++++++ t.py | 237 ++++++++++++++++++ 10 files changed, 1870 insertions(+) create mode 100644 Fanglin.py create mode 100644 atcf.py create mode 100644 cache.py create mode 100755 fieldinfo.py create mode 100644 get_model_time.py create mode 100644 mpas.py create mode 100644 mysavfig.py create mode 100755 myskewt.py create mode 100644 spc.py create mode 100644 t.py diff --git a/Fanglin.py b/Fanglin.py new file mode 100644 index 0000000..ba4f688 --- /dev/null +++ b/Fanglin.py @@ -0,0 +1,19 @@ +import numpy as np + +def confidence_interval(x): + nsz = x.size + std = np.std(x) + # From 11 Aug 2016 email from Fanglin + # 95% confidence level defined by {-intvl, intvl} + if nsz>=80: + intvl=1.960*std/np.sqrt(nsz-1) + if nsz>=40 and nsz <80: + intvl=2.000*std/np.sqrt(nsz-1) + if nsz>=20 and nsz <40: + intvl=2.042*std/np.sqrt(nsz-1) + if nsz<20: + intvl=2.228*std/np.sqrt(nsz-1) + + return intvl + + diff --git a/atcf.py b/atcf.py new file mode 100644 index 0000000..f1eb336 --- /dev/null +++ b/atcf.py @@ -0,0 +1,617 @@ +import pandas as pd +import pdb +import re +import csv +import os, sys +from netCDF4 import Dataset +import numpy as np + + +def kts2category(kts): + category = -1 + if kts > 34: + category = 0 + if kts > 64: + category = 1 + if kts > 83: + category = 2 + if kts > 96: + category = 3 + if kts > 113: + category = 4 + if kts > 137: + category = 5 + return category + + +ifile = '/glade/work/ahijevyc/work/atcf/Irma.ECMWF.dat' +ifile = '/glade/scratch/mpasrt/uni/2018071700/latlon_0.500deg_0.25km/gfdl_tracker/tcgen/fort.64' + + + +# Standard ATCF columns (doesn't include track id, like in fort.66). +# https://www.nrlmry.navy.mil/atcf_web/docs/database/new/abrdeck.html +atcfcolumns=["basin","cy","initial_time","technum","model","fhr","lat","lon","vmax","minp","ty", + "rad", "windcode", "rad1", "rad2", "rad3", "rad4", "pouter", "router", "rmw", "gusts", "eye", + "subregion", "maxseas", "initials", "dir", "speed", "stormname", "depth", "seas", "seascode", + "seas1", "seas2", "seas3", "seas4", "userdefined", "userdata"] + +def cyclone_phase_space_columns(): + names = [] + names.append('cpsB') # Cyclone Phase Space "Parameter B" for thermal asymmetry. (Values are *10) + names.append('cpsll') # Cyclone Phase Space lower level (600-900 mb) thermal wind parameter, for diagnosing low-level warm core. (Values are *10) + names.append('cpsul') # Cyclone Phase Space upper level (300-600 mb) thermal wind parameter, for diagnosing upper-level warm core. (Values are *10) + return names + + +def getcy(cys): + return cys[0:2] + +def read(ifile = ifile, debug=False, fullcircle=False): + # Read data into Pandas Dataframe + print('reading', ifile, 'fullcircle=', fullcircle) + names = list(atcfcolumns) # make a copy of list, not a copy of the reference to the list. + converters={ + # The problem with CY is ATCF only reserves 2 characters for it. + "cy" : lambda x: x.strip(), # cy is not always an integer (e.g. 10E) # Why strip leading zeros? + "initial_time" : lambda x: pd.to_datetime(x.strip(),format='%Y%m%d%H'), + "technum" : lambda x: x.strip(), #strip leading spaces but not leading zeros + "model" : lambda x: x.strip(), # Strip leading whitespace - for matching later. + "vmax": float, + "minp": float, + "minp": float, + "ty" : lambda x: x.strip(), + "windcode" : lambda x: x[-3:], + "rad1": float, + "rad2": float, + "rad3": float, + "rad4": float, + "pouter": float, + "router": float, + "subregion": lambda x: x[-2:], + # subregion ends up being 3 when written with .to_string + # strange subregion only needs one character, but official a-decks leave 3. + "initials" : lambda x: x[-3:], + 'stormname': lambda x: x[-9:], + 'depth' : lambda x: x[-1:], + "seascode" : lambda x: x[-3:], + "seas1": float, + "seas2": float, + "seas3": float, + "seas4": float, + "userdefined" : lambda x: x.strip(), + "userdata" : lambda x: x.strip(), + } + dtype={ + 'rmw' : np.float64, + 'gusts' : np.float64, + 'eye' : np.float64, + 'maxseas' : np.float64, + 'dir' : np.float64, + 'speed' : np.float64, + "seas" : np.float64, + } + + # Tried using converter for these columns, but couldn't convert 4-space string to float. + # If you add a key na_values, also add it to dtype dict, and remove it from converters. + na_values = { + "rmw" : 4*' ', + "gusts" : 4*' ', + "eye" : 4*' ', + "maxseas" : 4*' ', + "dir" : 4*' ', + "speed" : 4*' ', + "seas" : 3*' ', # one less than other columns + } + + + reader = csv.reader(open(ifile),delimiter=',') + testline = next(reader) + num_cols = len(testline) + if debug: + print(testline) + print('num_cols=',num_cols) + del reader + + # Output from HWRF vortex tracker, fort.64 and fort.66 + # are mostly ATCF format but have subset of columns + if num_cols == 43: + print('assume HWRF tracker fort.64-style output with 43 columns in', ifile) + TPstr = "THERMO PARAMS" + if testline[35].strip() != TPstr: + print("expected 36th column to be", TPstr) + print("got", testline[35].strip()) + sys.exit(4) + for ii in range(20,35): + names[ii] = "space filler" + names = names[0:35] + names.append(TPstr) + names.extend(cyclone_phase_space_columns()) + names.append('warmcore') + names.append("warmcore_strength") + names.append("string") + names.append("string") + + # fort.66 has track id in the 3rd column. + if num_cols == 31: + print('assume fort.66-style with 31 columns in', ifile) + # There is a cyclogenesis ID column for fort.66 + if debug: + print('inserted ID for cyclogenesis in column 2') + names.insert(2, 'id') # ID for the cyclogenesis + print('Using 1st 21 elements of names list') + names = names[0:21] + if debug: + print('redefining columns 22-31') + names.extend(cyclone_phase_space_columns()) + names.append('warmcore') + names.append('dir') + names.append('speedms') + names.append('vort850mb') + names.append('maxvort850mb') + names.append('vort700mb') + names.append('maxvort700mb') + + # TODO read IDL output + if num_cols == 44 and 'min_warmcore_fract d' in testline[35]: + if debug: + print("Looks like IDL output") + names = [n.replace('userdata', 'min_warmcore_fract') for n in names] + names.append('dT500') + names.append('dT200') + names.append('ddZ850200') + names.append('rainc') + names.append('rainnc') + names.append('id') + + if num_cols == 11: + print("Assuming simple adeck with 11 columns") + if ifile[-4:] != '.dat': + print("even though file doesn't end in .dat", ifile) + names = names[0:11] + + usecols = list(range(len(names))) + + # If you get a beyond index range (or something like that) error, see if userdata column is intermittent and has commas in it. + # If so, clean it up (i.e. truncate it) + + #atrack = ['basin','cy','initial_time','technum','model'] + #if 'id' in names: + # atrack.append('id') + + if debug: + print("before pd.read_csv") + print('column names', names) + print("usecols=",usecols) + print("converters=",converters) + print("dype=", dtype) + df = pd.read_csv(ifile,index_col=None,header=None, delimiter=",", usecols=usecols, names=names, + converters=converters, na_values=na_values, dtype=dtype) + # fort.64 has asterisks sometimes. Problem with hwrf_tracker. + badlines = df['lon'].str.contains("\*") + if any(badlines): + df = df[~badlines] + + # Extract last character of lat and lon columns + # Multiply integer by -1 if "S" or "W" + # Divide by 10 + S = df.lat.str[-1] == 'S' + lat = df.lat.str[:-1].astype(float) / 10. + lat[S] = lat[S] * -1 + df.lat = lat + W = df.lon.str[-1] == 'W' + lon = df.lon.str[:-1].astype(float) / 10. + lon[W] = lon[W] * -1 + df.lon = lon + + if debug: + pdb.set_trace() + # Derive valid time. valid_time = initial_time + fhr + # Use datetime module to add, where yyyymmddh is a datetime object and fhr is a timedelta object. + df['valid_time'] = df.initial_time + pd.to_timedelta(df.fhr, unit='h') + + for col in atcfcolumns: + if col not in df.columns: + if debug: + print(col, 'not in DataFrame. Fill with appropriate value.') + # if 'rad' column doesn't exist make it zeroes + if col in ['rad', 'rad1', 'rad2', 'rad3', 'rad4','pouter', 'router', 'seas', 'seas1','seas2','seas3','seas4']: + df[col] = 0. + + # Initialize other default values. + if col in ['windcode', 'seascode']: + df[col] = ' ' + + # Numbers are NaN + if col in ['rmw','gusts','eye','maxseas','dir','speed']: + df[col] = np.NaN + + # Strings are empty + if col in ['subregion','stormname','depth','userdefined','userdata']: + df[col] = '' + + if col in ['initials', 'depth']: + df[col] = 'X' + + + if debug: + pdb.set_trace() + if fullcircle: + if debug: + print("full circle wind radii") + # Full circle wind radii instead of quadrants + # TODO better way than this hack + df['windcode'] = 'AAA' + df['rad1'] = df[['rad1','rad2','rad3','rad4']].max(axis=1) + df['rad2'] = 0 + df['rad3'] = 0 + df['rad4'] = 0 + + return df + +def x2s(x): + # Convert absolute value of float to integer number of tenths for ATCF lat/lon + # called by lat2s and lon2s + x *= 10 + x = np.around(x) + x = np.abs(x) + return str(int(x)) + +def lat2s(lat): + NS = 'N' if lat >= 0 else 'S' + lat = x2s(lat) + NS + ',' + return lat + +def lon2s(lon): + EW = 'E' if lon >= 0 else 'W' + lon = '%4s' % x2s(lon) + EW + ',' + return lon + +# function to compute great circle distance between point lat1 and lon1 and arrays of points +# given by lons, lats +# Returns 2 things: +# 1) distance in km +# 2) initial bearing from 1st pt to 2nd pt. +def dist_bearing(lon1,lons,lat1,lats): + lon1 = np.radians(lon1) + lons = np.radians(lons) + lat1 = np.radians(lat1) + lats = np.radians(lats) + # great circle distance. + arg = np.sin(lat1)*np.sin(lats)+np.cos(lat1)*np.cos(lats)*np.cos(lon1-lons) + #arg = np.where(np.fabs(arg) < 1., arg, 0.999999) + + dlon = lons-lon1 + bearing = np.arctan2(np.sin(dlon)*np.cos(lats), np.cos(lat1)*np.sin(lats) - np.sin(lat1)*np.cos(lats)*np.cos(dlon)) + + # convert from radians to degrees + bearing = np.degrees(bearing) + + # -180 - 180 -> 0 - 360 + bearing = (bearing + 360) % 360 + + # Ellipsoid [CLARKE 1866] Semi-Major Axis (Equatorial Radius) + a = 6378.2064 + return np.arccos(arg)* a, bearing + + + +ms2kts = 1.94384 +km2nm = 0.539957 + +quads = {'NE':0, 'SE':90, 'SW':180, 'NW':270} +thresh_kts = np.array([34, 50, 64]) + + + +def get_max_ext_of_wind(speed_kts, distance_km, bearing, raw_vmax_kts, quads=quads, thresh_kts=thresh_kts, debug=False): + rad_nm = {} + # Put in dictionary "rad_nm" where rad_nm = { + # 34: {'NE':rad1, 'SE':rad2, 'SW':rad3, 'NW':rad4}, + # 50: {'NE':rad1, 'SE':rad2, 'SW':rad3, 'NW':rad4}, + # 64: {'NE':rad1, 'SE':rad2, 'SW':rad3, 'NW':rad4} + # } + rad_nm['raw_vmax_kts'] = raw_vmax_kts + rad_nm['thresh_kts'] = thresh_kts + rad_nm['quads'] = quads + for wind_thresh_kts in thresh_kts[thresh_kts < raw_vmax_kts]: + rad_nm[wind_thresh_kts] = {} + for quad,az in quads.items(): + # Originally had distance_km < 800, but Chris D. suggested 300nm in Sep 2018 email + # This was to deal with Irma and the unrelated 34 knot onshore flow in Georgia + # Looking at HURDAT2 R34 sizes (since 2004), ex-tropical storm Karen 2015 had 710nm. + # Removing EX storms, the max was 480 nm in Hurricane Sandy 2012 + iquad = (az <= bearing) & (bearing < az+90) & (speed_kts >= wind_thresh_kts) & (distance_km < 300./km2nm) + rad_nm[wind_thresh_kts][quad] = 0 + if np.sum(iquad) > 0: + max_dist_of_wind_threshold = np.max(distance_km[iquad]) * km2nm + imax_dist_of_wind_threshold = np.argmax(distance_km[iquad]) + if debug: + print('get_max_ext_of_wind():', wind_thresh_kts, quad, '%3d-%3d'%(az,az+90), '%4d'%np.sum(iquad), '%10.6f'%max_dist_of_wind_threshold, '%10.6f'%bearing[iquad][imax_dist_of_wind_threshold]) + rad_nm[wind_thresh_kts][quad] = max_dist_of_wind_threshold + return rad_nm + + +def derived_winds(u10, v10, mslp, lonCell, latCell, row, vmax_search_radius=250., mslp_search_radius=100., debug=False): + + + # Given a row (with row.lon and row.lat)... + + # Derive cell distances and bearings + distance_km, bearing = dist_bearing(row.lon, lonCell, row.lat, latCell) + + # Derive 10m wind speed and Vt from u10 and v10 + speed_kts = np.sqrt(u10**2 + v10**2) * ms2kts + + # Tangential (cyclonic) wind speed + # v dx - u dy + dx = lonCell - row.lon + # work on the dateline? + dx[dx>=180] = dx[dx>=180]-360. + dy = latCell - row.lat + Vt = v10 * dx - u10 * dy + if row.lat < 0: + Vt = -Vt + + # Restrict Vmax search + vmaxrad = distance_km < vmax_search_radius + ispeed_max = np.argmax(speed_kts[vmaxrad]) + raw_vmax_kts = speed_kts[vmaxrad].max() + + # Check if tangential component of max wind is negative (anti-cyclonic) + if Vt[vmaxrad][ispeed_max] < 0: + print("center", row.valid_time, row.lat, row.lon) + print("max wind is anti-cyclonic!", Vt[vmaxrad][ispeed_max]) + print("max wind lat/lon", latCell[vmaxrad][ispeed_max], lonCell[vmaxrad][ispeed_max]) + print("max wind U/V", u10[vmaxrad][ispeed_max], v10[vmaxrad][ispeed_max]) + if debug: pdb.set_trace() + + # Check if average tangential wind within search radius is negative (anti-cyclonic) + average_tangential_wind = np.average(Vt[vmaxrad]) + if average_tangential_wind < 0: + print("center", row.valid_time, row.lat, row.lon) + print("avg wind is anti-cyclonic!", average_tangential_wind) + if debug: pdb.set_trace() + + # Get radius of max wind + raw_RMW_nm = distance_km[vmaxrad][ispeed_max] * km2nm + if debug: + print('max wind lat', latCell[vmaxrad][ispeed_max], 'lon', lonCell[vmaxrad][ispeed_max]) + + # Restrict min mslp search + mslprad = distance_km < mslp_search_radius + raw_minp = mslp[mslprad].min() / 100. + + # Get max extent of wind at thresh_kts thresholds. + rad_nm = get_max_ext_of_wind(speed_kts, distance_km, bearing, raw_vmax_kts, debug=debug) + + return raw_vmax_kts, raw_RMW_nm, raw_minp, rad_nm + + +def add_wind_rad_lines(row, rad_nm, fullcircle=False, debug=False): + raw_vmax_kts = rad_nm['raw_vmax_kts'] + thresh_kts = rad_nm['thresh_kts'] + # if not empty...must be NEQ + if row.windcode.strip() and row.windcode != 'NEQ': + print('bad windcode', row.windcode, 'in', row) + print('expected NEQ') + sys.exit(1) + lines = pd.DataFrame() + for thresh in thresh_kts[thresh_kts < raw_vmax_kts]: + if any(rad_nm[thresh].values()): + newrow = row.copy() + newrow[['windcode','rad','rad1','rad2','rad3','rad4']] = ['NEQ', thresh, rad_nm[thresh]['NE'], rad_nm[thresh]['SE'], rad_nm[thresh]['SW'], rad_nm[thresh]['NW']] + # Append row with 34, 50, or 64 knot radii + lines = lines.append(newrow) + if fullcircle: + # Append row with full circle 34, 50, or 64 knot radius + # MET-TC will not derive this on its own - see email from John Halley-Gotway Oct 11, 2018 + # Probably shouldn't have AAA and NEQ in same file. + newrow = row.copy() + newrow[['windcode','rad','rad1']] = ['AAA',thresh, np.nanmax(list(rad_nm[thresh].values()))] + lines = lines.append(newrow) + + return lines + + +def update_df(df, row, raw_vmax_kts, raw_RMW_nm, raw_minp, rad_nm, debug=False): + + # Called by origgrid and origmesh + + if debug: + print('before', row[['valid_time','lon','lat', 'vmax', 'minp', 'rmw']]) + row.vmax = raw_vmax_kts + row.minp = raw_minp + row.rmw = raw_RMW_nm + # Add note of original mesh = True in user data (not defined) column + row.userdata += 'origmeshTrue' + if debug: + print('after', row[['vmax', 'minp', 'rmw']]) + + # Can't get rid of SettingWithCopyWarning! + df.loc[row.name,:] = row + + + # Append 34/50/64 knot lines to DataFrame + newlines = add_wind_rad_lines(row, rad_nm, debug=debug) + # If there are new lines, drop the old one and append new ones. + if not newlines.empty: + df.drop(row.name, inplace=True) + df = df.append(newlines) + + + + # Sort DataFrame by index (deal with appended wind radii lines) + # sort by rad too + df = df.sort_index().sort_values(['initial_time','fhr','rad']) + + return df + + + + + +def write(ofile, df, fullcircle=False, debug=False): + if df.empty: + print("afcf.write(): DataFrame is empty.", ofile, "not written") + return + + # TODO: deal with fullcircle. + print("writing", ofile) + + # Valid time is not part of ATCF file. + del(df["valid_time"]) + + if debug: + pdb.set_trace() + # Had to add parentheses () to .len to not get error about instancemethod not being iterable. + if max(df.cy.str.len()) > 2: + print('cy more than 2 characters. Truncating...') + # Append full CY to userdata column (after a comma) + df['userdata'] = df['userdata'] + ', ' + df['cy'] + # Keep first 2 characters + df['cy'] = df['cy'].str.slice(0,2) + formatters={ + "basin": '{},'.format, + # The problem with CY is ATCF only reserves 2 characters for it. + "cy": lambda x: x.zfill(2)+"," , # not always an integer (e.g. 10E) # 20181116 force to be integer + # Convert initial_time from datetime to string. + "initial_time":lambda x: x.strftime('%Y%m%d%H,'), + "technum":'{},'.format, + "model":'{},'.format, + "fhr":'{:3.0f},'.format, + "lat":lat2s, + "lon":lon2s, + "vmax":'{:3.0f},'.format, + "minp":'{:4.0f},'.format, + "ty":'{},'.format, + "windcode":'{:>3s},'.format, + "rad":'{:3.0f},'.format, + "rad1":'{:4.0f},'.format, + "rad2":'{:4.0f},'.format, + "rad3":'{:4.0f},'.format, + "rad4":'{:4.0f},'.format, + "pouter":'{:4.0f},'.format, + "router":'{:4.0f},'.format, + "rmw":'{:3.0f},'.format, + "gusts":'{:3.0f},'.format, + "eye":'{:3.0f},'.format, + "subregion":'{:>2s},'.format, + "maxseas":'{:3.0f},'.format, + "initials":'{:>3s},'.format, + "dir":'{:3.0f},'.format, + "speed":'{:3.0f},'.format, + "stormname":'{:>9s},'.format, + "depth":'{:>1s},'.format, + "seas":'{:2.0f},'.format, + "seascode":'{:>3s},'.format, + "seas1":'{:4.0f},'.format, + "seas2":'{:4.0f},'.format, + "seas3":'{:4.0f},'.format, + "seas4":'{:4.0f},'.format, + "userdefined":'{:>18s},'.format, + "cpsB":'{:4.0f},'.format, + "cpsll":'{:4.0f},'.format, + "cpsul":'{:4.0f},'.format, + "warmcore":'{},'.format, + "direction":'{},'.format, + } + junk = df.to_string(header=False, index=False, na_rep=' ', columns=atcfcolumns, formatters=formatters) + + # TODO: FIX IDL-STYLE COLUMNS. STORMNAME IS MISSING A LETTER + + # na_rep=' ' has no effect + # strings have extra space in front of them + junk = junk.split('\n') + # replace first 3 occurrences of ', ' with ', '. + # replace ', XX,' with ', XX,' + # replace 'nan' with ' ' + junk = [j.replace(', ', ', ', 3).replace(', XX,',', XX,').replace('nan',' ') for j in junk] + #delete space before windcode + junk = [j[:68]+j[69:] for j in junk] + #delete space before initials + junk = [j[:133]+j[134:] for j in junk] + #delete space before depth + junk = [j[:161]+j[162:] for j in junk] + #delete space before seascode + junk = [j[:168]+j[169:] for j in junk] + junk = '\n'.join(junk) + + if debug: + pdb.set_trace() + + f = open(ofile, "w") + f.write(junk+"\n") + + f.close() + print("wrote", ofile) + + +def origgrid(df, griddir, debug=False): + # Get vmax, minp, radius of max wind, max radii of wind thresholds from ECMWF grid, not from tracker. + # Assumes + # ECMWF data came from TIGGE and were converted from GRIB to netCDF with ncl_convert2nc. + # 4-character model string in ATCF file is "EExx" (where xx is the 2-digit ensemble member). + # ECMWF ensemble member in directory named "ens_xx" (where xx is the 2-digit ensemble member). + # File path is "ens_xx/${gs}yyyymmddhh.xx.nc", where ${gs} is the grid spacing (0p15, 0p25, or 0p5). + + for run_id, group in df.groupby(['initial_time', 'model']): + initial_time, model = run_id + m = re.search(r'EE(\d\d)', model) + if not m: + if debug: + print('Assuming ECMWF ensemble member, but did not find EE\d\d in model string') + print('no original grid for',model,'- skipping') + continue + ens = int(m.group(1)) # strip leading zero + if ens < 1: + continue + # Allow some naming conventions + # ens_n/yyyymmddhh.n.nc + # ens_n/0p15yyyymmddhh_sfc.nc + # ens_n/0p25yyyymmddhh_sfc.nc + # ens_n/0p5yyyymmddhh_sfc.nc + yyyymmddhh = initial_time.strftime('%Y%m%d%H') + # If first filename doesn't exist, try the next one, and so on... + # List in order of most preferred to least preferred. + potential_gridfiles = [ + "ens_"+str(ens)+"/"+ "0p15"+yyyymmddhh+"."+str(ens)+".nc", + "ens_"+str(ens)+"/"+ "0p25"+yyyymmddhh+"."+str(ens)+".nc", + "ens_"+str(ens)+"/"+ "0p5"+yyyymmddhh+"."+str(ens)+".nc", + "ens_"+str(ens)+"/"+ yyyymmddhh+"."+str(ens)+".nc" + ] + for gridfile in potential_gridfiles: + if os.path.isfile(griddir + gridfile): + break + else: + print("no", griddir + gridfile) + + print('opening', gridfile) + nc = Dataset(griddir + gridfile, "r") + lon = nc.variables['lon_0'][:] + lat = nc.variables['lat_0'][:] + lonCell,latCell = np.meshgrid(lon, lat) + u10s = nc.variables['10u_P1_L103_GLL0'][:] + v10s = nc.variables['10v_P1_L103_GLL0'][:] + mslps = nc.variables['msl_P1_L101_GLL0'][:] + model_forecast_times = nc.variables['forecast_time0'][:] + nc.close() + for index, row in group.iterrows(): + if not any(model_forecast_times == row.fhr): + print(row.fhr, 'not in model file') + continue + itime = np.argmax(model_forecast_times == row.fhr) + u10 = u10s[itime,:,:] + v10 = v10s[itime,:,:] + mslp = mslps[itime,:,:] + + # Extract vmax, RMW, minp, and radii of wind thresholds + raw_vmax_kts, raw_RMW_nm, raw_minp, rad_nm = derived_winds(u10, v10, mslp, lonCell, latCell, row, debug=debug) + + + df = update_df(df, row, raw_vmax_kts, raw_RMW_nm, raw_minp, rad_nm, debug=debug) + + return df + +if __name__ == "__main__": + read() diff --git a/cache.py b/cache.py new file mode 100644 index 0000000..8fcf226 --- /dev/null +++ b/cache.py @@ -0,0 +1,80 @@ +# Copied from http://code.activestate.com/recipes/491261-caching-and-throttling-for-urllib2/ May 16, 2017 +import http.client +import unittest +import hashlib +import urllib.request, urllib.error, urllib.parse +import os +import io +__version__ = (0,1) +__author__ = "Staffan Malmgren " + + +class CacheHandler(urllib.request.BaseHandler): + """Stores responses in a persistant on-disk cache. + + If a subsequent GET request is made for the same URL, the stored + response is returned, saving time, resources and bandwith""" + def __init__(self,cacheLocation): + """The location of the cache directory""" + self.cacheLocation = cacheLocation + if not os.path.exists(self.cacheLocation): + os.mkdir(self.cacheLocation) + + def default_open(self,request): + if ((request.get_method() == "GET") and + (CachedResponse.ExistsInCache(self.cacheLocation, request.get_full_url()))): + # print "CacheHandler: Returning CACHED response for %s" % request.get_full_url() + return CachedResponse(self.cacheLocation, request.get_full_url(), setCacheHeader=True) + else: + return urllib.request.urlopen(request.get_full_url()) + + def http_response(self, request, response): + if request.get_method() == "GET": + if 'd-cache' not in response.info(): + CachedResponse.StoreInCache(self.cacheLocation, request.get_full_url(), response) + return CachedResponse(self.cacheLocation, request.get_full_url(), setCacheHeader=False) + else: + return CachedResponse(self.cacheLocation, request.get_full_url(), setCacheHeader=True) + else: + return response + +class CachedResponse(io.StringIO): + """An urllib2.response-like object for cached responses. + + To determine wheter a response is cached or coming directly from + the network, check the x-cache header rather than the object type.""" + + def ExistsInCache(cacheLocation, url): + hash = hashlib.md5(url).hexdigest() + return (os.path.exists(cacheLocation + "/" + hash + ".headers") and + os.path.exists(cacheLocation + "/" + hash + ".body")) + ExistsInCache = staticmethod(ExistsInCache) + + def StoreInCache(cacheLocation, url, response): + hash = hashlib.md5(url).hexdigest() + f = open(cacheLocation + "/" + hash + ".headers", "w") + headers = str(response.info()) + f.write(headers) + f.close() + f = open(cacheLocation + "/" + hash + ".body", "w") + f.write(response.read()) + f.close() + StoreInCache = staticmethod(StoreInCache) + + def __init__(self, cacheLocation,url,setCacheHeader=True): + self.cacheLocation = cacheLocation + hash = hashlib.md5(url).hexdigest() + io.StringIO.__init__(self, file(self.cacheLocation + "/" + hash+".body").read()) + self.url = url + self.code = 200 + self.msg = "OK" + headerbuf = file(self.cacheLocation + "/" + hash+".headers").read() + if setCacheHeader: + headerbuf += "d-cache: %s/%s\r\n" % (self.cacheLocation,hash) + self.headers = http.client.HTTPMessage(io.StringIO(headerbuf)) + + def info(self): + return self.headers + def geturl(self): + return self.url + diff --git a/fieldinfo.py b/fieldinfo.py new file mode 100755 index 0000000..f49cab3 --- /dev/null +++ b/fieldinfo.py @@ -0,0 +1,224 @@ +import os # for NCARG_ROOT +import numpy as np +tenths = np.arange(0.1,1.1,0.1) +fifths = np.arange(0.2,1.2,0.2) + +def readcm(name): + '''Read colormap from file formatted as 0-1 RGB CSV''' + projdir = '/glade/u/home/wrfrt/wwe/python_scripts/' + fh = open(projdir+name, 'r') + rgb = np.loadtxt(fh) + fh.close() + return rgb.tolist() + +def readNCLcm(name): + '''Read in NCL colormap for use in matplotlib + Replaces original function in /glade/u/home/wrfrt/wwe/python_scripts/fieldinfo.py + ''' + + # comments start with ; or # + # first real line is ncolors = 256 (or something like that) + # The rest is bunch of rgb values, one trio per line. + + fh = open(os.getenv('NCARG_ROOT','/glade/u/apps/opt/ncl/6.5.0/intel/17.0.1')+'/lib/ncarg/colormaps/%s.rgb'%name, 'r') + rgb = np.loadtxt(fh, comments=[';', '#', 'n']) # treat ncolors=x as a comment + fh.close() + if rgb.max() > 1: + rgb = rgb/255.0 + return rgb.tolist() + + +fieldinfo = { + # surface and convection-related entries + 'precip' :{ 'levels' : [0,0.01,0.05,0.1,0.2,0.3,0.4,0.5,0.75,1,1.5,2,2.5,3.0], 'cmap': [readNCLcm('precip2_17lev')[i] for i in (0,1,2,4,5,6,7,8,10,12,13,14,15)], 'fname':['PREC_ACC_NC'] }, +# 'precip-24hr' :{ 'levels' : [0,0.01,0.05,0.1,0.25,0.5,0.75,1.0,1.25,1.5,2.0,3.0,4.0,5.0,6.0,7.0], 'cmap': [readNCLcm('precip2_17lev')[i] for i in (0,1,2,4,5,6,7,8,10,12,13,14,15,16,17)], 'fname':['PREC_ACC_NC'] }, + 'precip-24hr' :{ 'levels' : [0,0.01,0.05,0.1,0.25,0.5,0.75,1.0,1.25,1.5,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0,12.0,13.0], 'cmap': [readNCLcm('precip2_17lev')[i] for i in (0,1,2,4,5,6,7,8,10,12,13,14,15,16,17)]+['#777777', '#AAAAAA', '#CCCCCC', '#EEEEEE']+[readNCLcm('sunshine_diff_12lev')[i] for i in (4,2,1)], 'fname':['PREC_ACC_NC'] }, +# 'precipacc' :{ 'levels' : [0,0.01,0.05,0.1,0.25,0.5,0.75,1.0,1.25,1.5,2.0,3.0,4.0,5.0,6.0,7.0], 'cmap': [readNCLcm('precip2_17lev')[i] for i in (0,1,2,4,5,6,7,8,10,12,13,14,15,16,17)], 'fname':['RAINNC'] }, + 'precipacc':{ 'levels' : [0,0.01,0.05,0.1,0.25,0.5,0.75,1.0,1.25,1.5,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0,12.0,13.0], 'cmap': [readNCLcm('precip2_17lev')[i] for i in (0,1,2,4,5,6,7,8,10,12,13,14,15,16,17)]+['#777777', '#AAAAAA', '#CCCCCC', '#EEEEEE']+[readNCLcm('sunshine_diff_12lev')[i] for i in (4,2,1)], 'fname':['RAINNC'] }, + 'sbcape' :{ 'levels' : [100,250,500,750,1000,1250,1500,1750,2000,2500,3000,3500,4000,4500,5000,5500,6000], + 'cmap' : ['#eeeeee', '#dddddd', '#cccccc', '#aaaaaa']+readNCLcm('precip2_17lev')[3:-1], 'fname': ['SBCAPE'], 'filename':'upp' }, + 'mlcape' :{ 'levels' : [100,250,500,750,1000,1250,1500,1750,2000,2500,3000,3500,4000,4500,5000,5500,6000], + 'cmap' : ['#eeeeee', '#dddddd', '#cccccc', '#aaaaaa']+readNCLcm('precip2_17lev')[3:-1], 'fname': ['MLCAPE'], 'filename':'upp' }, + 'mucape' :{ 'levels' : [100,250,500,750,1000,1250,1500,1750,2000,2500,3000,3500,4000,4500,5000,5500,6000], + 'cmap' : ['#eeeeee', '#dddddd', '#cccccc', '#aaaaaa']+readNCLcm('precip2_17lev')[3:-1], 'fname': ['MUCAPE'], 'filename':'upp' }, + 'sbcinh' :{ 'levels' : [50,75,100,150,200,250,500], 'cmap': readNCLcm('topo_15lev')[1:], 'fname': ['SBCINH'], 'filename':'upp' }, + 'mlcinh' :{ 'levels' : [50,75,100,150,200,250,500], 'cmap': readNCLcm('topo_15lev')[1:], 'fname': ['MLCINH'], 'filename':'upp' }, + 'pwat' :{ 'levels' : [0.25,0.5,0.75,1.0,1.25,1.5,1.75,2.0,2.5,3.0,3.5,4.0], + 'cmap' : ['#dddddd', '#cccccc', '#e1e1d3', '#e1d5b1', '#ffffd5', '#e5ffa7', '#addd8e', '#41ab5d', '#007837', '#005529', '#0029b1'], + 'fname' : ['PWAT'], 'filename':'upp'}, + 'hailk1' :{ 'levels' : [0.25,0.5,0.75,1.0,1.25,1.5,1.75,2.0,2.5,3.0,3.5,4.0], 'cmap' : ['#eeeeee', '#dddddd', '#cccccc', '#aaaaaa']+readNCLcm('precip2_17lev')[3:-1], 'fname':['HAIL_MAXK1'], 'filename': 'diag' }, + 'hail2d' :{ 'levels' : [0.25,0.5,0.75,1.0,1.25,1.5,1.75,2.0,2.5,3.0,3.5,4.0], 'cmap' : ['#eeeeee', '#dddddd', '#cccccc', '#aaaaaa']+readNCLcm('precip2_17lev')[3:-1], 'fname':['HAIL_MAX2D'], 'filename': 'diag' }, + 'afhail' :{ 'levels' : [0.25,0.5,0.75,1.0,1.25,1.5,1.75,2.0,2.5,3.0,3.5,4.0], 'cmap' : ['#eeeeee', '#dddddd', '#cccccc', '#aaaaaa']+readNCLcm('precip2_17lev')[3:-1], 'fname': ['HAILCAST_DIAM_MAX'], 'filename':'diag' }, + 't2' :{ 'levels' : [-35,-30,-25,-20,-15,-10,-5,0,5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100,105,110,115,120], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['T2'] }, + 't2depart' :{ 'levels' : [-35,-30,-25,-20,-15,-10,-5,0,5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100,105,110,115,120], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['T2'] }, + 'mslp' :{ 'levels' : [960,964,968,972,976,980,984,988,992,996,1000,1004,1008,1012,1016,1020,1024,1028,1032,1036,1040,1044,1048,1052], 'cmap':readNCLcm('nice_gfdl')[3:193], 'fname':['MSLP'], 'filename': 'upp' }, + 'mslp-tc' :{ 'levels' : [908,912,916,920,924,928,932,936,940,944,948,952,956,960,964,968,972,976,980,984,988,992,996,1000,1004,1008,1012,1016,1020,1024,1028,1032,1036,1040,1044,1048,1052], 'cmap':readNCLcm('nice_gfdl')[3:193], 'fname':['MSLP'], 'filename': 'upp' }, + 'td2' :{ 'levels' : [-20,-15,-10,-5,0,5,10,15,20,25,30,35,40,45,50,55,60,64,68,72,76,80,84], + 'cmap':['#f3e7e8','#d0a0a4','#ad5960', '#8b131d', '#8b4513','#ad7c59', '#c5a289','#dcc7b8','#eeeeee', '#dddddd', '#bbbbbb', '#e1e1d3', '#e1d5b1','#ccb77a','#ffffe5','#f7fcb9', '#addd8e', '#41ab5d', '#006837', '#004529', '#195257', '#4c787c'], + 'fname' : ['DEWPOINT_2M'], 'filename':'upp'}, + 'td2depart' :{ 'levels' : [-20,-15,-10,-5,0,5,10,15,20,25,30,35,40,45,50,55,60,64,68,72,76,80,84], + 'cmap':['#f3e7e8','#d0a0a4','#ad5960', '#8b131d', '#8b4513','#ad7c59', '#c5a289','#dcc7b8','#eeeeee', '#dddddd', '#bbbbbb', '#e1e1d3', '#e1d5b1','#ccb77a','#ffffe5','#f7fcb9', '#addd8e', '#41ab5d', '#006837', '#004529', '#195257', '#4c787c'], + 'fname' : ['DEWPOINT_2M'], 'filename':'upp'}, + 'thetae' :{ 'levels' : [300,305,310,315,320,325,330,335,340,345,350,355,360], 'cmap': ['#eeeeee', '#dddddd', '#cccccc', '#aaaaaa']+readNCLcm('precip2_17lev')[3:-1], 'fname' : ['T2', 'Q2', 'PSFC'], 'filename': 'diag'}, + 'rh2m' :{ 'levels' : [0,10,20,30,40,50,60,70,80,90,100,110], 'cmap': readNCLcm('precip2_17lev')[:17][::-1], 'fname': ['T2', 'PSFC', 'Q2'], 'filename': 'diag'}, + 'heatindex' :{ 'levels' : [65,70,75,80,85,90,95,100,105,110,115,120,125,130], 'cmap': readNCLcm('MPL_hot')[::-1], 'fname': ['AFWA_HEATIDX'], 'filename':'diag' }, +# 'pblh' :{ 'levels' : [0,250,500,750,1000,1250,1500,1750,2000,2250,2500,2750,3000], 'cmap': readNCLcm('precip2_17lev')[3:-1], 'fname': ['C_PBLH'], 'filename':'diag' }, + 'pblh' :{ 'levels' : [0,250,500,750,1000,1250,1500,1750,2000,2500,3000,3500,4000], + 'cmap': ['#eeeeee', '#dddddd', '#cccccc', '#bbbbbb', '#44aaee','#88bbff', '#aaccff', '#bbddff', '#efd6c1', '#e5c1a1', '#eebb32', '#bb9918'], 'fname': ['PBLH'] }, + 'hmuh' :{ 'levels' : [10,25,50,75,100,125,150,175,200,250,300,400,500], 'cmap': readNCLcm('prcp_1')[1:15], 'fname': ['UP_HELI_MAX'], 'filename':'diag'}, + 'hmuh-neg' :{ 'levels' : [10,25,50,75,100,125,150,175,200,250,300,400,500], 'cmap': readNCLcm('prcp_1')[1:15], 'fname': ['UP_HELI_MIN'], 'filename':'diag'}, + 'hmuh03' :{ 'levels' : [10,25,50,75,100,125,150,175,200,250,300,400,500], 'cmap': readNCLcm('prcp_1')[1:15], 'fname': ['UP_HELI_MAX03'], 'filename':'diag'}, + 'rvort1' :{ 'levels' : [0.005,0.006,0.007,0.008,0.009,0.01,0.011,0.012,0.013,0.014,0.015], 'cmap': readNCLcm('prcp_1')[1:15], 'fname': ['REL_VORT_MAX01'], 'filename':'diag'}, + 'hmup' :{ 'levels' : [4,6,8,10,12,14,16,18,20,24,28,32,36,40,44,48], 'cmap': readNCLcm('prcp_1')[1:16], 'fname': ['W_UP_MAX'], 'filename':'diag' }, + #'hmdn' :{ 'levels' : [-19,-17,-15,-13,-11,-9,-7,-5,-3,-1,0], 'cmap': readNCLcm('prcp_1')[16:1:-1]+['#ffffff'], 'fname': ['W_DN_MAX'], 'filename':'diag' }, + 'hmdn' :{ 'levels' : [2,3,4,6,8,10,12,14,16,18,20,22,24,26,28,30], 'cmap': readNCLcm('prcp_1')[1:16], 'fname': ['W_DN_MAX'], 'filename':'diag' }, + 'hmwind' :{ 'levels' : [10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42], 'cmap': readNCLcm('prcp_1')[:16], 'fname': ['WSPD10MAX'], 'filename':'diag' }, + #'hmwind' :{ 'levels' : [20,25,30,35,40,45,50,55,60,65,70,75,80,85], 'cmap': readNCLcm('prcp_1')[1:16], 'fname': ['WSPD10MAX'], 'filename':'diag' }, + 'hmgrp' :{ 'levels' : [0.01,0.1,0.25,0.5,0.75,1.0,1.5,2.0,2.5,3.0,4.0,5.0], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['GRPL_MAX'], 'filename':'diag' }, +# 'cref' :{ 'levels' : [5,10,15,20,25,30,35,40,45,50,55,60,65,70], 'cmap': readcm('cmap_rad.rgb')[2:16], 'fname': ['REFL_10CM'], 'arraylevel':'max' }, + 'cref' :{ 'levels' : [5,10,15,20,25,30,35,40,45,50,55,60,65,70], 'cmap': readcm('cmap_rad.rgb')[1:14], 'fname': ['REFL_MAX_COL'], 'filename':'upp' }, + 'lmlref' :{ 'levels' : [5,10,15,20,25,30,35,40,45,50,55,60,65,70], 'cmap': readcm('cmap_rad.rgb')[1:14], 'fname': ['REFL_10CM'], 'arraylevel':0 }, + 'ref1km' :{ 'levels' : [5,10,15,20,25,30,35,40,45,50,55,60,65,70], 'cmap': readcm('cmap_rad.rgb')[1:14], 'fname': ['REFL_1KM_AGL'], 'filename':'upp' }, + 'echotop' :{ 'levels' : [1000,5000,10000,15000,20000,25000,30000,35000,40000,45000,50000,55000,60000,65000], 'cmap': readNCLcm('precip3_16lev')[1::], 'fname': ['ECHOTOP'], 'filename':'diag' }, + 'srh3' :{ 'levels' : [50,100,150,200,250,300,400,500], 'cmap': readNCLcm('perc2_9lev'), 'fname': ['SR_HELICITY_3KM'], 'filename' : 'upp' }, + 'srh1' :{ 'levels' : [50,100,150,200,250,300,400,500], 'cmap': readNCLcm('perc2_9lev'), 'fname': ['SR_HELICITY_1KM'], 'filename' : 'upp' }, + 'shr06mag' :{ 'levels' : [30,35,40,45,50,55,60,65,70,75,80], 'cmap': readNCLcm('perc2_9lev'), 'fname': ['UBSHR6', 'VBSHR6'], 'filename':'upp' }, + 'shr01mag' :{ 'levels' : [10,15,20,25,30,35,40,45,50,55], 'cmap': readNCLcm('perc2_9lev'), 'fname': ['UBSHR1', 'VBSHR1'], 'filename':'upp' }, + 'zlfc' :{ 'levels' : [0,250,500,750,1000,1250,1500,2000,2500,3000,3500,4000,5000], 'cmap': [readNCLcm('nice_gfdl')[i] for i in [3,20,37,54,72,89,106,123,141,158,175,193]], 'fname': ['AFWA_ZLFC'], 'filename':'diag' }, + 'zlcl' :{ 'levels' : [0,250,500,750,1000,1250,1500,2000,2500,3000,3500,4000,5000], 'cmap': [readNCLcm('nice_gfdl')[i] for i in [3,20,37,54,72,89,106,123,141,158,175,193]], 'fname': ['LCL_HEIGHT'], 'filename':'upp' }, + 'ltg1' :{ 'levels' : [0.1,0.5,1,1.5,2,2.5,3,4,5,6,7,8,10,12], 'cmap': readNCLcm('prcp_1')[:15], 'fname': ['LTG1_MAX'], 'filename':'diag' }, + 'ltg2' :{ 'levels' : [0.1,0.5,1,1.5,2,2.5,3,4,5,6,7,8,10,12], 'cmap': readNCLcm('prcp_1')[:15], 'fname': ['LTG2_MAX'], 'filename':'diag' }, + 'ltg3' :{ 'levels' : [0.1,0.5,1,1.5,2,2.5,3,4,5,6,7,8,10,12], 'cmap': readNCLcm('prcp_1')[:15], 'fname': ['LTG3_MAX'], 'filename':'diag' }, + 'liftidx' :{ 'levels' : [-14,-12,-10,-8,-6,-4,-2,0,2,4,6,8], 'cmap': readNCLcm('nice_gfdl')[193:3:-1]+['#ffffff'], 'fname': ['SFC_LI'], 'filename':'upp'}, + 'bmin' :{ 'levels' : [-20,-16,-12,-10,-8,-6,-4,-2,-1,-0.5,0,0.5], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['MLBMIN'], 'filename':'upp' }, + 'pbmin' :{ 'levels' : [0,30,60,90,120,150,180],'cmap': ['#dddddd', '#aaaaaa']+readNCLcm('precip2_17lev')[3:-1], 'fname': ['MLPBMIN','PBMIN_SFC'], 'filename':'upp' }, + 'goesch3' :{ 'levels' : [-80,-78,-76,-74,-72,-70,-68,-66,-64,-62,-60,-58,-56,-54,-52,-50,-48,-46,-44,-42,-40,-38,-36,-34,-32,-30,-28,-26,-24,-22,-20,-18,-16,-14,-12,-10], 'cmap': readcm('cmap_sat2.rgb')[38:1:-1], 'fname': ['GOESE_WV'], 'filename':'upp' }, + 'goesch4' :{ 'levels' : [-80,-76,-72,-68,-64,-60,-56,-52,-48,-44,-40,-36,-32,-28,-24,-20,-16,-12,-8,-4,0,4,8,12,16,20,24,28,32,36,40], 'cmap': readcm('cmap_satir.rgb')[32:1:-1], 'fname': ['GOESE_IR'], 'filename':'upp' }, + #'afwavis' :{ 'levels' : [0.0,0.1,0.25,0.5,1.0,2.0,3.0,4.0,5.0,6.0,8.0,10.0,12.0], 'cmap': readNCLcm('nice_gfdl')[3:175]+['#ffffff'], 'fname': ['AFWA_VIS'], 'filename':'diag' }, + 'afwavis' :{ 'levels' : [0.0,0.1,0.25,0.5,1.0,2.0,3.0,4.0,5.0,6.0,8.0,10.0,12.0], 'cmap': readNCLcm('nice_gfdl')[3:175]+['#ffffff'], 'fname': ['VISIBILITY'], 'filename':'upp' }, + + # winter fields + 'snow' :{ 'levels' : [0.01,0.1,0.25,0.5,0.75,1,1.5,2,2.5,3,4,5], 'cmap':['#dddddd','#aaaaaa']+[readNCLcm('precip3_16lev')[i] for i in [1,3,5,8,10,12,14,16]]+['#FF99FF'], 'fname':['AFWA_SNOWFALL_HRLY'] }, # CSS mod + 'snow-6hr' :{ 'levels' : [0.25,0.5,0.75,1,2,3,4,5,6,8,10,12], 'cmap':['#dddddd','#aaaaaa']+[readNCLcm('precip3_16lev')[i] for i in [1,3,5,8,10,12,14,16]]+['#FF99FF'], 'fname':['AFWA_SNOWFALL_HRLY'] }, # CSS mod + 'snow-12hr' :{ 'levels' : [0.5,1,2,3,6,8,10,12,14,16,18,20], 'cmap':['#dddddd','#aaaaaa']+[readNCLcm('precip3_16lev')[i] for i in [1,3,5,8,10,12,14,16]]+['#FF99FF'], 'fname':['AFWA_SNOWFALL_HRLY'] }, # CSS mod + 'snow-24hr' :{ 'levels' : [1,3,6,8,10,12,15,18,21,24,30,36], 'cmap':['#dddddd','#aaaaaa']+[readNCLcm('precip3_16lev')[i] for i in [1,3,5,8,10,12,14,16]]+['#FF99FF'], 'fname':['AFWA_SNOWFALL_HRLY'] }, # CSS mod + 'snowacc' :{ 'levels' : [0.01,0.1,0.5,1,2,3,4,5,6,8,10,12,18,24,36,48,60], 'cmap':['#dddddd','#aaaaaa']+[readNCLcm('precip3_16lev')[i] for i in [1,2,3,4,5,6,8,10,11,12,13,15,16]]+['#FF99FF'], 'fname':['AFWA_SNOWFALL'], 'filename':'diag'}, # CSS mod colortable +# 'snowliq' :{ 'levels' : [0.1,0.5,1,1.5,2,2.5,3,4,5,6], 'cmap':readNCLcm('precip3_16lev')[1:], 'fname':['AFWA_SNOW'], 'filename':'diag'}, + 'ice' :{ 'levels' : [0.01,0.05,0.1,0.15,0.2,0.25,0.3,0.4,0.5,0.75,1,1.25], 'cmap':[readNCLcm('precip3_16lev')[i] for i in [1,2,3,4,5,6,8,10,11,12,13,15,16]], 'fname':['AFWA_ICE_HRLY'], 'filename':'wrfout'}, + 'iceacc' :{ 'levels' : [0.01,0.05,0.1,0.15,0.2,0.25,0.3,0.4,0.5,0.75,1,1.25], 'cmap':[readNCLcm('precip3_16lev')[i] for i in [1,2,3,4,5,6,8,10,11,12,13,15,16]], 'fname':['AFWA_ICE'], 'filename':'diag'}, + 'fzra' :{ 'levels' : [0.01,0.05,0.1,0.15,0.2,0.25,0.3,0.4,0.5,0.75,1,1.25], 'cmap':[readNCLcm('precip3_16lev')[i] for i in [1,2,3,4,5,6,8,10,11,12,13,15,16]], 'fname':['AFWA_FZRA_HRLY'] }, # CSS added, hrly + 'fzraacc' :{ 'levels' : [0.01,0.05,0.1,0.15,0.2,0.25,0.3,0.4,0.5,0.75,1,1.25], 'cmap':[readNCLcm('precip3_16lev')[i] for i in [1,2,3,4,5,6,8,10,11,12,13,15,16]], 'fname':['AFWA_FZRA'], 'filename':'diag'}, + 'windchill' :{ 'levels' : [-40,-35,-30,-25,-20,-15,-10,-5,0,5,10,15,20,25,30,35,40,45], 'cmap':readNCLcm('GMT_ocean')[20:], 'fname':['AFWA_WCHILL'], 'filename':'diag'}, + 'freezelev' :{ 'levels' : [0, 500, 1000, 1500, 2000, 2500, 3000, 3500, 4000, 4500, 5000], 'cmap':readNCLcm('nice_gfdl')[3:193], 'fname':['FZLEV'], 'filename':'diag'}, + 'thck1000-500' :{ 'levels' : [480,486,492,498,504,510,516,522,528,534,540,546,552,558,564,570,576,582,588,592,600], 'cmap':readNCLcm('perc2_9lev'), 'fname':['GHT_PL', 'GHT_PL'], 'arraylevel':[0,5], 'filename':'diag'}, # CSS mod + 'thck1000-850' :{ 'levels' : [82,85,88,91,94,97,100,103,106,109,112,115,118,121,124,127,130,133,136,139,142,145,148,151,154,157,160], 'cmap':readNCLcm('perc2_9lev'), 'fname':['GHT_PL', 'GHT_PL'], 'arraylevel':[0,2], 'filename':'diag'}, # CSS mod + + # pressure level entries + 'hgt250' :{ 'levels' : [9700,9760,9820,9880,9940,10000,10060,10120,10180,10240,10300,10360,10420,10480,10540,10600,10660,10720,10780,10840,10900,10960,11020], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['GHT_PL'], 'filename':'diag', 'arraylevel':8 }, + 'hgt300' :{ 'levels' : [8400,8460,8520,8580,8640,8700,8760,8820,8880,8940,9000,9060,9120,9180,9240,9300,9360,9420,9480,9540,9600,9660,9720,9780,9840,9900,9960,10020], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['GHT_PL'], 'filename':'diag', 'arraylevel':7 }, + 'hgt500' :{ 'levels' : [4800,4860,4920,4980,5040,5100,5160,5220,5280,5340,5400,5460,5520,5580,5640,5700,5760,5820,5880,5940,6000], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['GHT_PL'], 'filename':'diag', 'arraylevel':5 }, + 'hgt700' :{ 'levels' : [2700,2730,2760,2790,2820,2850,2880,2910,2940,2970,3000,3030,3060,3090,3120,3150,3180,3210,3240,3270,3300], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['GHT_PL'], 'filename':'diag', 'arraylevel':3 }, + 'hgt850' :{ 'levels' : [1200,1230,1260,1290,1320,1350,1380,1410,1440,1470,1500,1530,1560,1590,1620,1650], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['GHT_PL'], 'filename':'diag', 'arraylevel':2 }, + 'hgt925' :{ 'levels' : [550,580,610,640,670,700,730,760,790,820,850,880,910,940,970,1000,1030], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['GHT_PL'], 'filename':'diag', 'arraylevel':1 }, + 'speed250' :{ 'levels' : [10,20,30,40,50,60,70,80,90,100,110,120,130,140,150,160,170], 'cmap': readNCLcm('wind_17lev'), 'fname': ['S_PL'], 'filename':'diag', 'arraylevel':8 }, + 'speed300' :{ 'levels' : [10,20,30,40,50,60,70,80,90,100,110,120,130,140,150,160,170], 'cmap': readNCLcm('wind_17lev'), 'fname': ['S_PL'], 'filename':'diag', 'arraylevel':7 }, + # RAS: adjusted these ranges - need to capture higher wind speeds + #'speed500' :{ 'levels' : [5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85], 'cmap': readNCLcm('wind_17lev'), 'fname': ['S_PL'], 'filename':'diag', 'arraylevel':5 }, + 'speed500' :{ 'levels' : [15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100], 'cmap': readNCLcm('wind_17lev'), 'fname': ['S_PL'], 'filename':'diag', 'arraylevel':5 }, + #'speed700' :{ 'levels' : [4,8,12,16,20,24,28,32,36,40,44,48,52,56,60,64,68], 'cmap': readNCLcm('wind_17lev'), 'fname': ['S_PL'], 'filename':'diag', 'arraylevel':3 }, + 'speed700' :{ 'levels' : [5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85], 'cmap': readNCLcm('wind_17lev'), 'fname': ['S_PL'], 'filename':'diag', 'arraylevel':3 }, + #'speed850' :{ 'levels' : [3,6,9,12,15,18,21,24,27,30,33,36,39,42,45,48,51], 'cmap': readNCLcm('wind_17lev'), 'fname': ['S_PL'], 'filename':'diag', 'arraylevel':2 }, + 'speed850' :{ 'levels' : [6,10,14,18,22,26,30,34,38,42,46,50,54,58,62,66,70,74], 'cmap': readNCLcm('wind_17lev'), 'fname': ['S_PL'], 'filename':'diag', 'arraylevel':2 }, + #'speed925' :{ 'levels' : [3,6,9,12,15,18,21,24,27,30,33,36,39,42,45,48,51], 'cmap': readNCLcm('wind_17lev'), 'fname': ['S_PL'], 'filename':'diag', 'arraylevel':1 }, + 'speed925' :{ 'levels' : [6,10,14,18,22,26,30,34,38,42,46,50,54,58,62,66,70,74], 'cmap': readNCLcm('wind_17lev'), 'fname': ['S_PL'], 'filename':'diag', 'arraylevel':1 }, + 'temp250' :{ 'levels' : [-65,-63,-61,-59,-57,-55,-53,-51,-49,-47,-45,-43,-41,-39,-37,-35,-33,-31,-29], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['T_PL'], 'filename':'diag', 'arraylevel':8 }, + 'temp300' :{ 'levels' : [-65,-63,-61,-59,-57,-55,-53,-51,-49,-47,-45,-43,-41,-39,-37,-35,-33,-31,-29], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['T_PL'], 'filename':'diag', 'arraylevel':7 }, + 'temp500' :{ 'levels' : [-41,-39,-37,-35,-33,-31,-29,-26,-23,-20,-17,-14,-11,-8,-5,-2], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['T_PL'], 'filename':'diag', 'arraylevel':5 }, + 'temp700' :{ 'levels' : [-36,-33,-30,-27,-24,-21,-18,-15,-12,-9,-6,-3,0,3,6,9,12,15,18,21], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['T_PL'], 'filename':'diag', 'arraylevel':3 }, + 'temp850' :{ 'levels' : [-30,-27,-24,-21,-18,-15,-12,-9,-6,-3,0,3,6,9,12,15,18,21,24,27,30], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['T_PL'], 'filename':'diag', 'arraylevel':2 }, + 'temp925' :{ 'levels' : [-24,-21,-18,-15,-12,-9,-6,-3,0,3,6,9,12,15,18,21,24,27,30,33], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['T_PL'], 'filename':'diag', 'arraylevel':1 }, + #'td300' :{ 'levels' : [-65,-60,-55,-50,-45,-40,-35,-30], 'cmap' : readNCLcm('nice_gfdl'), 'fname': ['TD_PL'], 'filename':'diag', 'arraylevel':7 }, + #'td500' :{ 'levels' : [-50,-45,-40,-35,-30,-25,-20,-15,-10], 'cmap' : readNCLcm('nice_gfdl'), 'fname': ['TD_PL'], 'filename':'diag', 'arraylevel':5 }, + 'td700' :{ 'levels' : [-30,-25,-20,-15,-10,-5,0,5,10], 'cmap' : readNCLcm('nice_gfdl')[3:193], 'fname': ['TD_PL'], 'filename':'diag', 'arraylevel':3 }, + 'td850' :{ 'levels' : [-40,-35,-30,-25,-20,-15,-10,-5,0,5,10,15,20,25,30], 'cmap' : readNCLcm('nice_gfdl')[3:193], 'fname': ['TD_PL'], 'filename':'diag', 'arraylevel':2 }, + 'td925' :{ 'levels' : [-30,-25,-20,-15,-10,-5,0,5,10,15,20,25,30], 'cmap' : readNCLcm('nice_gfdl')[3:193], 'fname': ['TD_PL'], 'filename':'diag', 'arraylevel':1 }, + 'rh300' :{ 'levels' : [0,10,20,30,40,50,60,70,80,90,100], 'cmap' : [readNCLcm('MPL_PuOr')[i] for i in (2,18,34,50)]+[readNCLcm('MPL_Greens')[j] for j in (2,17,50,75,106,125)], 'fname': ['RH_PL'], 'filename':'diag', 'arraylevel':7 }, + 'rh500' :{ 'levels' : [0,10,20,30,40,50,60,70,80,90,100], 'cmap' : [readNCLcm('MPL_PuOr')[i] for i in (2,18,34,50)]+[readNCLcm('MPL_Greens')[j] for j in (2,17,50,75,106,125)], 'fname': ['RH_PL'], 'filename':'diag', 'arraylevel':5 }, +# 'rh700' :{ 'levels' : [0,10,20,30,40,50,60,70,80,90,100], 'cmap' : [readNCLcm('MPL_PuOr')[i] for i in (2,18,34,50)]+[readNCLcm('MPL_Greens')[j] for j in (2,17,50,75,106,125)], 'fname': ['RH_PL'], 'filename':'diag', 'arraylevel':3 }, + 'rh700' :{ 'levels' : [0,10,20,30,40,50,60,70,80,90,100], 'cmap' : readNCLcm('CBR_drywet'), 'fname': ['RH_PL'], 'filename':'diag', 'arraylevel':3 }, + 'rh850' :{ 'levels' : [0,10,20,30,40,50,60,70,80,90,100], 'cmap' : readNCLcm('CBR_drywet'), 'fname': ['RH_PL'], 'filename':'diag', 'arraylevel':2 }, +# 'rh850' :{ 'levels' : [0,10,20,30,40,50,60,70,80,90,100], 'cmap' : [readNCLcm('MPL_PuOr')[i] for i in (2,18,34,50)]+[readNCLcm('MPL_Greens')[j] for j in (2,17,50,75,106,125)], 'fname': ['RH_PL'], 'filename':'diag', 'arraylevel':2 }, + 'rh925' :{ 'levels' : [0,10,20,30,40,50,60,70,80,90,100], 'cmap' : readNCLcm('CBR_drywet'), 'fname': ['RH_PL'], 'filename':'diag', 'arraylevel':1 }, +# 'rh925' :{ 'levels' : [0,10,20,30,40,50,60,70,80,90,100], 'cmap' : [readNCLcm('MPL_PuOr')[i] for i in (2,18,34,50)]+[readNCLcm('MPL_Greens')[j] for j in (2,17,50,75,106,125)], 'fname': ['RH_PL'], 'filename':'diag', 'arraylevel':1 }, + 'avo500' :{ 'levels' : [0,9,12,15,18,21,24,27,30,33], 'cmap': readNCLcm('prcp_1'), 'fname': ['AVORT_PL'], 'filename':'diag', 'arraylevel':5 }, +'pvort320k' :{ 'levels' : [0,0.1,0.2,0.3,0.4,0.5,0.75,1,1.5,2,3,4,5,7,10], + 'cmap' : ['#ffffff','#eeeeee','#dddddd','#cccccc','#bbbbbb','#d1c5b1','#e1d5b9','#f1ead3','#003399','#0033FF','#0099FF','#00CCFF','#8866FF','#9933FF','#660099'], + 'fname': ['PVORT_320K'], 'filename':'upp' }, + 'bunkmag' :{ 'levels' : [20,25,30,35,40,45,50,55,60], 'cmap':readNCLcm('wind_17lev')[1:], 'fname':['U_COMP_STM_6KM', 'V_COMP_STM_6KM'], 'filename':'upp' }, + 'speed10m' :{ 'levels' : [3,6,9,12,15,18,21,24,27,30,33,36,39,42,45,48,51], 'cmap': readNCLcm('wind_17lev')[1:],'fname' : ['U10', 'V10'], 'filename':'diag'}, + 'speed10m-tc' :{ 'levels' : [6,12,18,24,30,36,42,48,54,60,66,72,78,84,90,96,102], 'cmap': readNCLcm('wind_17lev')[1:],'fname' : ['U10', 'V10'], 'filename':'diag'}, + 'stp' :{ 'levels' : [0.5,0.75,1.0,1.5,2.0,3.0,4.0,5.0,6.0,7.0,8.0], 'cmap':readNCLcm('perc2_9lev'), 'fname':['SBCAPE','LCL_HEIGHT','SR_HELICITY_1KM','UBSHR6','VBSHR6'], 'arraylevel':[None,None,None,None,None], 'filename':'upp'}, + #'sigsvr :{ 'levels' : [1e5,2e5,3e5,4e5,5e5,6e5,8e5,10e5], 'cmap':readNCLcm('prcp_1'), 'fname':['MLCAPE','UHSHR], 'filename':'upp'} + 'ptype' :{ 'levels' : [0.01,0.1,0.25,0.5,0.75,1,1.5,2,2.5,3,3.5,4], 'cmap':['#dddddd','#aaaaaa']+readNCLcm('precip3_16lev')[1:], 'fname':['AFWA_RAIN_HRLY', 'AFWA_FZRA_HRLY', 'AFWA_ICE_HRLY', 'AFWA_SNOWFALL_HRLY'], 'filename':'wrfout'}, + 'ptypes' :{ 'levels' : [fifths,fifths,fifths,fifths], 'cmap':['green','blue','orange','red'], 'fname':['AFWA_RAIN_HRLY', 'AFWA_SNOWFALL_HRLY', 'AFWA_ICE_HRLY','AFWA_FZRA_HRLY'], 'filename':'wrfout'}, + 'ptypes-upp' :{ 'levels' : [fifths,fifths,fifths,fifths], 'cmap':['green','blue','orange','red'], 'fname':['UPP_CRAIN', 'UPP_CSNOW', 'UPP_CICEP', 'UPP_CFRZR'], 'filename':'upp'}, + 'ptypes-gsd' :{ 'levels' : [fifths,fifths,fifths,fifths], 'cmap':['green','blue','orange','red'], 'fname':[ 'CRAIN', 'CSNOW', 'CICEP', 'CFRZR'], 'filename':'upp'}, + 'winter' :{ 'levels' : [0.01,0.1,0.25,0.5,0.75,1,1.5,2,2.5,3,3.5,4], 'cmap':['#dddddd','#aaaaaa']+readNCLcm('precip3_16lev')[1:], 'fname':['AFWA_RAIN_HRLY', 'AFWA_FZRA_HRLY', 'AFWA_ICE_HRLY', 'AFWA_SNOWFALL_HRLY'], 'filename':'wrfout'}, + 'crefuh' :{ 'levels' : [5,10,15,20,25,30,35,40,45,50,55,60,65,70], 'cmap': readcm('cmap_rad.rgb')[0:13], 'fname': ['REFL_MAX_COL', 'MAX_UPDRAFT_HELICITY'], 'filename':'upp' }, + + # wind barb entries + 'wind10m' :{ 'fname' : ['U10', 'V10'], 'filename':'diag', 'skip':40 }, + 'wind250' :{ 'fname' : ['U_PL', 'V_PL'], 'filename':'diag', 'arraylevel':8, 'skip':40 }, + 'wind300' :{ 'fname' : ['U_PL', 'V_PL'], 'filename':'diag', 'arraylevel':7, 'skip':40 }, + 'wind500' :{ 'fname' : ['U_PL', 'V_PL'], 'filename':'diag', 'arraylevel':5, 'skip':40 }, + 'wind700' :{ 'fname' : ['U_PL', 'V_PL'], 'filename':'diag', 'arraylevel':3, 'skip':40 }, + 'wind850' :{ 'fname' : ['U_PL', 'V_PL'], 'filename':'diag', 'arraylevel':2, 'skip':40 }, + 'wind925' :{ 'fname' : ['U_PL', 'V_PL'], 'filename':'diag', 'arraylevel':1, 'skip':40 }, + 'shr06' :{ 'fname' : ['UBSHR6','VBSHR6'], 'filename': 'upp', 'skip':40 }, + 'shr01' :{ 'fname' : ['UBSHR1', 'VBSHR1'], 'filename': 'upp', 'skip':40 }, + 'bunkers' :{ 'fname' : ['U_COMP_STM_6KM', 'V_COMP_STM_6KM'], 'filename': 'upp', 'skip':40 }, +} + +# Combine levels from RAIN, FZRA, ICE, and SNOW for plotting 1-hr accumulated precip for each type. Ahijevych added this +#fieldinfo['ptypes']['levels'] = [fieldinfo['precip']['levels'][1:],fieldinfo['snow']['levels'],fieldinfo['ice']['levels'],fieldinfo['fzra']['levels']] + +# domains = { 'domainname': { 'corners':[ll_lat,ll_lon,ur_lat,ur_lon], 'figsize':[w,h] } } + +# These are based on the 2015-2017 NCAR ensemble +#domains = { 'CONUS' :{ 'corners': [23.1593,-120.811,46.8857,-65.0212], 'fig_width': 1080 }, +# 'SGP' :{ 'corners': [25.3,-107.00,36.00,-88.70], 'fig_width':1080 }, +# 'NGP' :{ 'corners': [40.00,-105.0,50.30,-82.00], 'fig_width':1080 }, +# 'CGP' :{ 'corners': [33.00,-107.50,45.00,-86.60], 'fig_width':1080 }, +# 'SW' :{ 'corners': [28.00,-121.50,44.39,-102.10], 'fig_width':1080 }, +# 'NW' :{ 'corners': [37.00,-124.40,51.60,-102.10], 'fig_width':1080 }, +# 'SE' :{ 'corners': [26.10,-92.75,36.00,-71.00], 'fig_width':1080 }, +# 'NE' :{ 'corners': [38.00,-91.00,46.80,-65.30], 'fig_width':1080 }, +# 'MATL':{ 'corners': [33.50,-92.25,41.50,-68.50], 'fig_width':1080 }, +#} + +# These are based on the 2018 full CONUS-HRRRE 00z runs (same as NCAR ensemble above, except for CONUS, NE, SW, NW). Get CONUS from file +#domains = { 'CONUS' :{ 'corners': [23.1593,-120.811,46.8857,-65.0212], 'fig_width': 1080 }, +# 'SGP' :{ 'corners': [25.3,-107.00,36.00,-88.70], 'fig_width':1080 }, +# 'NGP' :{ 'corners': [40.00,-105.0,50.30,-82.00], 'fig_width':1080 }, +# 'CGP' :{ 'corners': [33.00,-107.50,45.00,-86.60], 'fig_width':1080 }, +# 'SW' :{ 'corners': [28.00,-121.00,44.39,-102.10], 'fig_width':1080 }, +# 'NW' :{ 'corners': [37.00,-124.00,51.60,-102.10], 'fig_width':1080 }, +# 'SE' :{ 'corners': [26.10,-92.75,36.00,-71.00], 'fig_width':1080 }, +# 'NE' :{ 'corners': [38.00,-91.00,46.80,-65.50], 'fig_width':1080 }, +# 'MATL':{ 'corners': [33.50,-92.25,41.50,-68.50], 'fig_width':1080 }, +# 'CONUS':{'file': '/glade2/scratch2/wrfrt/realtime_ensemble/ensf_2018hwt/2018042100/wrf_rundir/ens_1/wrfinput_d02'} +#} + +# These are based on the 2018 55% CONUS-HRRRE 12z runs (get CONUS from file) +domains = { 'CONUS' :{ 'corners': [23.1593,-120.811,46.8857,-65.0212], 'fig_width': 1080 }, + 'SGP' :{ 'corners': [25.3,-107.60,36.00,-88.70], 'fig_width':1080 }, + 'NGP' :{ 'corners': [40.00,-108.0,49.80,-82.00], 'fig_width':1080 }, + 'CGP' :{ 'corners': [33.00,-108.50,45.00,-86.60], 'fig_width':1080 }, + 'SW' :{ 'corners': [28.00,-121.00,44.39,-102.10], 'fig_width':1080 }, + 'NW' :{ 'corners': [37.00,-124.00,51.60,-102.10], 'fig_width':1080 }, + 'SE' :{ 'corners': [26.10,-92.75,36.00,-71.00], 'fig_width':1080 }, + 'NE' :{ 'corners': [38.00,-91.00,46.80,-65.70], 'fig_width':1080 }, + 'MATL':{ 'corners': [33.50,-92.25,41.50,-68.50], 'fig_width':1080 }, + 'CONUS':{'file': '/glade2/scratch2/wrfrt/realtime_ensemble/ensf_2018hwt/2018042012/wrf_rundir/ens_1/wrfinput_d02'} +} + diff --git a/get_model_time.py b/get_model_time.py new file mode 100644 index 0000000..aa8ddb7 --- /dev/null +++ b/get_model_time.py @@ -0,0 +1,62 @@ +from netCDF4 import Dataset +import datetime +import pytz +import pdb +import re +import sys +import os + +def valid(ncfilename, diagnostic_name): + + nc = Dataset(ncfilename,"r") + try: + x = nc.variables[diagnostic_name] + except KeyError: + print(diagnostic_name, "not found. Choices:", list(nc.variables.keys())) + sys.exit(1) + global_atts = nc.ncattrs() + if 'valid_date' in global_atts: + valid_time = nc.valid_date + # convert unicode to datetime object + valid_time = datetime.datetime.strptime(valid_time, '%Y-%m-%d_%H:%M:%S') + elif 'initial_time' in x.ncattrs(): + initialization_time = datetime.datetime.strptime(x.initial_time, '%m/%d/%Y (%H:%M)') + if x.forecast_time_units == 'hours': + td = datetime.timedelta(0,0,0,0,0,1) + if x.forecast_time_units == '15 minutes': + td = datetime.timedelta(0,0,0,0,15,0) + if x.forecast_time_units == '30 minutes': + td = datetime.timedelta(0,0,0,0,30,0) + + forecast_lead_time = x.forecast_time * td + valid_time = initialization_time + forecast_lead_time + elif 'valid_time' in x.ncattrs(): + valid_time = datetime.datetime.strptime(x.valid_time, '%Y%m%d_%H%M%S') + elif 'START_DATE' in global_atts: + # Like ds300 NCAR WRF ensemble diags files + initialization_time = datetime.datetime.strptime(nc.START_DATE, '%Y-%m-%d_%H:%M:%S') + basename = os.path.basename(ncfilename) + # start with "diag", then anything, then _d[0-9][0-9], then _yyyymmddhh, then ... + m = re.search('diag.*_d\d\d_201\d{7}.*_f(\d\d\d).nc', basename) + if m: + fhr_str = m.group(1) + fhr = int(fhr_str) + forecast_lead_time = datetime.timedelta(hours=fhr) + valid_time = initialization_time + forecast_lead_time + else: + print("Could not get valid time from "+basename+". Using init time instead") + valid_time = initialization_time + else: + print("don't know how to get time from", ncfilename) + print(x) + pdb.set_trace() + + nc.close() + + # return time_zone_aware datetimes + return pytz.utc.localize(valid_time), pytz.utc.localize(initialization_time) + +def init(ncfilename, diagnostic_name): + valid_time, init_time = valid(ncfilename, diagnostic_name) + return init_time + diff --git a/mpas.py b/mpas.py new file mode 100644 index 0000000..f6e96b7 --- /dev/null +++ b/mpas.py @@ -0,0 +1,72 @@ +import pandas as pd +import sys +from netCDF4 import Dataset +import datetime +import numpy as np +import pdb +import re +import atcf + +def get_diag_name(valid_time, prefix='diag.', suffix='.nc'): + diag_name = prefix + valid_time.strftime("%Y-%m-%d_%H.%M.%S") + suffix + return diag_name + +def origmesh(df, initfile, diagdir, debug=False): + + # Get raw values from MPAS mesh + + # input + # df = pandas Dataframe version of atcf data + # init.nc = path to file with mesh cells lat/lon (first time this is run) + # or a dictionary containing mesh cells lat/lon (faster) + # diagdir = path to directory with diagnostics files. + + + # The first time this is called initfile is a simple string. + # Next time, it is a dictionary with all the needed variables. + if isinstance(initfile, str): + if debug: + print("reading lat/lon from", initfile) + init = Dataset(initfile,"r") + lonCellrad = init.variables['lonCell'][:] + latCellrad = init.variables['latCell'][:] + lonCell = np.degrees(lonCellrad) + latCell = np.degrees(latCellrad) + lonCell[lonCell >= 180] = lonCell[lonCell >=180] - 360. + nEdgesOnCell = init.variables['nEdgesOnCell'][:] + cellsOnCell = init.variables['cellsOnCell'][:] + init.close() + initfile = { + "initfile":initfile, + "lonCell":lonCell, + "latCell":latCell, + "nEdgesOnCell":nEdgesOnCell, + "cellsOnCell":cellsOnCell + } + else: + if debug: + print("reading lat/lon from dictionary") + lonCell = initfile["lonCell"] + latCell = initfile["latCell"] + nEdgesOnCell = initfile["nEdgesOnCell"] + cellsOnCell = initfile["cellsOnCell"] + + itime = 0 + for index, row in df.iterrows(): + diagfile = get_diag_name(row.valid_time, prefix='diag.', suffix='.nc') + + if debug: print("reading diagfile", diagdir+diagfile) + nc = Dataset(diagdir+diagfile, "r") + + u10 = nc.variables['u10'][itime,:] + v10 = nc.variables['v10'][itime,:] + mslp = nc.variables['mslp'][itime,:] + nc.close() + + # Extract vmax, RMW, minp, and radii of wind thresholds + raw_vmax_kts, raw_RMW_nm, raw_minp, rad_nm = atcf.derived_winds(u10, v10, mslp, lonCell, latCell, row, debug=debug) + + df = atcf.update_df(df, row, raw_vmax_kts, raw_RMW_nm, raw_minp, rad_nm, debug=debug) + return df, initfile + + diff --git a/mysavfig.py b/mysavfig.py new file mode 100644 index 0000000..ba95e13 --- /dev/null +++ b/mysavfig.py @@ -0,0 +1,16 @@ +import matplotlib.pyplot as plt +from subprocess import call # to use "mogrify" +import datetime as dt +def mysavfig(ofile, string="", timestamp=True, size=5, **kwargs): + if timestamp: + string = string + '\n' + 'created '+str(dt.datetime.now(tz=None)).split('.')[0] # + '\n' # extra newline to keep timestamp onscreen. + th = plt.annotate(string, xy=(2,1), xycoords='figure pixels', horizontalalignment='left', verticalalignment='bottom', size=size) + #plt.tight_layout() # Tried this but it screwed up SHARPY_skewt. Also tried bbox_inches='tight' below but not in that version of matplotlib. + # Tried this but it made a large blank space on the left side of SHARPY_skewt. + # plt.savefig(ofile,dpi=dpi,bbox_inches='tight') + plt.savefig(ofile, **kwargs) + th.remove() # permits the figure to reused without overlaying multiple "created . . " dates. + cmd = "mogrify +matte -type Palette -colors 255 " + ofile # prevents flickering when displaying on yellowstone. + print("created", ofile) + return call(cmd.split()) + diff --git a/myskewt.py b/myskewt.py new file mode 100755 index 0000000..d41ea8b --- /dev/null +++ b/myskewt.py @@ -0,0 +1,294 @@ +import sys +import pdb +sys.path.append('/glade/u/home/ahijevyc/lib/python2.7/site-packages/SHARPpy-1.3.0-py2.7.egg') +sys.path.append('/glade/u/home/ahijevyc/lib/python2.7/site-packages') +import sharppy +import sharppy.sharptab.interp as interp +import sharppy.sharptab.params as params +import sharppy.sharptab.profile as profile +import sharppy.sharptab.thermo as thermo +import sharppy.sharptab.utils as utils +import sharppy.sharptab.winds as winds + +import numpy as np +from StringIO import StringIO +import matplotlib.pyplot as plt +import cartopy + + +def parseCLS(sfile): + ## read in the file + data = np.array([l.strip() for l in sfile.split('\n')]) + + latitude = float(data[3].split(',')[5]) + longitude = float(data[3].split(',')[4]) + ## necessary index points + start_idx = 15 + finish_idx = len(data) + + ## put it all together for StringIO + full_data = '\n'.join(data[start_idx : finish_idx][:]) + sound_data = StringIO( full_data ) + + ## read the data into arrays + data = np.genfromtxt( sound_data ) + clean_data = [] + for i in data: + if i[1] != 9999 and i[2] != 999 and i[3] != 999 and i[7] != 999 and i[8] != 999 and i[14] != 99999: + clean_data.append(i) + p = np.array([i[1] for i in clean_data]) + h = np.array([i[14] for i in clean_data]) + T = np.array([i[2] for i in clean_data]) + Td = np.array([i[3] for i in clean_data]) + wdir = np.array([i[8] for i in clean_data]) + wspd = np.array([i[7] for i in clean_data]) + wspd = utils.MS2KTS(wspd) + wdir[wdir == 360] = 0. # Some wind directions are 360. Like in /glade/p/work/ahijevyc/GFS/Joaquin/g132325165.frd + + max_points = 250 + s = p.size/max_points + if s == 0: s = 1 + print "stride=",s + return p[::s], h[::s], T[::s], Td[::s], wdir[::s], wspd[::s], latitude, longitude + +def thetas(tempC, presvals): + return ((tempC + thermo.ZEROCNK) / (np.power((1000. / presvals),thermo.ROCP))) - thermo.ZEROCNK + + + +def dewpoint_approximation(T,RH): + # approximation valid for + # 0 degC < T < 60 degC + # 1% < RH < 100% + # 0 degC < Td < 50 degC + # constants + a = 17.271 + b = 237.7 # degC + + Td = (b * gamma(T,RH)) / (a - gamma(T,RH)) + + return Td + + +def gamma(T,RH): + # constants + a = 17.271 + b = 237.7 # degC + + g = (a * T / (b + T)) + np.log(RH/100.0) + + return g + +def draw_background(ax, dry=np.arange(-50,110,20), moist=np.arange(-5,40,5), presvals=np.arange(1050, 0, -10), mix=[1,2,4,8,12,16,20,24]): + + # Plot dry adiabats + for t in dry: + ax.semilogy(thetas(t, presvals), presvals,'r-', alpha=0.2, linewidth=1) + + # Plot moist adiabats to topp + topp = 200 + moist_presvals = np.arange(np.max(presvals), topp, -10) + for t in moist: + tw = [] + for p in moist_presvals: + tw.append(thermo.wetlift(1000., t, p)) + ax.semilogy(tw, moist_presvals, 'k-', alpha=0.2, linewidth=0.5, linestyle='dashed') + # add moist adiabat text labels + ax.text(thermo.wetlift(1000., t, topp+15), topp+15, t, va='center', ha='center', size=5.6, alpha=0.3, clip_on=True) + + + # Mixing ratio lines and labels to topp + + topp = 650 + for w in mix: + ww = [] + for p in presvals: + ww.append(thermo.temp_at_mixrat(w,p)) + ax.semilogy(ww, presvals, 'g', alpha=0.35, linewidth=1, linestyle="dotted") + ax.text(thermo.temp_at_mixrat(w, topp), topp, w, va='bottom', ha='center', size=6.7, color='g', alpha=0.35) + + # Disables the log-formatting (10 to the power of x) that comes with semilogy + ax.yaxis.set_major_formatter(plt.ScalarFormatter()) + ax.set_yticks(np.linspace(100,1000,10)) + ax.set_ylim(1050,100) + plt.ylabel('Pressure (hPa)') + + # The first time this axis object is returned, no xtick gridlines show up for -110 to -60C + ax.xaxis.set_major_locator(plt.MultipleLocator(10)) + ax.set_xlim(-50,45) + plt.xlabel('Temperature (C)') + + ax.grid(True, linestyle='solid', alpha=0.5) + + +def draw_hodo(): + bbox_props = dict(boxstyle="square", color="w", alpha=0.5, lw=0.5) + ax = plt.axes([.5,.63,.2,.26]) + ax.get_xaxis().set_visible(False) + ax.get_yaxis().set_visible(False) + az = np.radians(-35) + for i in range(10,100,10): + # Draw the range rings around the hodograph. + lw = .4 if i % 20 == 0 else 0.25 + circle = plt.Circle((0,0),i,color='k', alpha=.3, fill=False, lw=lw) + ax.add_artist(circle) + if i % 20 == 0 and i < 100: + # The minus 2 nudges it a little closer to origin. + plt.text((i-2)*np.cos(az),(i-2)*np.sin(az),str(i)+" kt",rotation=np.degrees(az),size=5,alpha=.4, + ha='center', zorder=1, bbox=bbox_props) + + ax.set_xlim(-40,80) + ax.set_ylim(-60,60) + ax.axhline(y=0, color='k') + ax.axvline(x=0, color='k') + + return ax + + +def add_hodo(ax, prof, lw=1, color='black', ls='solid', size=5, AGLs=[1,2,6,10]): + # Return 2D hodograph line and list of text AGL labels. + + + # Draw hodo line + u_prof = prof.u[prof.pres >= 100] + v_prof = prof.v[prof.pres >= 100] + hodo, = ax.plot(u_prof,v_prof, 'k-', lw=lw, color=color, ls=ls) + + # position AGL text labels + bbox_props = dict(boxstyle="square", fc="w", ec="0.5", linewidth=0.5, alpha=0.7) + AGL_labels = [] + AGL_labels.append(ax.text(-35,-55,'km AGL',size=size,bbox=bbox_props)) + for a in AGLs: + in_meters = a*1000 + if in_meters <= np.max(interp.to_agl(prof, prof.hght)): + junk = ax.text(0,0,str(a),ha='center',va='center',size=size,bbox=bbox_props,color=color) + ind = np.min(np.where(interp.to_agl(prof,prof.hght)>in_meters)) + junk.set_position((prof.u[ind],prof.v[ind])) + junk.set_clip_on(True) + AGL_labels.append(junk) + return hodo, AGL_labels + + +def wind_barb_spaced(ax, prof, xpos=1.0, yspace=0.04): + # yspace = 0.04 means ~26 barbs. + s = [] + bot = 2000. + # Space out wind barbs evenly on log axis. + for ind, i in enumerate(prof.pres): + if i < 100: break + if np.log(bot/i) > yspace: + s.append(ind) + bot = i + # x coordinate in (0-1); y coordinate in pressure log(p) + b = plt.barbs(xpos*np.ones(len(prof.pres[s])), prof.pres[s], prof.u[s], prof.v[s], + length=5.8, lw=0.35, clip_on=False, transform=ax.get_yaxis_transform()) + + # 'knots' label under wind barb stack + kts = ax.text(1.0, prof.pres[0]+10, 'knots', clip_on=False, transform=ax.get_yaxis_transform(),ha='center',va='top',size=7) + return b, kts + + +def add_globe(longitude, latitude): + # Globe with dot on location. + mapax = plt.axes([.795, 0.09,.18,.18], projection=cartopy.crs.Orthographic(longitude, latitude)) + mapax.add_feature(cartopy.feature.OCEAN, zorder=0) + mapax.add_feature(cartopy.feature.LAND, zorder=0) + mapax.set_global() + sloc = mapax.plot(longitude, latitude,'o', color='green', markeredgewidth=0, markersize=4., transform=cartopy.crs.Geodetic()) + return mapax + + +def indices(prof): + + # return a formatted-string list of stability and kinematic indices + + sfcpcl = params.parcelx(prof, flag=1) + mupcl = params.parcelx(prof, flag=3) # most unstable + mlpcl = params.parcelx(prof, flag=4) # 100 mb mean layer parcel + + + pcl = mupcl + sfc = prof.pres[prof.sfc] + p3km = interp.pres(prof, interp.to_msl(prof, 3000.)) + p6km = interp.pres(prof, interp.to_msl(prof, 6000.)) + p1km = interp.pres(prof, interp.to_msl(prof, 1000.)) + mean_3km = winds.mean_wind(prof, pbot=sfc, ptop=p3km) + sfc_6km_shear = winds.wind_shear(prof, pbot=sfc, ptop=p6km) + sfc_3km_shear = winds.wind_shear(prof, pbot=sfc, ptop=p3km) + sfc_1km_shear = winds.wind_shear(prof, pbot=sfc, ptop=p1km) + #print "0-3 km Pressure-Weighted Mean Wind (kt):", utils.comp2vec(mean_3km[0], mean_3km[1])[1] + #print "0-6 km Shear (kt):", utils.comp2vec(sfc_6km_shear[0], sfc_6km_shear[1])[1] + srwind = params.bunkers_storm_motion(prof) + srh3km = winds.helicity(prof, 0, 3000., stu = srwind[0], stv = srwind[1]) + srh1km = winds.helicity(prof, 0, 1000., stu = srwind[0], stv = srwind[1]) + #print "0-3 km Storm Relative Helicity [m2/s2]:",srh3km[0] + + #### Calculating variables based off of the effective inflow layer: + + # The effective inflow layer concept is used to obtain the layer of buoyant parcels that feed a storm's inflow. + # Here are a few examples of how to compute variables that require the effective inflow layer in order to calculate them: + + stp_fixed = params.stp_fixed(sfcpcl.bplus, sfcpcl.lclhght, srh1km[0], utils.comp2vec(sfc_6km_shear[0], sfc_6km_shear[1])[1]) + ship = params.ship(prof) + + # If you get an error about not converting masked constant to python int + # use the round() function instead of int() - Ahijevych May 11 2016 + # 2nd element of list is the # of decimal places + indices = {'SBCAPE': [sfcpcl.bplus, 0, 'J $\mathregular{kg^{-1}}$'], + 'SBCIN': [sfcpcl.bminus, 0, 'J $\mathregular{kg^{-1}}$'], + 'SBLCL': [sfcpcl.lclhght, 0, 'm AGL'], + 'SBLFC': [sfcpcl.lfchght, 0, 'm AGL'], + 'SBEL': [sfcpcl.elhght, 0, 'm AGL'], + 'SBLI': [sfcpcl.li5, 0, 'C'], + 'MLCAPE': [mlpcl.bplus, 0, 'J $\mathregular{kg^{-1}}$'], + 'MLCIN': [mlpcl.bminus, 0, 'J $\mathregular{kg^{-1}}$'], + 'MLLCL': [mlpcl.lclhght, 0, 'm AGL'], + 'MLLFC': [mlpcl.lfchght, 0, 'm AGL'], + 'MLEL': [mlpcl.elhght, 0, 'm AGL'], + 'MLLI': [mlpcl.li5, 0, 'C'], + 'MUCAPE': [mupcl.bplus, 0, 'J $\mathregular{kg^{-1}}$'], + 'MUCIN': [mupcl.bminus, 0, 'J $\mathregular{kg^{-1}}$'], + 'MULCL': [mupcl.lclhght, 0, 'm AGL'], + 'MULFC': [mupcl.lfchght, 0, 'm AGL'], + 'MUEL': [mupcl.elhght, 0, 'm AGL'], + 'MULI': [mupcl.li5, 0, 'C'], + '0-1 km SRH': [srh1km[0], 0, '$\mathregular{m^{2}s^{-2}}$'], + '0-1 km Shear': [utils.comp2vec(sfc_1km_shear[0], sfc_1km_shear[1])[1], 0, 'kt'], + '0-3 km SRH': [srh3km[0], 0, '$\mathregular{m^{2}s^{-2}}$'], + '0-6 km Shear': [utils.comp2vec(sfc_6km_shear[0], sfc_6km_shear[1])[1], 0, 'kt'], + 'PWV': [params.precip_water(prof), 2, 'inch'], + 'K-index': [params.k_index(prof), 0, ''], + 'STP(fix)': [stp_fixed, 1, ''], + 'SHIP': [ship, 1, '']} + + + + eff_inflow = params.effective_inflow_layer(prof) + if any(eff_inflow): + ebot_hght = interp.to_agl(prof, interp.hght(prof, eff_inflow[0])) + etop_hght = interp.to_agl(prof, interp.hght(prof, eff_inflow[1])) + #print "Effective Inflow Layer Bottom Height (m AGL):", ebot_hght + #print "Effective Inflow Layer Top Height (m AGL):", etop_hght + effective_srh = winds.helicity(prof, ebot_hght, etop_hght, stu = srwind[0], stv = srwind[1]) + indices['Eff. SRH'] = [effective_srh[0], 0, '$\mathregular{m^{2}s^{-2}}$'] + #print "Effective Inflow Layer SRH (m2/s2):", effective_srh[0] + ebwd = winds.wind_shear(prof, pbot=eff_inflow[0], ptop=eff_inflow[1]) + ebwspd = utils.mag( *ebwd ) + indices['EBWD'] = [ebwspd,0, 'kt'] + #print "Effective Bulk Wind Difference:", ebwspd + scp = params.scp(mupcl.bplus, effective_srh[0], ebwspd) + indices['SCP'] = [scp, 1, ''] + stp_cin = params.stp_cin(mlpcl.bplus, effective_srh[0], ebwspd, mlpcl.lclhght, mlpcl.bminus) + indices['STP(cin)'] = [stp_cin, 1, ''] + #print "Supercell Composite Parameter:", scp + #print "Significant Tornado Parameter (w/CIN):", stp_cin + #print "Significant Tornado Parameter (fixed):", stp_fixed + + # Update the indices within the indices dictionary on the side of the plot. + string = '' + for index, value in sorted(indices.iteritems()): + format = '%.'+str(value[1])+'f' + string += index + ": " + format % value[0] + " " + value[2] + '\n' + + return string + diff --git a/spc.py b/spc.py new file mode 100644 index 0000000..4d08c27 --- /dev/null +++ b/spc.py @@ -0,0 +1,249 @@ +import pandas as pd +import pytz +import glob +import sys # for stderr output +import pdb +import numpy as np +import datetime +import matplotlib.path as mpath +import cartopy + +def spc_event_filename(event_type): + name = '/glade/work/ahijevyc/share/SPC/'+event_type+'/' + if event_type=='torn': + return "/glade/work/ahijevyc/share/SPC/1950-2017_actual_tornadoes.csv" + name = name + "1955-2017_"+event_type+".csv" + return name + + + +def get_storm_reports( + start = datetime.datetime(2016,6,10,0,0,0,0,pytz.UTC), + end = datetime.datetime(2016,7,1,0,0,0,0,pytz.UTC), + event_types = ["torn", "wind", "hail"], + debug = False + ): + + if debug: + print("storm_reports: start:",start) + print("storm_reports: end:",end) + print("storm_reports: event types:",event_types) + + # Create one DataFrame to hold all event types. + all_rpts = pd.DataFrame() + for event_type in event_types: + rpts_file = spc_event_filename(event_type) + if debug: + print("input file:",rpts_file) + pdb.set_trace() + + # csv format described in http://www.spc.noaa.gov/wcm/data/SPC_severe_database_description.pdf + # SPC storm report files downloaded from http://www.spc.noaa.gov/wcm/#data to + # cheyenne:/glade/work/ahijevyc/share/ Mar 2019. + # Multi-year zip files have headers; pre-2016 single-year csv files have no header. + + dtype = { + "om": np.int64, + "yr": np.int32, + "mo": np.int32, + "dy": np.int32, + "date": str, + "tz": np.int32, + "st": str, + "stf": np.int64, + "stn": np.int64, + "mag": np.float64, # you might think there is "sz" for hail and "f" for torn, but all "mag" + "inj": np.int64, + "fat": np.int64, + "loss": np.float64, + "closs": np.float64, + "slat": np.float64, + "slon": np.float64, + "elat": np.float64, + "elon": np.float64, + "len": np.float64, + "wid": np.float64, + "ns": np.int64, + "sn": np.int64, + "sg": np.int64, + "f1": np.int64, + "f2": np.int64, + "f3": np.int64, + "f4": np.int64, + "mt": str, + "fc": np.int64, + } + rpts = pd.read_csv(rpts_file, parse_dates=[['date','time']], dtype=dtype, infer_datetime_format=True) + rpts["event_type"] = event_type + + # Augment event type for large hail and high wind. + if event_type == "hail": + largehail = rpts.mag >= 2. + if any(largehail): + rpts.loc[largehail,"event_type"] = "large hail" + if event_type == "wind": + highwind = rpts.mag >= 65. + if any(highwind): + rpts.loc[highwind, "event_type"] = "high wind" + + # tz=3 is CST + # tz=9 is GMT + # To convert to UTC, add 9 and subtract tz hours. + + rpts["time"] = rpts['date_time'] + pd.to_timedelta(9 - rpts.tz, unit='h') + # make time-zone aware datetime object + rpts["time"] = rpts["time"].dt.tz_localize(pytz.UTC) + + if any(rpts['tz'] != 3): + print(rpts_file, file=sys.stderr) + unexpected_timezones = rpts[['om','date_time','tz']][rpts['tz'] != 3] + print("get_storm_reports: found",len(unexpected_timezones),"unexpected timezones") + if debug: + print(unexpected_timezones, file=sys.stderr) + print("WARNING - proceeding with program. Wrote to SPC Mar 22 2017 about fixing these lines", file=sys.stderr) + + time_window = (rpts.time >= start) & (rpts.time < end) + rpts = rpts[time_window] + if debug: + print("found",len(rpts),event_type,"reports") + all_rpts = all_rpts.append(rpts, ignore_index=True, sort=False) + + + return all_rpts + +def plot(storm_reports, ax, scale=1, drawradius=0, alpha=0.4, debug=False): + + if storm_reports.empty: + # is this the right thing to return? what about empty list []? or rpts? + if debug: + print("spc.plot(): storm reports DataFrame is empty. Returning") + return + + # Color, size, marker, and label of wind, hail, and tornado storm reports + kwdict = { + "wind": {"c" : 'blue', "s": 8*scale, "marker":"s", "label":"Wind Report"}, + "high wind": {"c" : 'black', "s":12*scale, "marker":"s", "label":"Wind Report/HI"}, + "hail": {"c" : 'green', "s":12*scale, "marker":"^", "label":"Hail Report"}, + "large hail": {"c" : 'black', "s":16*scale, "marker":"^", "label":"Hail Report/LG"}, + "torn": {"c" : 'red', "s":12*scale, "marker":"v", "label":"Torn Report"} + } + + storm_rpts_plots = [] + + for event_type in ["wind", "high wind", "hail", "large hail", "torn"]: + if debug: + print("looking for "+event_type) + xrpts = storm_reports[storm_reports.event_type == event_type] + print("plot",len(xrpts),event_type,"reports") + if len(xrpts) == 0: + continue + lons, lats = xrpts.slon.values, xrpts.slat.values + storm_rpts_plot = ax.scatter(lons, lats, alpha = alpha, edgecolors="None", **kwdict[event_type], + transform=cartopy.crs.Geodetic()) + storm_rpts_plots.append(storm_rpts_plot) + if debug: + pdb.set_trace() + if drawradius > 0: + # With lons and lats, specifying more than one dimension allows individual points to be drawn. + # Otherwise a grid of circles will be drawn. + # It warns about using PlateCarree to approximate Geodetic. It still warps the circles + # appropriately, so I think this is okay. + within_radius = ax.tissot(rad_km = drawradius, lons=lons[np.newaxis], lats=lats[np.newaxis], + facecolor=kwdict[event_type]["c"], alpha=0.4, label=str(drawradius)+" km radius") + # TODO: Legend does not support tissot cartopy.mpl.feature_artist. + # A proxy artist may be used instead. + # matplotlib.org/users/legend_guide.html# + # creating-artists-specifically-for-adding-to-the-legend-aka-proxy-artists + # storm_rpts_plots.append(within_radius) + + return storm_rpts_plots + + + +def to_MET(df, gribcode=187): + # gribcode 187 :lightning + # INPUT: storm_reports DataFrame from spc.get_storm_reports() + # Output: MET point observation format, one observation per line. + # Use gribcode if specified. Otherwise lightning by default (187). + # + # Each observation line will consist of the following 11 columns of data: + met_columns = ["Message_Type", "Station_ID", "Valid_Time", "Lat", "Lon", "Elevation", "Grib_Code", "Level", "Height", "QC_String", "Observation_Value"] + + df["Message_Type"] = "ADPSFC" + df["Station_ID"] = df.stn + df["Valid_Time"] = df.time.dt.strftime('%Y%m%d_%H%M%S') #df.time should be aware of UTC time zone + df["Lat"] = (df.slat+df.elat)/2 + df["Lon"] = (df.slon+df.elon)/2 + df["Elevation"] = "NA" + df["Grib_Code"] = gribcode # 187 :lightning + df["Level"] = "NA" + df["Height"] = "NA" + df["QC_String"] = "NA" + df["Observation_Value"] = 1 + # index=False don't write index number + return df.to_string(columns=met_columns,index=False,header=False) + + + +# NCDC storm event details don't have a lat and lon. Just a city, a range, and an azimuth. +# Perhaps the lat and lon are in the storm event location files (as opposed to "details"). +# Maybe just go with SPC storm reports. + +def stormEvents( + idir="/glade/work/ahijevyc/share/stormevents", + start = datetime.datetime(2016,6,1), + end = datetime.datetime(2016,7,1), + event_type = "Tornado", + debug = False + ): + # INPUT: idir + # Directory with input files. For example: StormEvents_details-ftp_v1.0_d2015_c20180525.csv + # Downloaded from NCDC storm events database at https://www.ncdc.noaa.gov/stormevents/ftp.jsp + + ifiles = glob.glob(idir+"/StormEvents_details-*csv*") + year = start.year + ifile = [s for s in ifiles if "_d"+str(year)+"_c" in s] + if len(ifile) != 1: + print("Did not find one storm events file", ifile) + pdb.set_trace() + + ifile = ifile[0] + if debug: + pdb.set_trace() + + events = pd.read_csv(ifile) + # This is ugly + events["BEGIN"] = pd.to_datetime(events.BEGIN_DATE_TIME, format="%d-%b-%y %H:%M:%S") + events["END"] = pd.to_datetime(events.END_DATE_TIME, format="%d-%b-%y %H:%M:%S") + # Just return a particular event type. 'Thunderstorm Wind' or 'Tornado' or 'Hail', for example. + if event_type is not None: + events = events[events.EVENT_TYPE == event_type] + + if start is not None: + events = events[events.BEGIN >= start] + + if end is not None: + events = events[events.END < end] + + return events + +def events2met(df): + # Point Observation Format + # input ASCII MET point observation format contains one observation per line. + # Each input observation line should consist of the following 11 columns of data: + met_columns = ["Message_Type", "Station_ID", "Valid_Time", "Lat", "Lon", "Elevation", "Grib_Code", "Level", "Height", "QC_String", "Observation_Value"] + + df["Message_Type"] = "ADPSFC" + df["Station_ID"] = df.WFO + df["Valid_Time"] = df.BEGIN.dt.strftime('%Y%m%d_%H%M%S') + df["Lat"] = df.lat # I don't know how to get this. + df["Lon"] = df.lon + df["Elevation"] = "NA" + df["Grib_Code"] = "NA" + df["Level"] = "NA" + df["Height"] = "NA" + df["QC_String"] = "NA" + df["Observation_Value"] = df.TOR_F_SCALE + print(df.to_string(columns=met_columns)) + + diff --git a/t.py b/t.py new file mode 100644 index 0000000..e1a20c2 --- /dev/null +++ b/t.py @@ -0,0 +1,237 @@ +# Copyright (c) 2014,2015,2016,2017 MetPy Developers. +# Distributed under the terms of the BSD 3-Clause License. +# SPDX-License-Identifier: BSD-3-Clause +"""Make Skew-T Log-P based plots. + +Contain tools for making Skew-T Log-P plots, including the base plotting class, +`SkewT`, as well as a class for making a `Hodograph`. +""" + +import matplotlib +from matplotlib.axes import Axes +import matplotlib.axis as maxis +from matplotlib.collections import LineCollection +import matplotlib.colors as mcolors +from matplotlib.patches import Circle +from matplotlib.projections import register_projection +import matplotlib.spines as mspines +from matplotlib.ticker import MultipleLocator, NullFormatter, ScalarFormatter +import matplotlib.transforms as transforms +import numpy as np + +from ._util import colored_line +from ..calc import dewpoint, dry_lapse, moist_lapse, vapor_pressure +from ..calc.tools import _delete_masked_points +from ..interpolate import interpolate_1d +from ..package_tools import Exporter +from ..units import concatenate, units + +exporter = Exporter(globals()) + + +class SkewXTick(maxis.XTick): + r"""Make x-axis ticks for Skew-T plots. + + This class adds to the standard :class:`matplotlib.axis.XTick` dynamic checking + for whether a top or bottom tick is actually within the data limits at that part + and draw as appropriate. It also performs similar checking for gridlines. + """ + + def update_position(self, loc): + """Set the location of tick in data coords with scalar *loc*.""" + # This ensures that the new value of the location is set before + # any other updates take place. + self._loc = loc + super(SkewXTick, self).update_position(loc) + + def _has_default_loc(self): + return self.get_loc() is None + + def _need_lower(self): + return (self._has_default_loc() or + transforms.interval_contains(self.axes.lower_xlim, + self.get_loc())) + + def _need_upper(self): + return (self._has_default_loc() or + transforms.interval_contains(self.axes.upper_xlim, + self.get_loc())) + + @property + def gridOn(self): # noqa: N802 + """Control whether the gridline is drawn for this tick.""" + return (self._gridOn and (self._has_default_loc() or + transforms.interval_contains(self.get_view_interval(), + self.get_loc()))) + + @gridOn.setter + def gridOn(self, value): # noqa: N802 + self._gridOn = value + + @property + def tick1On(self): # noqa: N802 + """Control whether the lower tick mark is drawn for this tick.""" + return self._tick1On and self._need_lower() + + @tick1On.setter + def tick1On(self, value): # noqa: N802 + self._tick1On = value + + @property + def label1On(self): # noqa: N802 + """Control whether the lower tick label is drawn for this tick.""" + return self._label1On and self._need_lower() + + @label1On.setter + def label1On(self, value): # noqa: N802 + self._label1On = value + + @property + def tick2On(self): # noqa: N802 + """Control whether the upper tick mark is drawn for this tick.""" + return self._tick2On and self._need_upper() + + @tick2On.setter + def tick2On(self, value): # noqa: N802 + self._tick2On = value + + @property + def label2On(self): # noqa: N802 + """Control whether the upper tick label is drawn for this tick.""" + return self._label2On and self._need_upper() + + @label2On.setter + def label2On(self, value): # noqa: N802 + self._label2On = value + + def get_view_interval(self): + """Get the view interval.""" + return self.axes.xaxis.get_view_interval() + + +class SkewXAxis(maxis.XAxis): + r"""Make an x-axis that works properly for Skew-T plots. + + This class exists to force the use of our custom :class:`SkewXTick` as well + as provide a custom value for interview that combines the extents of the + upper and lower x-limits from the axes. + """ + + def _get_tick(self, major): + return SkewXTick(self.axes, None, '', major=major) + + def get_view_interval(self): + """Get the view interval.""" + return self.axes.upper_xlim[0], self.axes.lower_xlim[1] + + +class SkewSpine(mspines.Spine): + r"""Make an x-axis spine that works properly for Skew-T plots. + + This class exists to use the separate x-limits from the axes to properly + locate the spine. + """ + + def _adjust_location(self): + pts = self._path.vertices + if self.spine_type == 'top': + pts[:, 0] = self.axes.upper_xlim + else: + pts[:, 0] = self.axes.lower_xlim + + +class SkewXAxes(Axes): + r"""Make a set of axes for Skew-T plots. + + This class handles registration of the skew-xaxes as a projection as well as setting up + the appropriate transformations. It also makes sure we use our instances for spines + and x-axis: :class:`SkewSpine` and :class:`SkewXAxis`. It provides properties to + facilitate finding the x-limits for the bottom and top of the plot as well. + """ + + # The projection must specify a name. This will be used be the + # user to select the projection, i.e. ``subplot(111, + # projection='skewx')``. + name = 'skewx' + + def __init__(self, *args, **kwargs): + r"""Initialize `SkewXAxes`. + + Parameters + ---------- + args : Arbitrary positional arguments + Passed to :class:`matplotlib.axes.Axes` + + position: int, optional + The rotation of the x-axis against the y-axis, in degrees. + + kwargs : Arbitrary keyword arguments + Passed to :class:`matplotlib.axes.Axes` + + """ + # This needs to be popped and set before moving on + self.rot = kwargs.pop('rotation', 30) + Axes.__init__(self, *args, **kwargs) + + def _init_axis(self): + # Taken from Axes and modified to use our modified X-axis + self.xaxis = SkewXAxis(self) + self.spines['top'].register_axis(self.xaxis) + self.spines['bottom'].register_axis(self.xaxis) + self.yaxis = maxis.YAxis(self) + self.spines['left'].register_axis(self.yaxis) + self.spines['right'].register_axis(self.yaxis) + + def _gen_axes_spines(self, locations=None, offset=0.0, units='inches'): + # pylint: disable=unused-argument + spines = {'top': SkewSpine.linear_spine(self, 'top'), + 'bottom': mspines.Spine.linear_spine(self, 'bottom'), + 'left': mspines.Spine.linear_spine(self, 'left'), + 'right': mspines.Spine.linear_spine(self, 'right')} + return spines + + def _set_lim_and_transforms(self): + """Set limits and transforms. + + This is called once when the plot is created to set up all the + transforms for the data, text and grids. + + """ + # Get the standard transform setup from the Axes base class + Axes._set_lim_and_transforms(self) + + # Need to put the skew in the middle, after the scale and limits, + # but before the transAxes. This way, the skew is done in Axes + # coordinates thus performing the transform around the proper origin + # We keep the pre-transAxes transform around for other users, like the + # spines for finding bounds + self.transDataToAxes = (self.transScale + + (self.transLimits + + transforms.Affine2D().skew_deg(self.rot, 0))) + + # Create the full transform from Data to Pixels + self.transData = self.transDataToAxes + self.transAxes + + # Blended transforms like this need to have the skewing applied using + # both axes, in axes coords like before. + self._xaxis_transform = (transforms.blended_transform_factory( + self.transScale + self.transLimits, + transforms.IdentityTransform()) + + transforms.Affine2D().skew_deg(self.rot, 0)) + self.transAxes + + @property + def lower_xlim(self): + """Get the data limits for the x-axis along the bottom of the axes.""" + return self.axes.viewLim.intervalx + + @property + def upper_xlim(self): + """Get the data limits for the x-axis along the top of the axes.""" + return self.transDataToAxes.inverted().transform([[0., 1.], [1., 1.]])[:, 0] + + +# Now register the projection with matplotlib so the user can select +# it. +register_projection(SkewXAxes) + + From 78843ac35a4a8ae34329617be6873ff452bd234b Mon Sep 17 00:00:00 2001 From: ahijevyc Date: Wed, 20 Mar 2019 10:08:00 -0600 Subject: [PATCH 09/68] add python3 print parentheses --- make_webplot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/make_webplot.py b/make_webplot.py index 8047ccc..4e69460 100755 --- a/make_webplot.py +++ b/make_webplot.py @@ -3,7 +3,7 @@ import sys, time, os from webplot import webPlot, readGrid, saveNewMap -def log(msg): print time.ctime(time.time()),':', msg +def log(msg): print(time.ctime(time.time()),':', msg) log('Begin Script') stime = time.time() From fb5d0cf0f84031f74ed8658d504f9d46e5140c54 Mon Sep 17 00:00:00 2001 From: ahijevyc Date: Thu, 21 Mar 2019 12:39:45 -0600 Subject: [PATCH 10/68] remove tabs, move to python3 testing with mpas hwt2017 ensemble. --- webplot.py | 72 +++++++++++++++++++++++++++--------------------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/webplot.py b/webplot.py index e3a1b34..91c992f 100755 --- a/webplot.py +++ b/webplot.py @@ -2,11 +2,12 @@ import matplotlib.pyplot as plt from mpl_toolkits.basemap import * from datetime import * -import cPickle as pickle +import pickle as pickle import os, sys, time, argparse import scipy.ndimage as ndimage from scipy import interpolate import subprocess +import pdb from fieldinfo import * from netCDF4 import Dataset, MFDataset @@ -19,8 +20,8 @@ def __init__(self): self.debug = self.opts['debug'] self.autolevels = self.opts['autolevels'] self.domain = self.opts['domain'] - if ',' in self.opts['timerange']: self.shr, self.ehr = map(int, self.opts['timerange'].split(',')) - else: self.shr, self.ehr = int(self.opts['timerange']), int(self.opts['timerange']) + if ',' in self.opts['timerange']: self.shr, self.ehr = list(map(int, self.opts['timerange'].split(','))) + else: self.shr, self.ehr = int(self.opts['timerange']), int(self.opts['timerange']) self.createFilename() self.ENS_SIZE = int(os.getenv('ENS_SIZE', 10)) @@ -39,8 +40,8 @@ def createFilename(self): self.outfile = prefx+'_f'+'%03d'%self.shr+'-f'+'%03d'%self.ehr+'_'+self.domain+'.png' # 'test.png' # CSS def loadMap(self): - PYTHON_SCRIPTS_DIR = os.getenv('PYTHON_SCRIPTS_DIR', '/glade/u/home/wrfrt/rt_ensemble/python_scripts') - self.fig, self.ax, self.m = pickle.load(open('%s/rt2015_%s.pk'%(PYTHON_SCRIPTS_DIR,self.domain), 'r')) + PYTHON_SCRIPTS_DIR = os.getenv('PYTHON_SCRIPTS_DIR', '/glade/work/ahijevyc/share/rt_ensemble/python_scripts') + self.fig, self.ax, self.m = pickle.load(open('%s/rt2015_%s.pk'%(PYTHON_SCRIPTS_DIR,self.domain), 'rb')) lats, lons = readGrid(PYTHON_SCRIPTS_DIR) self.x, self.y = self.m(lons,lats) @@ -68,8 +69,8 @@ def plotDepartures(self): with open('/glade/u/home/sobash/RT2015_gpx/hly-inventory.txt') as f: for line in f: stn = line.split() - lonlat = map(float, stn[1:3][::-1]) - x_ob, y_ob = self.m(*zip(lonlat)) + lonlat = list(map(float, stn[1:3][::-1])) + x_ob, y_ob = self.m(*list(zip(lonlat))) hly_forecast[stn[0]] = fi((y_ob,x_ob))[0] hly_inventory[stn[0]] = (y_ob[0], x_ob[0], lonlat[0], lonlat[1]) @@ -111,7 +112,7 @@ def plotDepartures(self): cax.set_axis_bgcolor('#dddddd') cax.set_xticks([]) cax.set_yticks([]) - for i in cax.spines.itervalues(): i.set_linewidth(0.5) + for i in list(cax.spines.values()): i.set_linewidth(0.5) sizes, start_y, start_c, labels = [5,8,11,13,15], 0.84, 1, ['0-5F', '5-10F', '10-15F', '15-20F', '>= 20F'] cax.text(0.2,0.94,'Below', va='center', ha='center', fontdict=fontdict, transform=cax.transAxes) @@ -131,7 +132,7 @@ def interpolateData(self): #latlons = zip(lons[10::40,10::40].flatten(), lats[10::40,10::40].flatten()) f = interpolate.RegularGridInterpolator((self.y[:,0], self.x[0,:]), self.data['fill'][0], fill_value=-9999, bounds_error=False, method='linear') - x_ob, y_ob = self.m(*zip(*latlons)) + x_ob, y_ob = self.m(*list(zip(*latlons))) fcst_val = f((y_ob,x_ob)) fontdict = {'family':'monospace', 'size':9 } @@ -142,12 +143,12 @@ def interpolateData(self): self.ax.text(x_ob[i], y_ob[i], int(round(fcst_val[i])), fontdict=fontdict, ha='center', va='center') def plotReports(self): - import csv, re, urllib2 + import csv, re, urllib.request, urllib.error, urllib.parse url = 'http://www.spc.noaa.gov/climo/reports/%s_rpts_raw.csv'%(self.initdate.strftime('%y%m%d')) #url = 'http://www.spc.noaa.gov/climo/reports/yesterday.csv' - print url - response = urllib2.urlopen(url) + print(url) + response = urllib.request.urlopen(url) cr = csv.reader(response) lats, lons, type = [], [], [] @@ -238,7 +239,6 @@ def plotFill(self): # smooth some of the fill fields if self.opts['fill']['name'] == 'avo500': self.data['fill'][0] = ndimage.gaussian_filter(self.data['fill'][0], sigma=4) if self.opts['fill']['name'] == 'pbmin': self.data['fill'][0] = ndimage.gaussian_filter(self.data['fill'][0], sigma=2) - cs1 = self.m.contourf(self.x, self.y, self.data['fill'][0], levels=levels, cmap=cmap, norm=norm, extend='max', ax=self.ax) self.plotColorbar(cs1, levels, tick_labels, extend, extendfrac) @@ -283,8 +283,8 @@ def plotFill_ptypes(self): type_labels= self.opts['fill']['arrayname'] # Get label directly from fieldinfo arrayname. type_labels = ['Rain', 'Snow', 'Sleet', 'Freezing Rain'] # Hard-coded if any(len(lst) != ntypes for lst in [colors, threshes, type_labels]): - print "data, colors, threshes, and type_labels must all be same length" - print ntypes, colors, threshes, type_labels + print("data, colors, threshes, and type_labels must all be same length") + print(ntypes, colors, threshes, type_labels) sys.exit(1) # make axes for colorbar, 175px to left and 35px down from bottom of map x0, y0 = self.ax.transAxes.transform((0,0)) @@ -454,10 +454,10 @@ def plotStamp(self): def saveFigure(self, trans=False): # place NCAR logo 57 pixels below bottom of map, then save image if 'ensprod' in self.opts['fill']: # CSS needed incase not a fill plot - if not trans and self.opts['fill']['ensprod'] != 'stamp': - x, y = self.ax.transAxes.transform((0,0)) - self.fig.figimage(plt.imread('ncar.png'), xo=x, yo=(y-44), zorder=1000) - plt.text(x+10, y-54, 'ensemble.ucar.edu', fontdict={'size':9, 'color':'#505050'}, transform=None) + if not trans and self.opts['fill']['ensprod'] != 'stamp': + x, y = self.ax.transAxes.transform((0,0)) + self.fig.figimage(plt.imread('ncar.png'), xo=x, yo=(y-44), zorder=1000) + plt.text(x+10, y-54, 'ensemble.ucar.edu', fontdict={'size':9, 'color':'#505050'}, transform=None) plt.savefig(self.outfile, dpi=90, transparent=trans) @@ -476,7 +476,7 @@ def parseargs(): parser = argparse.ArgumentParser(description='Web plotting script for NCAR ensemble') parser.add_argument('-d', '--date', default=datetime.utcnow().strftime('%Y%m%d00'), help='initialization datetime (YYYYMMDDHH)') parser.add_argument('-tr', '--timerange', required=True, help='time range of forecasts (START,END)') - parser.add_argument('-f', '--fill', help='fill field (FIELD_PRODUCT_THRESH), field keys:'+','.join(fieldinfo.keys())) + parser.add_argument('-f', '--fill', help='fill field (FIELD_PRODUCT_THRESH), field keys:'+','.join(list(fieldinfo.keys()))) parser.add_argument('-c', '--contour', help='contour field (FIELD_PRODUCT_THRESH)') parser.add_argument('-b', '--barb', help='barb field (FIELD_PRODUCT_THRESH)') parser.add_argument('-bs', '--barbskip', help='barb skip interval') @@ -545,15 +545,16 @@ def makeEnsembleList(wrfinit, timerange, ENS_SIZE): file_list = { 'wrfout':[], 'upp': [], 'diag':[] } missing_list = { 'wrfout':[], 'upp': [], 'diag':[] } - EXP_DIR = os.getenv('EXP_DIR', '/glade/scratch/wrfrt/realtime_ensemble/ensf') - + # First convert netcdf4 to netcdf4-classic with nccopy + # for example, nccopy -d nc7 /glade/p/nsc/nmmm0046/schwartz/MPAS_ens_15-3km_mesh/POST/2017050100/ens_1/diag_latlon_g193.2017-05-02_00.00.00.nc /glade/scratch/ahijevyc/hwt2017/2017050100/ens_1/diag_latlon_g193.2017-05-02_00.00.00.nc + EXP_DIR = os.getenv('EXP_DIR', '/glade/scratch/ahijevyc/hwt2017') missing_index = 0 for hr in range(shr,ehr+1): - wrfvalidstr = (wrfinit + timedelta(hours=hr)).strftime('%Y-%m-%d_%H:%M:%S') + wrfvalidstr = (wrfinit + timedelta(hours=hr)).strftime('%Y-%m-%d_%H.%M.%S') yyyymmddhh = wrfinit.strftime('%Y%m%d%H') for mem in range(1,ENS_SIZE+1): wrfout = '%s/%s/wrf_rundir/ens_%d/wrfout_d02_%s'%(EXP_DIR,yyyymmddhh,mem,wrfvalidstr) - diag = '%s/%s/wrf_rundir/ens_%d/diags_d02.%s.nc'%(EXP_DIR,yyyymmddhh,mem,wrfvalidstr) + diag = '%s/%s/ens_%d/diag_latlon_g193.%s.nc'%(EXP_DIR,yyyymmddhh,mem,wrfvalidstr) upp = '%s/%s/post_rundir/mem_%d/fhr_%d/WRFTWO%02d.nc'%(EXP_DIR,yyyymmddhh,mem,hr,hr) if os.path.exists(wrfout): file_list['wrfout'].append(wrfout) else: missing_list['wrfout'].append(missing_index) @@ -566,15 +567,15 @@ def makeEnsembleList(wrfinit, timerange, ENS_SIZE): def readEnsemble(wrfinit, timerange=None, fields=None, debug=False, ENS_SIZE=10): ''' Reads in desired fields and returns 2-D arrays of data for each field (barb/contour/field) ''' - if debug: print fields + if debug: print(fields) datadict = {} file_list, missing_list = makeEnsembleList(wrfinit, timerange, ENS_SIZE) #construct list of files # loop through fill field, contour field, barb field and retrieve required data for f in ['fill', 'contour', 'barb']: - if not fields[f].keys(): continue - if debug: print 'Reading field:', fields[f]['name'], 'from', fields[f]['filename'] + if not list(fields[f].keys()): continue + if debug: print('Reading field:', fields[f]['name'], 'from', fields[f]['filename']) # save some variables for use in this function filename = fields[f]['filename'] @@ -585,13 +586,13 @@ def readEnsemble(wrfinit, timerange=None, fields=None, debug=False, ENS_SIZE=10) if fieldtype[0:3]=='mem': member = int(fieldtype[3:]) # open Multi-file netcdf dataset - if debug: print file_list[filename] + if debug: print(file_list[filename]) fh = MFDataset(file_list[filename]) # loop through each field, wind fields will have two fields that need to be read datalist = [] for n,array in enumerate(arrays): - if debug: print 'Reading', array + if debug: print('Reading', array) #read in 3D array (times*members,ny,nx) from file object if 'arraylevel' in fields[f]: @@ -671,7 +672,7 @@ def readEnsemble(wrfinit, timerange=None, fields=None, debug=False, ENS_SIZE=10) if fieldtype in ['neprob','neprobgt','neproblt']: data = compute_neprob(data, roi=14, sigma=float(fields['sigma']), type='gaussian') else: data = np.nanmean(data, axis=0) data = data+0.001 #hack to ensure that plot displays discrete prob values - if debug: print 'field', fieldname, 'has shape', data.shape, 'max', data.max(), 'min', data.min() + if debug: print('field', fieldname, 'has shape', data.shape, 'max', data.max(), 'min', data.min()) # attach data arrays for each type of field (e.g. { 'fill':[data], 'barb':[data,data] }) datadict[f].append(data) @@ -681,10 +682,9 @@ def readEnsemble(wrfinit, timerange=None, fields=None, debug=False, ENS_SIZE=10) return (datadict, missing_list) def readGrid(file_dir): - f = Dataset('%s/rt2015_latlon_d02.nc'%file_dir, 'r') - lats = f.variables['XLAT'][0,:] - lons = f.variables['XLONG'][0,:] - f.close() + lats = np.arange(90.,-90.25,-0.25) + lons = np.arange(0.,360,0.25) + lons, lats = np.meshgrid(lons,lats) return (lats,lons) def saveNewMap(domstr='CONUS'): @@ -723,7 +723,7 @@ def saveNewMap(domstr='CONUS'): m.drawstates(linewidth=0.25, ax=ax) m.drawcountries(ax=ax) - pickle.dump((fig,ax,m), open('rt2015_%s.pk'%domstr, 'w')) + pickle.dump((fig,ax,m), open('rt2015_%s.pk'%domstr, 'wb')) def compute_pmm(ensemble): mem, dy, dx = ensemble.shape @@ -797,5 +797,5 @@ def compute_rh(data): return 100*rh def showKeys(): - print fieldinfo.keys() + print(list(fieldinfo.keys())) sys.exit() From 5e127dc509a654e636cc9d05125fa43737505df0 Mon Sep 17 00:00:00 2001 From: ahijevyc Date: Tue, 2 Apr 2019 11:39:19 -0600 Subject: [PATCH 11/68] moved to ~ahijevyc/bin/. --- make_webplot.py | 43 ------------------------------------------- 1 file changed, 43 deletions(-) delete mode 100755 make_webplot.py diff --git a/make_webplot.py b/make_webplot.py deleted file mode 100755 index 4e69460..0000000 --- a/make_webplot.py +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env python - -import sys, time, os -from webplot import webPlot, readGrid, saveNewMap - -def log(msg): print(time.ctime(time.time()),':', msg) - -log('Begin Script') -stime = time.time() - -newPlot = webPlot() - -log('Reading Data') -newPlot.readEnsemble() - -#for dom in ['CONUS', 'NGP', 'SGP', 'CGP']: -for dom in ['CONUS', 'NGP', 'SGP', 'CGP','SW','NW','SE','NE','MATL']: - file_not_created, num_attempts = True, 0 - while file_not_created and num_attempts <= 3: - newPlot.domain = dom - - newPlot.createFilename() - fname = newPlot.outfile - - log('Loading Map for %s'%newPlot.domain) - newPlot.loadMap() - - log('Plotting Data') - newPlot.plotFields() - newPlot.plotTitleTimes() - - log('Writing Image') - newPlot.saveFigure() - - if os.path.exists(fname): - file_not_created = False - log('Created %s, %.1f KB'%(fname,os.stat(fname).st_size/1000.0)) - - num_attempts += 1 - -etime = time.time() -log('End Plotting (took %.2f sec)'%(etime-stime)) - From 69faeea9fd7788d297db2788a5353eda886c86ad Mon Sep 17 00:00:00 2001 From: ahijevyc Date: Tue, 2 Apr 2019 11:40:25 -0600 Subject: [PATCH 12/68] Note about time zones. Check sqlite3 database from Ryan Sobash. --- spc.py | 150 +++++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 130 insertions(+), 20 deletions(-) diff --git a/spc.py b/spc.py index 4d08c27..97e044b 100644 --- a/spc.py +++ b/spc.py @@ -5,8 +5,10 @@ import pdb import numpy as np import datetime +import os # for basename import matplotlib.path as mpath import cartopy +import sqlite3 def spc_event_filename(event_type): name = '/glade/work/ahijevyc/share/SPC/'+event_type+'/' @@ -33,9 +35,6 @@ def get_storm_reports( all_rpts = pd.DataFrame() for event_type in event_types: rpts_file = spc_event_filename(event_type) - if debug: - print("input file:",rpts_file) - pdb.set_trace() # csv format described in http://www.spc.noaa.gov/wcm/data/SPC_severe_database_description.pdf # SPC storm report files downloaded from http://www.spc.noaa.gov/wcm/#data to @@ -50,8 +49,8 @@ def get_storm_reports( "date": str, "tz": np.int32, "st": str, - "stf": np.int64, - "stn": np.int64, + "stf": np.int64, # State FIPS number. some Puerto Rico codes are incorrect + "stn": np.int64, # State number - number of this tornado in this state in this year "mag": np.float64, # you might think there is "sz" for hail and "f" for torn, but all "mag" "inj": np.int64, "fat": np.int64, @@ -74,7 +73,16 @@ def get_storm_reports( "fc": np.int64, } rpts = pd.read_csv(rpts_file, parse_dates=[['date','time']], dtype=dtype, infer_datetime_format=True) + if debug: + print("input file:",rpts_file) + print("read",len(rpts),"lines") + pdb.set_trace() rpts["event_type"] = event_type + rpts["source"] = os.path.basename(rpts_file) + + # -9 = unknown tornado F-scale + # Change -9 to NaN + rpts["mag"].replace(to_replace=-9, value = np.nan, inplace=True) # Augment event type for large hail and high wind. if event_type == "hail": @@ -86,29 +94,124 @@ def get_storm_reports( if any(highwind): rpts.loc[highwind, "event_type"] = "high wind" + # Fix tz=6. This is MDT in the NECI Storm Events database. + # This is confusing. A time in MDT is the same as the time in CST. + # So simply change tz to 3 (CST). + MDT = rpts['tz'] == 6 + if any(MDT): + MDT_timezones = rpts[['om','date_time','tz','event_type', 'source']][MDT] + print("get_storm_reports: found",len(MDT_timezones),"MDT time zones") + if debug: + print(MDT_timezones, file=sys.stderr) + print("changing tz from 6 to 3 because CST=MDT") + print("WARNING - proceeding with program. Wrote to SPC Apr 1 2019 about fixing these lines", file=sys.stderr) + Email20190401PatrickMarsh = "...took over database in 2017 and have no record or documentation as to what those timezones are. Each year I append new information to the end of the old information, so timezones will continue to exist as is until I can learn what those time zones are. take a look at the NCEI version of storm data. They may have information I do not." + rpts.loc[MDT == 6, "tz"] = 3 + # When timezone = 0, it is 'UNK' (unknown?) in the NCEI Storm Events database. + # When timezone = 6, it is 'MDT' (Mountain Daylight Time?) in the NCEI Storm Events database. + """ + om yr_mo_dy_time tz county/zone time zone according to NCEI Storm Events +2507 245 1956-06-01 11:33:00 0 UNK +2855 89 1957-04-02 23:45:00 0 UNK +8145 216 1965-05-05 14:45:00 6 MDT +9569 158 1967-04-21 12:33:00 0 UNK +13409 264 1972-05-13 18:08:00 0 UNK +15792 804 1974-08-13 15:03:00 0 UNK +20640 458 1980-06-04 16:30:00 0 UNK +22101 271 1982-05-11 14:25:00 0 UNK +24007 200 1984-04-26 19:32:00 0 CST +25899 501 1986-07-01 22:15:00 6 MDT +26054 656 1986-09-04 18:55:00 6 MDT +28793 419 1990-05-24 15:00:00 6 MDT +28797 422 1990-05-24 16:00:00 6 MDT +28810 433 1990-05-24 18:33:00 6 MDT +29192 815 1990-06-27 20:00:00 6 MDT +29232 855 1990-07-05 21:10:00 6 MDT +29347 971 1990-08-15 18:30:00 6 MDT +33262 151 1994-04-22 18:06:00 6 MDT +33557 446 1994-05-31 15:00:00 6 MDT +33572 461 1994-06-06 14:40:00 6 MDT +33574 463 1994-06-06 15:00:00 6 MDT +33585 474 1994-06-07 14:47:00 6 MDT +33586 476 1994-06-07 15:57:00 6 MDT +33587 477 1994-06-07 16:10:00 6 MDT +33589 478 1994-06-07 16:35:00 6 MDT +33592 482 1994-06-07 18:50:00 6 not in Storm Events database +33783 672 1994-06-29 15:45:00 6 MDT +33834 722 1994-07-06 18:17:00 6 MDT +33855 744 1994-07-12 14:30:00 6 MDT +33886 775 1994-07-18 15:30:00 6 MDT +33887 776 1994-07-18 16:00:00 6 MDT +33888 777 1994-07-18 16:25:00 6 MDT +33889 778 1994-07-18 16:40:00 6 MDT +33890 779 1994-07-18 16:55:00 6 not in Storm Events database +33891 780 1994-07-18 16:55:00 6 not in Storm Events database +33892 781 1994-07-18 17:00:00 6 MDT +""" + + + # All times, except for ?=unknown and 9=GMT, were converted to 3=CST. # tz=3 is CST # tz=9 is GMT - # To convert to UTC, add 9 and subtract tz hours. - + # Convert to UTC by adding 9 and subtracting tz hours. rpts["time"] = rpts['date_time'] + pd.to_timedelta(9 - rpts.tz, unit='h') # make time-zone aware datetime object rpts["time"] = rpts["time"].dt.tz_localize(pytz.UTC) - if any(rpts['tz'] != 3): - print(rpts_file, file=sys.stderr) - unexpected_timezones = rpts[['om','date_time','tz']][rpts['tz'] != 3] - print("get_storm_reports: found",len(unexpected_timezones),"unexpected timezones") + if any(rpts['tz'] == 0): + print("reports file: "+rpts_file, file=sys.stderr) + unknown_timezones = rpts[['om','date_time','tz','event_type', 'source']][rpts['tz'] == 0] if debug: - print(unexpected_timezones, file=sys.stderr) - print("WARNING - proceeding with program. Wrote to SPC Mar 22 2017 about fixing these lines", file=sys.stderr) + print("get_storm_reports: found",len(unknown_timezones),"unknown time zones") + print(unknown_timezones, file=sys.stderr) time_window = (rpts.time >= start) & (rpts.time < end) rpts = rpts[time_window] if debug: print("found",len(rpts),event_type,"reports") - all_rpts = all_rpts.append(rpts, ignore_index=True, sort=False) + + # Sanity Check: + # Verify I get the same thing as Ryan Sobash's sqlite3 database + conn = sqlite3.connect("/glade/u/home/sobash/2013RT/REPORTS/reports_all.db") + sqltable = "reports_" + event_type + # Could apply a datetime range here (WHERE datetime BETWEEN yyyy/mm/dd hh:mm:ss and blah), but converting from UTC to CST is tricky. + sqlcommand = "SELECT * FROM "+sqltable + sql_df = pd.read_sql_query(sqlcommand, conn, parse_dates=['datetime']) + conn.close() + # Add 6 hours to datetime. This converts to UTC. + sql_df["datetime"] = sql_df["datetime"] + pd.to_timedelta(6, unit='h') + # make it aware of its UTC timezone. + sql_df["datetime"] = sql_df["datetime"].dt.tz_localize(pytz.UTC) + sql_df = sql_df[(sql_df.datetime >= start) & (sql_df.datetime < end)] + + # See if they have the same number of rows + if len(sql_df) != len(rpts): + print("My data don't match Ryan's SQL database") + pdb.set_trace() + sys.exit(1) + # See if they have the same times + if (sql_df["datetime"].values != rpts["time"].values).any(): + print("My data times don't match Ryan's SQL database") + # See if they have the same locations + same_columns = ["slat", "slon", "elat", "elon"] + if (sql_df[same_columns].values != rpts[same_columns].values).any(): + print("My data locations don't match Ryan's SQL database") + max_abs_difference = np.max(np.abs(sql_df[same_columns]-rpts[same_columns])) + print("max abs difference") + print(max_abs_difference) + if all(max_abs_difference < 0.000001): + print("who cares about such a small difference?") + else: + pdb.set_trace() + sys.exit(1) + if debug: + print("From Ryan's SQL database") + print(sqlcommand) + print(sql_df.to_string()) + all_rpts = all_rpts.append(rpts, ignore_index=True, sort=False) + return all_rpts def plot(storm_reports, ax, scale=1, drawradius=0, alpha=0.4, debug=False): @@ -164,24 +267,31 @@ def to_MET(df, gribcode=187): # gribcode 187 :lightning # INPUT: storm_reports DataFrame from spc.get_storm_reports() # Output: MET point observation format, one observation per line. - # Use gribcode if specified. Otherwise lightning by default (187). + # Use gribcode if specified. Otherwise default. + # 10 = WIND # # Each observation line will consist of the following 11 columns of data: met_columns = ["Message_Type", "Station_ID", "Valid_Time", "Lat", "Lon", "Elevation", "Grib_Code", "Level", "Height", "QC_String", "Observation_Value"] df["Message_Type"] = "ADPSFC" - df["Station_ID"] = df.stn + df["Station_ID"] = df.st df["Valid_Time"] = df.time.dt.strftime('%Y%m%d_%H%M%S') #df.time should be aware of UTC time zone df["Lat"] = (df.slat+df.elat)/2 df["Lon"] = (df.slon+df.elon)/2 df["Elevation"] = "NA" - df["Grib_Code"] = gribcode # 187 :lightning - df["Level"] = "NA" + df["Grib_Code"] = gribcode df["Height"] = "NA" + # Change grib_code to 10 (WIND) where event type is wind + df.loc[df['event_type'].str.contains('wind'), "Grib_Code"] = 10 + df.loc[df['event_type'].str.contains('wind'), "Height"] = 10 + df["Level"] = "NA" df["QC_String"] = "NA" - df["Observation_Value"] = 1 + df["Observation_Value"] = df.mag # index=False don't write index number - return df.to_string(columns=met_columns,index=False,header=False) + # Change NaN to "NA" MET considers "NA" missing + # This may not matter, but by adding a string 'NA', it changes the format of the entire column. Floats change to integers (or maybe strings). + df.replace(to_replace=np.nan, value = 'NA', inplace=True) + return df.to_string(columns=met_columns,index=False,header=False) + "\n" From bd665bd1b933923a198ffdba26daec300753a3ea Mon Sep 17 00:00:00 2001 From: ahijevyc Date: Tue, 2 Apr 2019 11:42:45 -0600 Subject: [PATCH 13/68] use xarray because it can handle muliple files with NETCDF4 non-classic. netCDF4 MFDataset can't do that. More debugging messages --- webplot.py | 50 ++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/webplot.py b/webplot.py index 91c992f..27bfaaa 100755 --- a/webplot.py +++ b/webplot.py @@ -10,6 +10,8 @@ import pdb from fieldinfo import * from netCDF4 import Dataset, MFDataset +# use xarray because it can handle muliple files with NETCDF4 non-classic. netCDF4 MFDataset can't do that. +import xarray class webPlot: '''A class to plot data from NCAR ensemble''' @@ -239,6 +241,7 @@ def plotFill(self): # smooth some of the fill fields if self.opts['fill']['name'] == 'avo500': self.data['fill'][0] = ndimage.gaussian_filter(self.data['fill'][0], sigma=4) if self.opts['fill']['name'] == 'pbmin': self.data['fill'][0] = ndimage.gaussian_filter(self.data['fill'][0], sigma=2) + cs1 = self.m.contourf(self.x, self.y, self.data['fill'][0], levels=levels, cmap=cmap, norm=norm, extend='max', ax=self.ax) self.plotColorbar(cs1, levels, tick_labels, extend, extendfrac) @@ -340,7 +343,7 @@ def plotContour(self): def plotBarbs(self): skip = self.opts['barb']['skip'] - if self.domain != 'CONUS': skip = 20 + if self.domain != 'CONUS': skip = int(skip/2) if self.opts['fill']['name'] == 'crefuh': alpha=0.5 else: alpha=1.0 @@ -419,7 +422,7 @@ def plotStamp(self): # plot, unless file that has fill field is missing, then skip if member not in self.missing_members[filename] and member < self.ENS_SIZE: - cs1 = self.m.contourf(self.x, self.y, self.data['fill'][0][memberidx,:], levels=levels, cmap=cmap, norm=norm, extend='max', ax=thisax) + cs1 = self.m.contourf(self.x, self.y, self.data['fill'][0].isel(Time=memberidx), levels=levels, cmap=cmap, norm=norm, extend='max', ax=thisax) memberidx += 1 # use every other tick for large colortables, remove last tick label for both @@ -539,23 +542,34 @@ def parseargs(): opts[f] = thisdict return opts -def makeEnsembleList(wrfinit, timerange, ENS_SIZE): +def makeEnsembleList(wrfinit, timerange, ENS_SIZE, debug=False): # create lists of files (and missing file indices) for various file types shr, ehr = timerange file_list = { 'wrfout':[], 'upp': [], 'diag':[] } missing_list = { 'wrfout':[], 'upp': [], 'diag':[] } - # First convert netcdf4 to netcdf4-classic with nccopy + # To use MFDataset, first convert netcdf4 to netcdf4-classic with nccopy # for example, nccopy -d nc7 /glade/p/nsc/nmmm0046/schwartz/MPAS_ens_15-3km_mesh/POST/2017050100/ens_1/diag_latlon_g193.2017-05-02_00.00.00.nc /glade/scratch/ahijevyc/hwt2017/2017050100/ens_1/diag_latlon_g193.2017-05-02_00.00.00.nc EXP_DIR = os.getenv('EXP_DIR', '/glade/scratch/ahijevyc/hwt2017') + EXP_DIR = os.getenv('EXP_DIR', '/glade/p/nsc/nmmm0046/schwartz/MPAS_ens_15-3km_mesh/POST') missing_index = 0 for hr in range(shr,ehr+1): wrfvalidstr = (wrfinit + timedelta(hours=hr)).strftime('%Y-%m-%d_%H.%M.%S') yyyymmddhh = wrfinit.strftime('%Y%m%d%H') + if debug: + print("wrfvalidstr: "+wrfvalidstr) + print("yyyymmddhh: "+yyyymmddhh) for mem in range(1,ENS_SIZE+1): - wrfout = '%s/%s/wrf_rundir/ens_%d/wrfout_d02_%s'%(EXP_DIR,yyyymmddhh,mem,wrfvalidstr) + #wrfout = '%s/%s/wrf_rundir/ens_%d/wrfout_d02_%s'%(EXP_DIR,yyyymmddhh,mem,wrfvalidstr) diag = '%s/%s/ens_%d/diag_latlon_g193.%s.nc'%(EXP_DIR,yyyymmddhh,mem,wrfvalidstr) - upp = '%s/%s/post_rundir/mem_%d/fhr_%d/WRFTWO%02d.nc'%(EXP_DIR,yyyymmddhh,mem,hr,hr) + #upp = '%s/%s/post_rundir/mem_%d/fhr_%d/WRFTWO%02d.nc'%(EXP_DIR,yyyymmddhh,mem,hr,hr) + wrfout = diag + upp = diag + if debug: + if mem == 1: + print("wrfout: "+wrfout) + print("diag: "+diag) + print("upp: "+upp) if os.path.exists(wrfout): file_list['wrfout'].append(wrfout) else: missing_list['wrfout'].append(missing_index) if os.path.exists(diag): file_list['diag'].append(diag) @@ -563,6 +577,17 @@ def makeEnsembleList(wrfinit, timerange, ENS_SIZE): if os.path.exists(upp): file_list['upp'].append(upp) else: missing_list['upp'].append(missing_index) missing_index += 1 + if debug: + print("file_list",file_list) + print("missing_list", missing_list) + if not file_list['wrfout'] and not file_list['diag'] and not file_list['upp']: + print("wrfvalidstr: "+wrfvalidstr) + print("yyyymmddhh: "+yyyymmddhh) + print("wrfout: "+wrfout) + print("diag: "+diag) + print("upp: "+upp) + print("file_list is empty.") + pdb.set_trace() return (file_list, missing_list) def readEnsemble(wrfinit, timerange=None, fields=None, debug=False, ENS_SIZE=10): @@ -570,7 +595,7 @@ def readEnsemble(wrfinit, timerange=None, fields=None, debug=False, ENS_SIZE=10) if debug: print(fields) datadict = {} - file_list, missing_list = makeEnsembleList(wrfinit, timerange, ENS_SIZE) #construct list of files + file_list, missing_list = makeEnsembleList(wrfinit, timerange, ENS_SIZE, debug=debug) #construct list of files # loop through fill field, contour field, barb field and retrieve required data for f in ['fill', 'contour', 'barb']: @@ -586,8 +611,9 @@ def readEnsemble(wrfinit, timerange=None, fields=None, debug=False, ENS_SIZE=10) if fieldtype[0:3]=='mem': member = int(fieldtype[3:]) # open Multi-file netcdf dataset - if debug: print(file_list[filename]) - fh = MFDataset(file_list[filename]) + if debug: print(file_list[filename]) + fh = xarray.open_mfdataset(file_list[filename],concat_dim='Time') + # loop through each field, wind fields will have two fields that need to be read datalist = [] @@ -606,11 +632,11 @@ def readEnsemble(wrfinit, timerange=None, fields=None, debug=False, ENS_SIZE=10) # change units for certain fields if array in ['U_PL', 'V_PL', 'UBSHR6','VBSHR6','UBSHR1','VBSHR1','U10','V10', 'U_COMP_STM', 'V_COMP_STM','S_PL','U_COMP_STM_6KM','V_COMP_STM_6KM']: data = data*1.93 # m/s > kt - elif array in ['DEWPOINT_2M', 'T2', 'AFWA_WCHILL', 'AFWA_HEATIDX']: data = (data - 273.15)*1.8 + 32.0 # K > F + elif array in ['DEWPOINT_2M', 'T2', 't2m', 'AFWA_WCHILL', 'AFWA_HEATIDX']: data = (data - 273.15)*1.8 + 32.0 # K > F elif array in ['PREC_ACC_NC', 'PREC_ACC_C', 'AFWA_PWAT', 'PWAT', 'AFWA_SNOWFALL', 'AFWA_SNOW', 'AFWA_ICE', 'AFWA_FZRA','AFWA_RAIN_HRLY', 'AFWA_ICE_HRLY','AFWA_SNOWFALL_HRLY', 'AFWA_FZRA_HRLY']: data = data*0.0393701 # mm > in elif array in ['RAINNC', 'GRPL_MAX', 'SNOW_ACC_NC', 'AFWA_HAIL', 'HAILCAST_DIAM_MEAN', 'HAILCAST_DIAM_STD', 'HAILCAST_DIAM_MAX']: data = data*0.0393701 # mm > in elif array in ['T_PL', 'TD_PL', 'SFC_LI']: data = data - 273.15 # K > C - elif array in ['AFWA_MSLP', 'MSLP']: data = data*0.01 # Pa > hPa + elif array in ['AFWA_MSLP', 'MSLP', 'mslp']: data = data*0.01 # Pa > hPa elif array in ['ECHOTOP']: data = data*3.28084# m > ft elif array in ['AFWA_VIS', 'VISIBILITY']: data = (data*0.001)/1.61 # m > mi elif array in ['SBCINH', 'MLCINH', 'W_DN_MAX', 'UP_HELI_MIN']: data = data*-1.0 # make cin positive @@ -700,7 +726,7 @@ def saveNewMap(domstr='CONUS'): else: ll_lat, ll_lon, ur_lat, ur_lon = domains[domstr]['corners'] fig_width = domains[domstr]['fig_width'] - lat_1, lat_2, lon_0 = 32.0, 46.0, -101.0 + lat_1, lat_2, lon_0 = 38.5, 38.5, -97.5 dpi = 90 fig = plt.figure(dpi=dpi) From 153074f60b02fcb85b1aaf6558d39630ef799714 Mon Sep 17 00:00:00 2001 From: ahijevyc Date: Wed, 3 Apr 2019 10:35:42 -0600 Subject: [PATCH 14/68] merged in ~sobash/RT.../webplot.py. Deal with xarray and added .values when needed (like to use flatten()) --- webplot.py | 647 +++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 501 insertions(+), 146 deletions(-) diff --git a/webplot.py b/webplot.py index 27bfaaa..e2ea431 100755 --- a/webplot.py +++ b/webplot.py @@ -9,6 +9,8 @@ import subprocess import pdb from fieldinfo import * +# To use MFDataset, first convert netcdf4 to netcdf4-classic with nccopy +# for example, nccopy -d nc7 /glade/p/nsc/nmmm0046/schwartz/MPAS_ens_15-3km_mesh/POST/2017050100/ens_1/diag_latlon_g193.2017-05-02_00.00.00.nc /glade/scratch/ahijevyc/hwt2017/2017050100/ens_1/diag_latlon_g193.2017-05-02_00.00.00.nc from netCDF4 import Dataset, MFDataset # use xarray because it can handle muliple files with NETCDF4 non-classic. netCDF4 MFDataset can't do that. import xarray @@ -39,13 +41,22 @@ def createFilename(self): if self.shr == self.ehr: # CSS self.outfile = prefx+'_f'+'%03d'%self.shr+'_'+self.domain+'.png' # 'test.png' # CSS else: # CSS + #self.outfile = prefx+'_f'+'%03d'%self.shr+'-f'+'%03d'%self.ehr+'_'+self.domain+'_'+self.opts['date']+'.png' # 'test.png' # CSS self.outfile = prefx+'_f'+'%03d'%self.shr+'-f'+'%03d'%self.ehr+'_'+self.domain+'.png' # 'test.png' # CSS - def loadMap(self): - PYTHON_SCRIPTS_DIR = os.getenv('PYTHON_SCRIPTS_DIR', '/glade/work/ahijevyc/share/rt_ensemble/python_scripts') - self.fig, self.ax, self.m = pickle.load(open('%s/rt2015_%s.pk'%(PYTHON_SCRIPTS_DIR,self.domain), 'rb')) - lats, lons = readGrid(PYTHON_SCRIPTS_DIR) - self.x, self.y = self.m(lons,lats) + def loadMap(self, overlay=False): + if overlay: + PYTHON_SCRIPTS_DIR = os.getenv('PYTHON_SCRIPTS_DIR', '/glade/u/home/sobash/RT2015_gpx') + self.fig, self.ax, self.m = pickle.load(open('%s/overlays/rt2015_overlay_%s.pk'%(PYTHON_SCRIPTS_DIR,self.domain), 'r')) + else: + PYTHON_SCRIPTS_DIR = os.getenv('PYTHON_SCRIPTS_DIR', '/glade/work/ahijevyc/share/rt_ensemble/python_scripts') + self.fig, self.ax, self.m = pickle.load(open('%s/rt2015_%s.pk'%(PYTHON_SCRIPTS_DIR,self.domain), 'rb')) + + # load lat/lons + LATLON_FILE = os.getenv('LATLON_FILE', PYTHON_SCRIPTS_DIR+'/rt2015_latlon_d02.nc') + #self.lats, self.lons = readGrid(LATLON_FILE) + self.lats, self.lons = readGridMPAS() + self.x, self.y = self.m(self.lons,self.lats) def readEnsemble(self): self.data, self.missing_members = readEnsemble(self.initdate, timerange=[self.shr,self.ehr], fields=self.opts, debug=self.debug, ENS_SIZE=self.ENS_SIZE) @@ -53,7 +64,7 @@ def readEnsemble(self): def plotDepartures(self): from collections import OrderedDict hly_inventory, hly_forecast = OrderedDict(), OrderedDict() - + fieldname = self.opts['fill']['name'] if fieldname == 't2depart': normals = open('/glade/u/home/sobash/RT2015_gpx/hly-temp-normal.txt').readlines() elif fieldname == 'td2depart': normals = open('/glade/u/home/sobash/RT2015_gpx/hly-dewp-normal.txt').readlines() @@ -81,10 +92,10 @@ def plotDepartures(self): elif fieldname == 'td2depart': cmap = plt.get_cmap('BrBG') norm = colors.BoundaryNorm([-50,-19.999,-14.999,-9.999,-4.999,0,5,10,15,20,50], cmap.N) #want to have bins so they both dont include bndrys self.m.set_axes_limits(ax=self.ax) # need to do this if no other basemap functions have been called - + for line in normals: if monthday in line: - stn = line.split() + stn = line.split() try: y_ob, x_ob, lon, lat = hly_inventory[stn[0]] except: continue @@ -92,11 +103,11 @@ def plotDepartures(self): elif lon > -102 and lon <= -85: localhr = hr_ct #CT elif lon > -85: localhr = hr_et #ET else: localhr = hr_mt #MT - + forecast = hly_forecast[stn[0]] normal = float(stn[localhr+3][:-1])/10.0 departure = float(forecast - normal) - + if x_ob < self.m.xmax and x_ob > self.m.xmin and y_ob < self.m.ymax and y_ob > self.m.ymin and normal > -50: if abs(departure) < 5: size = 5**2 elif abs(departure) < 10: size = 8**2 @@ -115,7 +126,7 @@ def plotDepartures(self): cax.set_xticks([]) cax.set_yticks([]) for i in list(cax.spines.values()): i.set_linewidth(0.5) - + sizes, start_y, start_c, labels = [5,8,11,13,15], 0.84, 1, ['0-5F', '5-10F', '10-15F', '15-20F', '>= 20F'] cax.text(0.2,0.94,'Below', va='center', ha='center', fontdict=fontdict, transform=cax.transAxes) cax.text(0.8,0.94,'Above', va='center', ha='center', fontdict=fontdict, transform=cax.transAxes) @@ -123,55 +134,129 @@ def plotDepartures(self): cax.scatter(0.2,start_y-i*0.18,s=sizes[i]**2,c=(-start_c-i*5), cmap=cmap, norm=norm, transform=cax.transAxes) cax.text(0.5,start_y-i*0.18,labels[i], va='center', ha='center', fontdict=fontdict, transform=cax.transAxes) cax.scatter(0.8,start_y-i*0.18,s=sizes[i]**2,c=(start_c+i*5), cmap=cmap, norm=norm, transform=cax.transAxes) - - def interpolateData(self): + + def plotInterp(self): with open('stations.txt') as f: content = f.readlines() latlons = [ [ float(num[:-1]) for num in line.split()[-2:] ] for line in content ] latlons = np.array(latlons)[:,::-1] latlons[:,0] = -1*latlons[:,0] - #lats, lons = readGrid() - #latlons = zip(lons[10::40,10::40].flatten(), lats[10::40,10::40].flatten()) + #latlons = zip(self.lons[10::40,10::40].flatten(), self.lats[10::40,10::40].flatten()) f = interpolate.RegularGridInterpolator((self.y[:,0], self.x[0,:]), self.data['fill'][0], fill_value=-9999, bounds_error=False, method='linear') x_ob, y_ob = self.m(*list(zip(*latlons))) - fcst_val = f((y_ob,x_ob)) - + fcst_val = f((y_ob,x_ob)) + + self.m.set_axes_limits(ax=self.ax) # need to do this if no other basemap functions have been called + fontdict = {'family':'monospace', 'size':9 } - self.ax.cla() - self.ax.axis('off') + #self.ax.cla() + #self.ax.axis('off') for i in range(fcst_val.size): if x_ob[i] < self.m.xmax and x_ob[i] > self.m.xmin and y_ob[i] < self.m.ymax and y_ob[i] > self.m.ymin and fcst_val[i] != -9999: self.ax.text(x_ob[i], y_ob[i], int(round(fcst_val[i])), fontdict=fontdict, ha='center', va='center') def plotReports(self): - import csv, re, urllib.request, urllib.error, urllib.parse - - url = 'http://www.spc.noaa.gov/climo/reports/%s_rpts_raw.csv'%(self.initdate.strftime('%y%m%d')) + #import csv, re, urllib2 + #url = 'http://www.spc.noaa.gov/climo/reports/%s_rpts_raw.csv'%(self.initdate.strftime('%y%m%d')) + #print url #url = 'http://www.spc.noaa.gov/climo/reports/yesterday.csv' - print(url) - response = urllib.request.urlopen(url) - cr = csv.reader(response) + #url = 'http://www.spc.noaa.gov/climo/reports/today.csv' + #response = urllib2.urlopen(url) + #cr = csv.reader(response) + + import sqlite3 + colors = ['red', 'green', 'blue'] + conn = sqlite3.connect('/glade/u/home/sobash/2013RT/REPORTS/reports_all.db') + c = conn.cursor() + #for i,table in enumerate(['reports_torn', 'reports_hail', 'reports_wind']): + for i,table in enumerate(['reports_torn', 'reports_hail', 'reports_wind']): + if table == 'reports_hail': c.execute("SELECT slat, slon, datetime FROM %s WHERE size > 2 AND datetime > '2015-06-04 18:00' AND datetime <= '2015-06-05 00:00' ORDER BY datetime asc"%table) + if table == 'reports_torn': c.execute("SELECT slat, slon, datetime FROM %s WHERE datetime > '2015-06-04 18:00' AND datetime <= '2015-06-05 00:00' ORDER BY datetime asc"%table) + if table == 'reports_wind': c.execute("SELECT slat, slon, datetime FROM %s WHERE datetime > '2015-06-04 18:00' AND datetime <= '2015-06-05 00:00' ORDER BY datetime asc"%table) + rpts = c.fetchall() + #rpts.extend(c.fetchall()) + olats, olons, dt = zip(*rpts) + x_ob, y_ob = self.m(olons, olats) + self.m.scatter(x_ob, y_ob, color=colors[i], edgecolor=None, s=25, ax=self.ax) - lats, lons, type = [], [], [] - report_type = 'hail' - for row in cr: - if re.search('Hail', row[0]): report_type = 'hail' - if re.search('Wind', row[0]): report_type = 'wind' - if re.search('Tornado', row[0]): report_type = 'torn' - - if re.search('^\d{4}', row[0]): - lats.append(float(row[5])) - lons.append(float(row[6])) - type.append(report_type) + #lats, lons, type = [], [], [] + #report_type = 'hail' + #for row in cr: + # if re.search('Hail', row[0]): report_type = 'hail' + # if re.search('Wind', row[0]): report_type = 'wind' + # if re.search('Tornado', row[0]): report_type = 'torn' + + # if re.search('^\d{4}', row[0]): + # lats.append(float(row[5])) + # lons.append(float(row[6])) + # type.append(report_type) - x_ob, y_ob = self.m(lons, lats) - self.m.scatter(x_ob, y_ob, color='k', edgecolor='k', s=3, ax=self.ax) + #x_ob, y_ob = self.m(lons, lats) + #self.m.scatter(x_ob, y_ob, color='k', edgecolor='k', s=3, ax=self.ax) def plotWarnings(self): - self.m.readshapefile('/glade/u/home/sobash/SHARPpy/OBS/sbw_shp/%s'%self.initdate.strftime('%Y%m%d12'),'warnings',drawbounds=True, linewidth=1, color='black', ax=self.ax) - + try: self.m.readshapefile('/glade/u/home/sobash/SHARPpy/OBS/sbw_shp/%s'%self.initdate.strftime('%Y%m%d12'),'warnings',drawbounds=True, linewidth=1, color='black', ax=self.ax) + except IndexError: print('IndexError - likely no warnings present') + + def plotOutlook(self): + self.m.readshapefile('/glade/u/home/sobash/RT2015_gpx/outlook_shp/day1otlk_20150515_1200_cat','day1otlk_20150515_1200_cat',drawbounds=True, linewidth=1, color='green', ax=self.ax) + + def plotMRMS(self, thresh=40.0): + validstr = (self.initdate+timedelta(hours=self.shr)).strftime('%Y%m%d%H') + + # READ IN MRMS GRID + fh = Dataset('/glade/p/nmmm0001/sobash/MRMS/mrms_grid.nc', 'r') + lats = fh.variables['lat_0'][:] + lons = fh.variables['lon_0'][:] + fh.close() + + # READ IN DATA + fh = Dataset('/glade/scratch/sobash/mrms_tmp/cref_mrms_%s.nc'%validstr, 'r') + mrms = fh.variables['CREF'][:] + fh.close() + + lats, lons = np.meshgrid(lats, lons) + x, y = self.m(lons, lats) + + cs1 = self.m.contourf(x, y, mrms.T, levels=[thresh,1000], colors=['k'], ax=self.ax) + + x0, y1 = self.ax.transAxes.transform((0,1)) + fontdict = {'family':'monospace', 'size':11} + self.ax.text(x0+10, y1-15, 'MRMS CREF >= %d dBZ'%thresh, fontdict=fontdict, transform=None) + + def plotMRMSmax(self, field='qpe'): + validstr = (self.initdate+timedelta(hours=self.shr-1)).strftime('%Y%m%d%H') + #validstr = (self.initdate+timedelta(hours=self.shr-1)).strftime('%Y%m%d00') + + # READ IN MRMS GRID + fh = Dataset('/glade/p/nmmm0001/sobash/MRMS/mrms_grid.nc', 'r') + lats = fh.variables['lat_0'][:] + lons = fh.variables['lon_0'][:] + fh.close() + + # READ IN DATA + if field == 'qpe': + fname = '/glade/scratch/sobash/mrms_tmp/qpe_mrms_hrmax_%s.nc'%validstr + field = 'QPE01' + levels = [25.4,1000] + if field == 'cref': + fname = '/glade/scratch/sobash/mrms_tmp/cref_mrms_hrmax_%s.nc'%validstr + field = 'CREF' + levels = [40,1000] + + fh = Dataset(fname, 'r') + mrms = fh.variables[field][0,:] + fh.close() + + # plot + lats, lons = np.meshgrid(lats, lons) + x, y = self.m(lons, lats) + + cs1 = self.m.contourf(x, y, mrms.T, levels=levels, colors=['k'], ax=self.ax) + def plotTitleTimes(self): + if self.opts['over']: return fontdict = {'family':'monospace', 'size':12, 'weight':'bold'} # place title and times above corners of map @@ -195,12 +280,12 @@ def plotTitleTimes(self): if len(self.missing_members['wrfout']) > 0: missing_members = sorted(set([ (x%10)+1 for x in self.missing_members['wrfout'] ])) #get member number from missing indices missing_members_string = ', '.join(str(x) for x in missing_members) - self.ax.text(x1-5, y0+5, 'Missing member #s: %s'%missing_members_string, horizontalalignment='right', transform=None) + self.ax.text(x1-5, y0+5, 'Missing member #s: %s'%missing_members_string, horizontalalignment='right') def plotFields(self): if 'fill' in self.data: if self.opts['fill']['ensprod'] == 'paintball': self.plotPaintball() - elif self.opts['fill']['ensprod'] == 'stamp': self.plotStamp() + elif self.opts['fill']['ensprod'] in ['stamp', 'maxstamp']: self.plotStamp() else: self.plotFill() if 'contour' in self.data: @@ -211,10 +296,9 @@ def plotFields(self): if 'barb' in self.data: #self.plotStreamlines() self.plotBarbs() - - if self.opts['interp']: self.interpolateData() - if self.opts['fill']['name'] in ['t2depart', 'td2depart']: self.plotDepartures() - + + #if self.opts['fill']['name'] in ['t2depart', 'td2depart']: self.plotDepartures() + def plotFill(self): if self.opts['fill']['name'] == 'ptype': self.plotFill_ptype(); return if self.opts['fill']['name'][0:6] == 'ptypes': self.plotFill_ptypes(); return @@ -231,7 +315,7 @@ def plotFill(self): cmap = colors.ListedColormap(self.opts['fill']['colors']) extend, extendfrac = 'neither', 0.0 tick_labels = levels[:-1] - if self.opts['fill']['ensprod'] in ['prob', 'neprob', 'problt', 'probgt', 'neprobgt', 'neproblt']: + if self.opts['fill']['ensprod'] in ['prob', 'neprob', 'problt', 'probgt', 'neprobgt', 'neproblt', 'prob3d']: cmap = colors.ListedColormap(self.opts['fill']['colors'][:9]) cmap.set_over(self.opts['fill']['colors'][-1]) extend, extendfrac = 'max', 0.02 @@ -241,27 +325,30 @@ def plotFill(self): # smooth some of the fill fields if self.opts['fill']['name'] == 'avo500': self.data['fill'][0] = ndimage.gaussian_filter(self.data['fill'][0], sigma=4) if self.opts['fill']['name'] == 'pbmin': self.data['fill'][0] = ndimage.gaussian_filter(self.data['fill'][0], sigma=2) - - cs1 = self.m.contourf(self.x, self.y, self.data['fill'][0], levels=levels, cmap=cmap, norm=norm, extend='max', ax=self.ax) + + # Sometimes you get a warning kwarg tri is ignored. + # Tried removing tri=True but got IndexError: too many indices for array + cs1 = self.m.contourf(self.x, self.y, self.data['fill'][0], levels=levels, cmap=cmap, norm=norm, tri=True, extend='max', ax=self.ax) #MPAS + #cs1 = self.m.contourf(self.x, self.y, self.data['fill'][0], levels=levels, cmap=cmap, norm=norm, extend='max', ax=self.ax) self.plotColorbar(cs1, levels, tick_labels, extend, extendfrac) def plotFill_ptype(self): ml_type = np.zeros(self.data['fill'][0].shape) ml_type_prob = np.zeros(self.data['fill'][0].shape) - + for i in [1,2,3,4]: pts = (self.data['fill'][i-1] > ml_type_prob+0.001) ml_type_prob[pts] = self.data['fill'][i-1][pts] ml_type[pts] = i+0.001 - + cmap = colors.ListedColormap(['#7BBF6A', 'red', 'orange', 'blue']) norm = colors.BoundaryNorm([1,2,3,4,5], cmap.N) - + x = (self.x[1:,1:] + self.x[:-1,:-1])/2.0 y = (self.y[1:,1:] + self.y[:-1,:-1])/2.0 cs1 = self.m.pcolormesh(x, y, np.ma.masked_equal(ml_type[1:,1:], 0), cmap=cmap, norm=norm, edgecolors='None', ax=self.ax) - + # make axes for colorbar, 175px to left and 30px down from bottom of map x0, y0 = self.ax.transAxes.transform((0,0)) x, y = self.fig.transFigure.inverted().transform((x0+175,y0-29.5)) @@ -318,12 +405,12 @@ def plotReflectivityUH(self): tick_labels = levels[:-1] cs1 = self.m.contourf(self.x, self.y, self.data['fill'][0], levels=levels, cmap=cmap, norm=norm, extend='max', ax=self.ax) - self.m.contourf(self.x, self.y, self.data['fill'][1], levels=[50,1000], colors='black', ax=self.ax, alpha=0.3) - self.m.contour(self.x, self.y, self.data['fill'][1], levels=[50], colors='k', linewidth=0.5, ax=self.ax) - + self.m.contourf(self.x, self.y, self.data['fill'][1], levels=[75,1000], colors='black', ax=self.ax, alpha=0.3) + self.m.contour(self.x, self.y, self.data['fill'][1], levels=[75], colors='k', linewidth=0.5, ax=self.ax) + #maxuh = self.data['fill'][1].max() #self.ax.text(0.03,0.03,'Domain-wide UH max %0.f'%maxuh ,ha="left",va="top",bbox=dict(boxstyle="square",lw=0.5,fc="white"), transform=self.ax.transAxes) - + self.plotColorbar(cs1, levels, tick_labels) def plotColorbar(self, cs, levels, tick_labels, extend='neither', extendfrac=0.0): @@ -333,9 +420,11 @@ def plotColorbar(self, cs, levels, tick_labels, extend='neither', extendfrac=0.0 cax = self.fig.add_axes([x,y,0.985-x,y/3.0]) cb = plt.colorbar(cs, cax=cax, orientation='horizontal', extend=extend, extendfrac=extendfrac, ticks=tick_labels) cb.outline.set_linewidth(0.5) - + def plotContour(self): - data = ndimage.gaussian_filter(self.data['contour'][0], sigma=10) + if self.opts['contour']['name'] in ['t2-0c']: data = ndimage.gaussian_filter(self.data['contour'][0], sigma=2) + else: data = ndimage.gaussian_filter(self.data['contour'][0], sigma=10) + if self.opts['contour']['name'] in ['sbcinh','mlcinh']: linewidth, alpha = 0.5, 0.75 else: linewidth, alpha = 1.5, 1.0 cs2 = self.m.contour(self.x, self.y, data, levels=self.opts['contour']['levels'], colors='k', linewidths=linewidth, ax=self.ax, alpha=alpha) @@ -344,7 +433,7 @@ def plotContour(self): def plotBarbs(self): skip = self.opts['barb']['skip'] if self.domain != 'CONUS': skip = int(skip/2) - + if self.opts['fill']['name'] == 'crefuh': alpha=0.5 else: alpha=1.0 @@ -363,8 +452,8 @@ def plotPaintball(self): colorlist = self.opts['fill']['colors'] levels = self.opts['fill']['levels'] for i in range(self.data['fill'][0].shape[0]): - cs = self.m.contourf(self.x, self.y, self.data['fill'][0][i,:], levels=levels, colors=[colorlist[i]], ax=self.ax, alpha=0.5) - rects.append(plt.Rectangle((0,0),1,1,fc=colorlist[i])) + cs = self.m.contourf(self.x, self.y, self.data['fill'][0][i,:], levels=levels, colors=[colorlist[i%len(colorlist)]], ax=self.ax, alpha=0.5) + rects.append(plt.Rectangle((0,0),1,1,fc=colorlist[i%len(colorlist)])) labels.append("member %d"%(i+1)) plt.legend(rects, labels, ncol=5, loc='right', bbox_to_anchor=(1.0,-0.05), fontsize=11, \ @@ -374,11 +463,11 @@ def plotSpaghetti(self): proxy = [] colorlist = self.opts['contour']['colors'] levels = self.opts['contour']['levels'] - data = ndimage.gaussian_filter(self.data['contour'][0], sigma=[0,10,10]) - for i in range(data.shape[0]): + data = ndimage.gaussian_filter(self.data['contour'][0], sigma=[0,4,4]) + for i in range(self.data['contour'][0].shape[0]): #cs = self.m.contour(self.x, self.y, data[i,:], levels=levels, colors=[colorlist[i]], linewidths=2, linestyles='solid', ax=self.ax) - cs = self.m.contour(self.x, self.y, data[i,:], levels=levels, colors='k', linewidths=1, linestyles='solid', ax=self.ax) - proxy.append(plt.Rectangle((0,0),1,1,fc=colorlist[i])) + cs = self.m.contour(self.x, self.y, data[i,:], levels=levels, colors='k', alpha=0.6, linewidths=1, linestyles='solid', ax=self.ax) + #proxy.append(plt.Rectangle((0,0),1,1,fc=colorlist[i])) #plt.legend(proxy, ["member %d"%i for i in range(1,11)], ncol=5, loc='right', bbox_to_anchor=(1.0,-0.05), fontsize=11, \ # frameon=False, borderpad=0.25, borderaxespad=0.25, handletextpad=0.2) @@ -398,7 +487,7 @@ def plotStamp(self): cmap = colors.ListedColormap(self.opts['fill']['colors']) norm = colors.BoundaryNorm(levels, cmap.N) filename = self.opts['fill']['filename'] - + memberidx = 0 for j in range(0,num_rows): for i in range(0,num_columns): @@ -426,8 +515,8 @@ def plotStamp(self): memberidx += 1 # use every other tick for large colortables, remove last tick label for both - if self.opts['fill']['name'] in ['goesch3', 'goesch4', 't2', 'precipacc' ]: ticks = levels[:-1][::2] # CSS added precipacc - else: ticks = levels[:-1] + if self.opts['fill']['name'] in ['goesch3', 'goesch4', 't2', 'precipacc' ]: ticks = levels[:-1][::2] # CSS added precipacc + else: ticks = levels[:-1] # add colorbar to figure cax = fig.add_axes([0.51,0.3,0.48,0.02]) @@ -457,17 +546,18 @@ def plotStamp(self): def saveFigure(self, trans=False): # place NCAR logo 57 pixels below bottom of map, then save image if 'ensprod' in self.opts['fill']: # CSS needed incase not a fill plot - if not trans and self.opts['fill']['ensprod'] != 'stamp': - x, y = self.ax.transAxes.transform((0,0)) - self.fig.figimage(plt.imread('ncar.png'), xo=x, yo=(y-44), zorder=1000) - plt.text(x+10, y-54, 'ensemble.ucar.edu', fontdict={'size':9, 'color':'#505050'}, transform=None) - + if not trans and self.opts['fill']['ensprod'] not in ['stamp', 'maxstamp']: + x, y = self.ax.transAxes.transform((0,0)) + self.fig.figimage(plt.imread('ncar.png'), xo=x, yo=(y-44), zorder=1000) + plt.text(x+10, y-54, 'ensemble.ucar.edu', fontdict={'size':9, 'color':'#505050'}, transform=None) + plt.savefig(self.outfile, dpi=90, transparent=trans) if self.opts['convert']: #command = 'convert -colors 255 %s %s'%(self.outfile, self.outfile) - if not self.opts['fill']: ncolors = 48 + if not self.opts['fill']: ncolors = 48 #if no fill field exists elif self.opts['fill']['ensprod'] in ['prob', 'neprob', 'probgt', 'problt', 'neprobgt', 'neproblt']: ncolors = 48 + elif self.opts['fill']['name'] in ['crefuh']: ncolors = 48 else: ncolors = 255 command = '/glade/u/home/sobash/pngquant/pngquant %d %s --ext=.png --force'%(ncolors,self.outfile) ret = subprocess.check_call(command.split()) @@ -487,13 +577,16 @@ def parseargs(): parser.add_argument('-dom', '--domain', default='CONUS', help='domain to plot') parser.add_argument('-al', '--autolevels', action='store_true', help='use min/max to determine levels for plot') parser.add_argument('-con', '--convert', default=True, action='store_false', help='run final image through imagemagick') - parser.add_argument('-i', '--interp', action='store_true', help='plot interpolated station values') + parser.add_argument('-i', '--interp', default=False, action='store_true', help='plot interpolated station values') parser.add_argument('-sig', '--sigma', default=2, help='smooth probabilities using gaussian smoother') parser.add_argument('--debug', action='store_true', help='turn on debugging') + parser.add_argument('-v', '--verif', default=None, help='plot verification data') + parser.add_argument('--over', default=False, action='store_true', help='plot as overlay (no lines, transparent, no convert)') opts = vars(parser.parse_args()) + if opts['interp']: opts['over'] = True + # opts = { 'date':date, 'timerange':timerange, 'fill':'sbcape_prob_25', 'ensprod':'mean' ... } - # now, convert underscore delimited fill, contour, and barb args into dicts for f in ['contour','barb','fill']: thisdict = {} @@ -505,10 +598,13 @@ def parseargs(): thisdict['arrayname'] = fieldinfo[input[0]]['fname'] # assign contour levels and colors - if (input[1] in ['prob', 'neprob', 'problt', 'probgt', 'neprobgt', 'neproblt']): + if (input[1] in ['prob', 'neprob', 'probgt', 'problt', 'neprobgt', 'neproblt', 'prob3d']): thisdict['thresh'] = float(input[2]) - thisdict['levels'] = np.arange(0.1,1.1,0.1) + if int(opts['sigma']) != 40: thisdict['levels'] = np.arange(0.1,1.1,0.1) + else: thisdict['levels'] = [0.02,0.05,0.1,0.15,0.2,0.25,0.35,0.45,0.6] + thisdict['levels'] = np.arange(0.1,1.1,0.2) thisdict['colors'] = readNCLcm('perc2_9lev') + thisdict['colors'] = ['#d9d9d9', '#bdbdbd', '#969696', '#636363', '#252525'] elif (input[1] in ['paintball', 'spaghetti']): thisdict['thresh'] = float(input[2]) thisdict['levels'] = [float(input[2]), 1000] @@ -542,34 +638,24 @@ def parseargs(): opts[f] = thisdict return opts -def makeEnsembleList(wrfinit, timerange, ENS_SIZE, debug=False): +def makeEnsembleList(wrfinit, timerange, ENS_SIZE): # create lists of files (and missing file indices) for various file types shr, ehr = timerange file_list = { 'wrfout':[], 'upp': [], 'diag':[] } missing_list = { 'wrfout':[], 'upp': [], 'diag':[] } - # To use MFDataset, first convert netcdf4 to netcdf4-classic with nccopy - # for example, nccopy -d nc7 /glade/p/nsc/nmmm0046/schwartz/MPAS_ens_15-3km_mesh/POST/2017050100/ens_1/diag_latlon_g193.2017-05-02_00.00.00.nc /glade/scratch/ahijevyc/hwt2017/2017050100/ens_1/diag_latlon_g193.2017-05-02_00.00.00.nc - EXP_DIR = os.getenv('EXP_DIR', '/glade/scratch/ahijevyc/hwt2017') - EXP_DIR = os.getenv('EXP_DIR', '/glade/p/nsc/nmmm0046/schwartz/MPAS_ens_15-3km_mesh/POST') + EXP_DIR = os.getenv('EXP_DIR', '/glade/scratch/wrfrt/realtime_ensemble/ensf') + missing_index = 0 for hr in range(shr,ehr+1): - wrfvalidstr = (wrfinit + timedelta(hours=hr)).strftime('%Y-%m-%d_%H.%M.%S') + wrfvalidstr = (wrfinit + timedelta(hours=hr)).strftime('%Y-%m-%d_%H:%M:%S') yyyymmddhh = wrfinit.strftime('%Y%m%d%H') - if debug: - print("wrfvalidstr: "+wrfvalidstr) - print("yyyymmddhh: "+yyyymmddhh) for mem in range(1,ENS_SIZE+1): - #wrfout = '%s/%s/wrf_rundir/ens_%d/wrfout_d02_%s'%(EXP_DIR,yyyymmddhh,mem,wrfvalidstr) - diag = '%s/%s/ens_%d/diag_latlon_g193.%s.nc'%(EXP_DIR,yyyymmddhh,mem,wrfvalidstr) - #upp = '%s/%s/post_rundir/mem_%d/fhr_%d/WRFTWO%02d.nc'%(EXP_DIR,yyyymmddhh,mem,hr,hr) - wrfout = diag - upp = diag - if debug: - if mem == 1: - print("wrfout: "+wrfout) - print("diag: "+diag) - print("upp: "+upp) + wrfout = '%s/%s/wrf_rundir/ens_%d/wrfout_d02_%s'%(EXP_DIR,yyyymmddhh,mem,wrfvalidstr) + diag = '%s/%s/wrf_rundir/ens_%d/diags_d02.%s.nc'%(EXP_DIR,yyymmddhh,mem,wrfvalidstr) + #diag = '/glade/scratch/sobash/FOR_MORRIS/%s/mem%d/diags_d02_f%03d.nc'%(yyyymmddhh,mem,hr) + upp = '%s/%s/post_rundir/mem_%d/fhr_%d/WRFTWO%02d.nc'%(EXP_DIR,yyyymmddhh,mem,hr,hr) + #ens1 = '/glade/p/nmm0001/romine/rt2015/ens_1km/%s/mem%02d_%s.nc'%(yyyymmddhh,mem,yyyymmddhh) if os.path.exists(wrfout): file_list['wrfout'].append(wrfout) else: missing_list['wrfout'].append(missing_index) if os.path.exists(diag): file_list['diag'].append(diag) @@ -577,25 +663,165 @@ def makeEnsembleList(wrfinit, timerange, ENS_SIZE, debug=False): if os.path.exists(upp): file_list['upp'].append(upp) else: missing_list['upp'].append(missing_index) missing_index += 1 - if debug: - print("file_list",file_list) - print("missing_list", missing_list) - if not file_list['wrfout'] and not file_list['diag'] and not file_list['upp']: - print("wrfvalidstr: "+wrfvalidstr) - print("yyyymmddhh: "+yyyymmddhh) - print("wrfout: "+wrfout) - print("diag: "+diag) - print("upp: "+upp) - print("file_list is empty.") - pdb.set_trace() + return (file_list, missing_list) + +def makeEnsembleListStan(wrfinit, timerange, ENS_SIZE): + # create lists of files (and missing file indices) for various file types + shr, ehr = timerange + file_list = { 'wrfout':[], 'upp': [], 'diag':[] } + missing_list = { 'wrfout':[], 'upp': [], 'diag':[] } + + EXP_DIR = os.getenv('EXP_DIR', '/glade/scratch/trier/jun4-5/') + + missing_index = 0 + #for hr in range(shr,ehr+1): + print(shr, ehr) + for m in range(shr*60,(ehr*60)+1,15): + #wrfvalidstr = (wrfinit + timedelta(hours=m)).strftime('%Y-%m-%d_%H:%M:%S') + wrfvalidstr = (wrfinit + timedelta(minutes=m)).strftime('%Y-%m-%d_%H:%M:%S') + yyyymmddhh = wrfinit.strftime('%Y%m%d%H') + for mem in range(1,ENS_SIZE+1): + wrfout = '%s/ens_%d/wrfout_d02_%s'%(EXP_DIR,mem,wrfvalidstr) + diag = '%s/ens_%d/diags_d02.%s.nc'%(EXP_DIR,mem,wrfvalidstr) + print(diag) + if os.path.exists(wrfout): file_list['wrfout'].append(wrfout) + else: missing_list['wrfout'].append(missing_index) + if os.path.exists(diag): file_list['diag'].append(diag) + else: missing_list['diag'].append(missing_index) + missing_index += 1 + return (file_list, missing_list) + +def makeEnsembleListHREF(wrfinit, timerange, ENS_SIZE): + # create lists of files (and missing file indices) for various file types + shr, ehr = timerange + file_list = { 'wrfout':[], 'diag':[] } + missing_list = { 'wrfout':[], 'diag':[] } + missing_index = 0 + wrfinit_prev = (wrfinit - timedelta(hours=12)) + + for hr in range(shr,ehr+1): + yyyymmddhh = wrfinit.strftime('%Y%m%d%H') + yyyymmddhh_p = wrfinit_prev.strftime('%Y%m%d%H') + hr_p = hr - 12 + init = wrfinit.strftime('%H') + init_p = wrfinit_prev.strftime('%H') + + for mem in range(1,ENS_SIZE+1): + if mem == 1: diag = '/glade/scratch/sobash/HREF/%s/hiresw.t%sz.arw_5km.f%02d.conus.grib2'%(yyyymmddhh,init,hr) + if mem == 2: diag = '/glade/scratch/sobash/HREF/%s/hiresw.t%sz.arw_5km.f%02d.conusmem2.grib2'%(yyyymmddhh,init,hr) + if mem == 3: diag = '/glade/scratch/sobash/HREF/%s/hiresw.t%sz.nmmb_5km.f%02d.conus.grib2'%(yyyymmddhh,init,hr) + if mem == 4: diag = '/glade/scratch/sobash/HREF/%s/nam.t%sz.conusnest.hiresf%02d.tm00.grib2'%(yyyymmddhh,init,hr) + if mem == 5: diag = '/glade/scratch/sobash/HREF/%s/hiresw.t%sz.arw_5km.f%02d.conus.grib2'%(yyyymmddhh_p,init_p,hr_p) + if mem == 6: diag = '/glade/scratch/sobash/HREF/%s/hiresw.t%sz.arw_5km.f%02d.conusmem2.grib2'%(yyyymmddhh_p,init_p,hr_p) + if mem == 7: diag = '/glade/scratch/sobash/HREF/%s/hiresw.t%sz.nmmb_5km.f%02d.conus.grib2'%(yyyymmddhh_p,init_p,hr_p) + if mem == 8: diag = '/glade/scratch/sobash/HREF/%s/nam.t%sz.conusnest.hiresf%02d.tm00.grib2'%(yyyymmddhh_p,init_p,hr_p) + + print(diag) + + if os.path.exists(diag): file_list['diag'].append(diag) + else: missing_list['diag'].append(missing_index) + missing_index += 1 + + + return (file_list, missing_list) + +def makeEnsembleListMPAS(wrfinit, timerange, ENS_SIZE, debug=False): + # create lists of files (and missing file indices) for various file types + shr, ehr = timerange + file_list = { 'wrfout':[], 'diag':[] } + missing_list = { 'wrfout':[], 'diag':[] } + missing_index = 0 + for hr in range(shr,ehr+1): + wrfvalidstr = (wrfinit + timedelta(hours=hr)).strftime('%Y-%m-%d_%H.%M.%S') + yyyymmddhh = wrfinit.strftime('%Y%m%d%H') + for mem in range(1,ENS_SIZE+1): + diag = '/glade/p/nsc/nmmm0046/schwartz/MPAS_ens_15-3km_mesh/POST/%s/ens_%d/diag.%s.nc'%(yyyymmddhh,mem,wrfvalidstr) + print(diag) + if os.path.exists(diag): file_list['diag'].append(diag) + else: missing_list['diag'].append(missing_index) + missing_index += 1 + if not file_list['diag']: + print('Empty file_list') + return (file_list, missing_list) + +def makeEnsembleListArchive(wrfinit, timerange): + # create lists of files (and missing file indices) for various file types + shr, ehr = timerange + file_list = { 'wrfout':[], 'upp': [], 'diag':[] } + missing_list = { 'wrfout':[], 'upp': [], 'diag':[] } + + missing_index = 0 + yyyymmddhh = wrfinit.strftime('%Y%m%d%H') + for mem in range(1,11): + ens1 = '/glade/scratch/sobash/RT2015/%s/mem%d_surrogate_%s.nc'%(yyyymmddhh,mem,yyyymmddhh) + #ens1 = '/glade/scratch/sobash/RT2013_1KMENS/%s/mem%02d_%s.nc'%(yyyymmddhh,mem,yyyymmddhh) + print(ens1) + if os.path.exists(ens1): file_list['wrfout'].append(ens1) + else: missing_list['wrfout'].append(missing_index) + if os.path.exists(ens1): file_list['diag'].append(ens1) + else: missing_list['diag'].append(missing_index) + if os.path.exists(ens1): file_list['upp'].append(ens1) + else: missing_list['upp'].append(missing_index) + missing_index += 1 + return (file_list, missing_list) + +def makeEnsembleListNSC(wrfinit, timerange): + # create lists of files (and missing file indices) for various file types + shr, ehr = timerange + file_list = { 'wrfout':[], 'diag':[] } + missing_list = { 'wrfout':[], 'diag':[] } + + EXP_DIR = os.getenv('EXP_DIR', '/glade/scratch/wrfrt/realtime_ensemble/ensf') + + missing_index = 0 + for hr in range(shr,ehr+1): + wrfvalidstr = (wrfinit + timedelta(hours=hr)).strftime('%Y-%m-%d_%H_%M_%S') + yyyymmddhh = wrfinit.strftime('%Y%m%d%H') + for mem in range(1,2): + diag = '%s/%s/diags_d01_%s.nc'%(EXP_DIR,yyyymmddhh,wrfvalidstr) + print(diag) + if os.path.exists(diag): file_list['diag'].append(diag) + else: missing_list['diag'].append(missing_index) + missing_index += 1 + return (file_list, missing_list) + +def makeEnsembleListDA(wrfinit, timerange): + # create lists of files (and missing file indices) for various file types + shr, ehr = timerange + file_list = { 'wrfout':[], 'diag':[] } + missing_list = { 'wrfout':[], 'diag':[] } + + #EXP_DIR = os.getenv('EXP_DIR', '/glade/scratch/hclin/CONUS/wrfda/expdir/rt/fcst_15km') + #EXP_DIR = os.getenv('EXP_DIR', '/glade/scratch/sobash/VSE/1km_pbl7') + EXP_DIR = os.getenv('EXP_DIR', '/glade/scratch/schwartz/VSE/3km_pbl7') + missing_index = 0 + for hr in range(shr,ehr+1): + wrfvalidstr = (wrfinit + timedelta(hours=hr)).strftime('%Y-%m-%d_%H:%M:%S') + yyyymmddhh = wrfinit.strftime('%Y%m%d%H') + for mem in range(1,2): + wrfout = '%s/%s/wrfout_d01_%s'%(EXP_DIR,yyyymmddhh,wrfvalidstr) + #diag = '%s/%s/wrf/join/vse_d01.%s.nc'%(EXP_DIR,yyyymmddhh,wrfvalidstr) + diag = '%s/%s/wrf/diags_d01.%s.nc'%(EXP_DIR,yyyymmddhh,wrfvalidstr) + print(diag) + if os.path.exists(wrfout): file_list['wrfout'].append(wrfout) + else: missing_list['wrfout'].append(missing_index) + if os.path.exists(diag): file_list['diag'].append(diag) + else: missing_list['diag'].append(missing_index) + missing_index += 1 return (file_list, missing_list) def readEnsemble(wrfinit, timerange=None, fields=None, debug=False, ENS_SIZE=10): ''' Reads in desired fields and returns 2-D arrays of data for each field (barb/contour/field) ''' if debug: print(fields) - + datadict = {} - file_list, missing_list = makeEnsembleList(wrfinit, timerange, ENS_SIZE, debug=debug) #construct list of files + #file_list, missing_list = makeEnsembleList(wrfinit, timerange, ENS_SIZE) #construct list of files + #file_list, missing_list = makeEnsembleListNSC(wrfinit, timerange) #construct list of files + #file_list, missing_list = makeEnsembleListStan(wrfinit, timerange, ENS_SIZE) #construct list of files + file_list, missing_list = makeEnsembleListMPAS(wrfinit, timerange, ENS_SIZE, debug=debug) #construct list of files + #file_list, missing_list = makeEnsembleListArchive(wrfinit, timerange) #construct list of files + #file_list, missing_list = makeEnsembleListHybrid(wrfinit, timerange) #construct list of files + #file_list, missing_list = makeEnsembleListHREF(wrfinit, timerange, ENS_SIZE) #construct list of files # loop through fill field, contour field, barb field and retrieve required data for f in ['fill', 'contour', 'barb']: @@ -607,13 +833,16 @@ def readEnsemble(wrfinit, timerange=None, fields=None, debug=False, ENS_SIZE=10) arrays = fields[f]['arrayname'] fieldtype = fields[f]['ensprod'] fieldname = fields[f]['name'] - if fieldtype in ['prob', 'neprob', 'problt', 'probgt', 'neprobgt', 'neproblt']: thresh = fields[f]['thresh'] + if fieldtype in ['prob', 'neprob', 'problt', 'probgt', 'neprobgt', 'neproblt', 'prob3d']: thresh = fields[f]['thresh'] if fieldtype[0:3]=='mem': member = int(fieldtype[3:]) # open Multi-file netcdf dataset - if debug: print(file_list[filename]) - fh = xarray.open_mfdataset(file_list[filename],concat_dim='Time') + if debug: + pdb.set_trace() + fh = xarray.open_mfdataset(file_list[filename],concat_dim='Time') + # Dimension has different times and members. + fh = fh.rename({'Time':'TimeMember'}) # loop through each field, wind fields will have two fields that need to be read datalist = [] @@ -626,20 +855,32 @@ def readEnsemble(wrfinit, timerange=None, fields=None, debug=False, ENS_SIZE=10) else: level = fields[f]['arraylevel'] else: level = None - if level == 'max': data = np.amax(fh.variables[array][:,:,:,:], axis=1) - elif level is None: data = fh.variables[array][:,:,:] - else: data = fh.variables[array][:,level,:,:] + #if level == 'max': data = np.amax(fh.variables[array][:,:,:,:], axis=1) + #elif level is None: data = fh.variables[array][:,:,:] + #else: data = fh.variables[array][:,level,:,:] + + data = fh.variables[array][:,:] + + #data = data.reshape((10,data.shape[1])) + #data = np.swapaxes(data,0,1) # flip first two axes so time is first + + # if all times are in one file, then need to reshape and extract desired times + #data = data.reshape((10,49,data.shape[1],data.shape[2])) # reshape + #data = data[:,timerange[0]:timerange[1]+1,:,:] # extract desired times + #data = np.swapaxes(data,0,1) # flip first two axes so time is first + #data = data.reshape((10*((timerange[1]+1)-timerange[0])),data.shape[2],data.shape[3]) #reshape again # change units for certain fields - if array in ['U_PL', 'V_PL', 'UBSHR6','VBSHR6','UBSHR1','VBSHR1','U10','V10', 'U_COMP_STM', 'V_COMP_STM','S_PL','U_COMP_STM_6KM','V_COMP_STM_6KM']: data = data*1.93 # m/s > kt - elif array in ['DEWPOINT_2M', 'T2', 't2m', 'AFWA_WCHILL', 'AFWA_HEATIDX']: data = (data - 273.15)*1.8 + 32.0 # K > F - elif array in ['PREC_ACC_NC', 'PREC_ACC_C', 'AFWA_PWAT', 'PWAT', 'AFWA_SNOWFALL', 'AFWA_SNOW', 'AFWA_ICE', 'AFWA_FZRA','AFWA_RAIN_HRLY', 'AFWA_ICE_HRLY','AFWA_SNOWFALL_HRLY', 'AFWA_FZRA_HRLY']: data = data*0.0393701 # mm > in - elif array in ['RAINNC', 'GRPL_MAX', 'SNOW_ACC_NC', 'AFWA_HAIL', 'HAILCAST_DIAM_MEAN', 'HAILCAST_DIAM_STD', 'HAILCAST_DIAM_MAX']: data = data*0.0393701 # mm > in + if array in ['U_PL', 'V_PL', 'UBSHR6','VBSHR6','UBSHR1', 'VBSHR1', 'U10','V10', 'U_COMP_STM', 'V_COMP_STM','S_PL','U_COMP_STM_6KM','V_COMP_STM_6KM']: data = data*1.93 # m/s > kt + elif array in ['DEWPOINT_2M', 'T2', 'AFWA_WCHILL', 'AFWA_HEATIDX']: data = (data - 273.15)*1.8 + 32.0 # K > F + elif array in ['PREC_ACC_NC', 'PREC_ACC_C', 'AFWA_PWAT', 'PWAT', 'AFWA_RAIN', 'AFWA_SNOWFALL', 'AFWA_SNOW', 'AFWA_ICE', 'AFWA_FZRA','AFWA_RAIN_HRLY','AFWA_ICE_HRLY','AFWA_SNOWFALL_HRLY', 'AFWA_FZRA_HRLY']: data = data*0.0393701 # mm > in + elif array in ['RAINNC', 'GRPL_MAX', 'SNOW_ACC_NC', 'AFWA_HAIL', 'HAILCAST_DIAM_MAX']: data = data*0.0393701 # mm > in elif array in ['T_PL', 'TD_PL', 'SFC_LI']: data = data - 273.15 # K > C - elif array in ['AFWA_MSLP', 'MSLP', 'mslp']: data = data*0.01 # Pa > hPa + elif array in ['AFWA_MSLP', 'MSLP']: data = data*0.01 # Pa > hPa elif array in ['ECHOTOP']: data = data*3.28084# m > ft - elif array in ['AFWA_VIS', 'VISIBILITY']: data = (data*0.001)/1.61 # m > mi - elif array in ['SBCINH', 'MLCINH', 'W_DN_MAX', 'UP_HELI_MIN']: data = data*-1.0 # make cin positive + elif array in ['UP_HELI_MIN']: data = np.abs(data) + elif array in ['AFWA_VIS', 'VISIBILITY']: data = (data*0.001)/1.61 # m > mi + elif array in ['SBCINH', 'MLCINH', 'W_DN_MAX']: data = data*-1.0 # make cin positive elif array in ['PVORT_320K']: data = data*1000000 # multiply by 1e6 elif array in ['SBT123_GDS3_NTAT','SBT124_GDS3_NTAT','GOESE_WV','GOESE_IR']: data = data -273.15 # K -> C elif array in ['HAIL_MAXK1', 'HAIL_MAX2D']: data = data*39.3701 # m -> inches @@ -649,17 +890,21 @@ def readEnsemble(wrfinit, timerange=None, fields=None, debug=False, ENS_SIZE=10) datalist.append(data) # these are derived fields, we don't have in any of the input files but we can compute + print(datalist[0].shape) if 'name' in fields[f]: if fieldname in ['shr06mag', 'shr01mag', 'bunkmag','speed10m']: datalist = [np.sqrt(datalist[0]**2 + datalist[1]**2)] + elif fieldname == 'uhratio': datalist = [compute_uhratio(datalist)] elif fieldname == 'stp': datalist = [computestp(datalist)] # GSR in fields are T(K), mixing ratio (kg/kg), and surface pressure (Pa) elif fieldname == 'thetae': datalist = [compute_thetae(datalist)] elif fieldname == 'rh2m': datalist = [compute_rh(datalist)] + elif fieldname == 'sspf': datalist = [ compute_sspf(datalist, file_list['upp']) ] #elif fieldname == 'pbmin': datalist = [ datalist[1] - datalist[0][:,0,:] ] elif fieldname == 'pbmin': datalist = [ datalist[1] - datalist[0] ] # CSS changed above line for GRIB2 elif fieldname in ['thck1000-500', 'thck1000-850'] : datalist = [ datalist[1]*0.1 - datalist[0]*0.1 ] # CSS added for thicknesses elif fieldname == 'winter': datalist = [datalist[1] + datalist[2] + datalist[3]] - + elif fieldname == 'frzdepth': datalist = [computefrzdepth(datalist)] + datadict[f] = [] for data in datalist: # perform mean/max/variance/etc to reduce 3D array to 2D @@ -668,38 +913,55 @@ def readEnsemble(wrfinit, timerange=None, fields=None, debug=False, ENS_SIZE=10) elif (fieldtype == 'max'): data = np.amax(data, axis=0) elif (fieldtype == 'min'): data = np.amin(data, axis=0) elif (fieldtype == 'var'): data = np.std(data, axis=0) + elif (fieldtype == 'maxstamp'): + for i in missing_list[filename]: data = np.insert(data, i, np.nan, axis=0) #insert nan for missing files + data = np.reshape(data, (data.shape[0]/ENS_SIZE,ENS_SIZE,data.shape[1],data.shape[2])) + data = np.nanmax(data, axis=0) elif (fieldtype == 'summean'): for i in missing_list[filename]: data = np.insert(data, i, np.nan, axis=0) #insert nan for missing files data = np.reshape(data, (data.shape[0]/ENS_SIZE,ENS_SIZE,data.shape[1],data.shape[2])) data = np.nansum(data, axis=0) data = np.nanmean(data, axis=0) - elif (fieldtype == 'summax'): + elif (fieldtype == 'maxmean'): for i in missing_list[filename]: data = np.insert(data, i, np.nan, axis=0) #insert nan for missing files data = np.reshape(data, (data.shape[0]/ENS_SIZE,ENS_SIZE,data.shape[1],data.shape[2])) - data = np.nansum(data, axis=0) data = np.nanmax(data, axis=0) - elif (fieldtype == 'summin'): + data = np.nanmean(data, axis=0) + elif (fieldtype == 'summax'): for i in missing_list[filename]: data = np.insert(data, i, np.nan, axis=0) #insert nan for missing files data = np.reshape(data, (data.shape[0]/ENS_SIZE,ENS_SIZE,data.shape[1],data.shape[2])) data = np.nansum(data, axis=0) - data = np.nanmin(data, axis=0) + data = np.nanmax(data, axis=0) elif (fieldtype[0:3] == 'mem'): for i in missing_list[filename]: data = np.insert(data, i, np.nan, axis=0) #insert nan for missing files data = np.reshape(data, (data.shape[0]/ENS_SIZE,ENS_SIZE,data.shape[1],data.shape[2])) - if fieldname in ['precip', 'precipacc']: data = np.nansum(data, axis=0) + print(fieldname) + if fieldname in ['precip', 'precipacc']: + print('where we should be') + data = np.nanmax(data, axis=0) else: data = np.nanmax(data, axis=0) - data = data[member-1,:] + data = data[member-1,:] elif (fieldtype in ['prob', 'neprob', 'problt', 'probgt', 'neprobgt', 'neproblt']): if fieldtype in ['prob', 'neprob', 'probgt', 'neprobgt']: data = (data>=thresh).astype('float') elif fieldtype in ['problt', 'neproblt']: data = (data=thresh).astype('float') + for i in missing_list[filename]: data = np.insert(data, i, np.nan, axis=0) + data = np.reshape(data, (data.shape[0]/ENS_SIZE,ENS_SIZE,data.shape[1],data.shape[2])) + data = compute_prob3d(data, roi=14, sigma=float(fields['sigma']), type='gaussian') + if debug: print('field '+ fieldname+ ' has shape', data.shape, 'max', data.max(), 'min', data.min()) + print(data.max()) + #kernel = np.ones((7,7)) + #data = ndimage.filters.convolve(data, kernel/float(kernel.sum())) # attach data arrays for each type of field (e.g. { 'fill':[data], 'barb':[data,data] }) datadict[f].append(data) @@ -708,27 +970,46 @@ def readEnsemble(wrfinit, timerange=None, fields=None, debug=False, ENS_SIZE=10) return (datadict, missing_list) def readGrid(file_dir): - lats = np.arange(90.,-90.25,-0.25) - lons = np.arange(0.,360,0.25) - lons, lats = np.meshgrid(lons,lats) + f = Dataset(file_dir, 'r') + lats = f.variables['XLAT'][0,:] + lons = f.variables['XLONG'][0,:] + f.close() return (lats,lons) -def saveNewMap(domstr='CONUS'): - if 'file' in domains[domstr]: +def readGridMPAS(): + fh = Dataset("/glade/p/mmm/parc/schwartz/MPAS/15-3km_mesh/init.nc", "r") + lats = fh.variables['latCell'][:] + lons = fh.variables['lonCell'][:] + lats, lons = lats*57.29578, lons*57.29578 #convert radians to degrees + fh.close() + return (lats, lons) + +def saveNewMap(domstr='CONUS', wrfout=None): + # if domstr is not in the dictionary, then use provided wrfout to create new domain + if domstr not in domains: + fh = Dataset(wrfout, 'r') + lats = fh.variables['XLAT'][0,:] + lons = fh.variables['XLONG'][0,:] + ll_lat, ll_lon, ur_lat, ur_lon = lats[0,0], lons[0,0], lats[-1,-1], lons[-1,-1] + lat_1, lat_2, lon_0 = fh.TRUELAT1, fh.TRUELAT2, fh.STAND_LON + fig_width = 1080 + fh.close() + # else assume domstr is in dictionary + elif 'file' in domains[domstr]: fh = Dataset(domains[domstr]['file'], 'r') lats = fh.variables['XLAT'][0,:] lons = fh.variables['XLONG'][0,:] ll_lat, ll_lon, ur_lat, ur_lon = lats[0,0], lons[0,0], lats[-1,-1], lons[-1,-1] - lat_1, lat_2, lon_0 = fh.TRUELAT1, fh.TRUELAT2, fh.STAND_LON + lat_1, lat_2, lon_0 = fh.TRUELAT1, fh.TRUELAT2, fh.STAND_LON if 'fig_width' in domains[domstr]: fig_width = domains[domstr]['fig_width'] else: fig_width = 1080 fh.close() else: ll_lat, ll_lon, ur_lat, ur_lon = domains[domstr]['corners'] fig_width = domains[domstr]['fig_width'] - lat_1, lat_2, lon_0 = 38.5, 38.5, -97.5 + lat_1, lat_2, lon_0 = 32.0, 46.0, -101.0 - dpi = 90 + dpi = 90 fig = plt.figure(dpi=dpi) m = Basemap(projection='lcc', resolution='i', llcrnrlon=ll_lon, llcrnrlat=ll_lat, urcrnrlon=ur_lon, urcrnrlat=ur_lat, \ lat_1=lat_1, lat_2=lat_2, lon_0=lon_0, area_thresh=1000) @@ -744,25 +1025,56 @@ def saveNewMap(domstr='CONUS'): #x,y,w,h = 0.01, 0.8/float(fig_height), 0.98, 0.98*fig_width*m.aspect/float(fig_height) #too much padding at top x,y,w,h = 0.01, 0.7/float(fig_height), 0.98, 0.98*fig_width*m.aspect/float(fig_height) ax = fig.add_axes([x,y,w,h]) + for i in ax.spines.itervalues(): i.set_linewidth(0.5) m.drawcoastlines(linewidth=0.5, ax=ax) m.drawstates(linewidth=0.25, ax=ax) m.drawcountries(ax=ax) + m.drawcounties(linewidth=0.1, color='gray', ax=ax) pickle.dump((fig,ax,m), open('rt2015_%s.pk'%domstr, 'wb')) +def drawOverlay(domstr='CONUS'): + ll_lat, ll_lon, ur_lat, ur_lon = domains[domstr]['corners'] + fig_width = domains[domstr]['fig_width'] + lat_1, lat_2, lon_0 = 32.0, 46.0, -101.0 + dpi = 90 + + fig = plt.figure(dpi=dpi) + m = Basemap(projection='lcc', resolution='i', llcrnrlon=ll_lon, llcrnrlat=ll_lat, urcrnrlon=ur_lon, urcrnrlat=ur_lat, \ + lat_1=lat_1, lat_2=lat_2, lon_0=lon_0, area_thresh=1000) + + # compute height based on figure width, map aspect ratio, then add some vertical space for labels/colorbar + fig_width = fig_width/float(dpi) + #fig_height = fig_width*m.aspect + 0.93 + fig_height = fig_width*m.aspect + 1.25 + figsize = (fig_width, fig_height) + fig.set_size_inches(figsize) + + # place map 0.8" from bottom of figure, leave 0.45" at top for title (needs to be in figure-relative coords) + x,y,w,h = 0.01, 0.8/float(fig_height), 0.98, 0.98*fig_width*m.aspect/float(fig_height) + #x,y,w,h = 0.01, 0.7/float(fig_height), 0.98, 0.98*fig_width*m.aspect/float(fig_height) + ax = fig.add_axes([x,y,w,h]) + + #drawcounties doesnt work when called by itself? so have to drawcoastines first with lw=0 + m.drawcoastlines(linewidth=0, ax=ax) + m.drawcounties(ax=ax) + ax.axis('off') + plt.savefig('overlay_counties_%s.png'%domstr, dpi=90, transparent=True) + def compute_pmm(ensemble): - mem, dy, dx = ensemble.shape + mem = ensemble.shape[0] + spatial_dimensions = ensemble.shape[1:] ens_mean = np.mean(ensemble, axis=0) - ens_dist = np.sort(ensemble.flatten())[::-1] + ens_dist = np.sort(ensemble.values.flatten())[::-1] pmm = ens_dist[::mem] - ens_mean_index = np.argsort(ens_mean.flatten())[::-1] + ens_mean_index = np.argsort(ens_mean.values.flatten())[::-1] temp = np.empty_like(pmm) temp[ens_mean_index] = pmm - temp = np.where(ens_mean.flatten() > 0, temp, 0.0) - return temp.reshape((dy,dx)) + temp = np.where(ens_mean.values.flatten() > 0, temp, 0.0) + return temp.reshape(spatial_dimensions) def compute_neprob(ensemble, roi=0, sigma=0.0, type='gaussian'): y,x = np.ogrid[-roi:roi+1, -roi:roi+1] @@ -780,6 +1092,18 @@ def compute_neprob(ensemble, roi=0, sigma=0.0, type='gaussian'): ens_mean = ndimage.filters.gaussian_filter(ens_mean, sigma) return ens_mean +def compute_prob3d(ensemble, roi=0, sigma=0.0, type='gaussian'): + print(ensemble.shape) + y,x = np.ogrid[-roi:roi+1, -roi:roi+1] + kernel = x**2 + y**2 <= roi**2 + ens_roi = ndimage.filters.maximum_filter(ensemble, footprint=kernel[np.newaxis,np.newaxis,:]) + + print(ens_roi.shape) + ens_mean = np.nanmean(ens_roi, axis=1) + print(ens_mean.shape) + ens_mean = ndimage.filters.gaussian_filter(ens_mean, [2,20,20]) + return ens_mean[3,:] + def computestp(data): '''Compute STP with data array of [sbcape,sblcl,0-1srh,ushr06,vshr06]''' sbcape_term = (data[0]/1500.0) @@ -801,6 +1125,32 @@ def computestp(data): stp = np.ma.filled(stp, 0.0) return stp +def compute_sspf(data, cref_files): + fh = MFDataset(cref_files) + #cref = fh.variables['REFD_MAX'][:] #in diag files + cref = fh.variables['REFL_MAX_COL'][:] #in upp files + fh.close() + + # if all times are in one file, then need to reshape and extract desired times + #cref = cref.reshape((10,49,cref.shape[1],cref.shape[2])) # reshape + #cref = cref[:,13:36+1,:] # extract desired times + #cref = np.swapaxes(cref,0,1) # flip first two axes so time is first + #cref = cref.reshape((10*((13+1)-36)),cref.shape[2],cref.shape[3]) #reshape again + + # flag nearby points + #kernel = np.ones((5,5)) + cref_mask = (cref>=30.0) + #cref_mask = ndimage.filters.maximum_filter(cref_mask, footprint=kernel[np.newaxis,:]) + + #wind_cref_hits = (data[1]>=25.0) + wind_cref_hits = np.logical_and( (data[1]>=25.0), cref_mask) + + sspf = np.logical_or( np.logical_or((data[0]>=75), wind_cref_hits), (data[2]>=1)) + return sspf + +def compute_uhratio(data): + return np.where(data[1]>50, data[0]/data[1], 0.0) + def compute_thetae(data): # GSR constants for theta E calc P0 = 100000.0 # (Pa) @@ -825,3 +1175,8 @@ def compute_rh(data): def showKeys(): print(list(fieldinfo.keys())) sys.exit() + +def computefrzdepth(t): + frz_at_surface = np.where(t[0,:] < 33, True, False) #pts where surface T is below 33F + max_column_t = np.amax(t, axis=0) + above_frz_aloft = np.where(max_column_t > 32, True, False) #pts where max column T is above 32F From 7f70e9b1ddbb3528abf93208e2b21014ef6f5d5a Mon Sep 17 00:00:00 2001 From: ahijevyc Date: Wed, 3 Apr 2019 10:40:06 -0600 Subject: [PATCH 15/68] for variables that are converted to new units, add other naming conventions for MPAS hwt2017 ensemble. --- webplot.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/webplot.py b/webplot.py index e2ea431..7582c92 100755 --- a/webplot.py +++ b/webplot.py @@ -871,16 +871,16 @@ def readEnsemble(wrfinit, timerange=None, fields=None, debug=False, ENS_SIZE=10) #data = data.reshape((10*((timerange[1]+1)-timerange[0])),data.shape[2],data.shape[3]) #reshape again # change units for certain fields - if array in ['U_PL', 'V_PL', 'UBSHR6','VBSHR6','UBSHR1', 'VBSHR1', 'U10','V10', 'U_COMP_STM', 'V_COMP_STM','S_PL','U_COMP_STM_6KM','V_COMP_STM_6KM']: data = data*1.93 # m/s > kt - elif array in ['DEWPOINT_2M', 'T2', 'AFWA_WCHILL', 'AFWA_HEATIDX']: data = (data - 273.15)*1.8 + 32.0 # K > F - elif array in ['PREC_ACC_NC', 'PREC_ACC_C', 'AFWA_PWAT', 'PWAT', 'AFWA_RAIN', 'AFWA_SNOWFALL', 'AFWA_SNOW', 'AFWA_ICE', 'AFWA_FZRA','AFWA_RAIN_HRLY','AFWA_ICE_HRLY','AFWA_SNOWFALL_HRLY', 'AFWA_FZRA_HRLY']: data = data*0.0393701 # mm > in + if array in ['U_PL', 'V_PL', 'UBSHR6','VBSHR6','UBSHR1', 'VBSHR1', 'U10','u10','v10','V10', 'U_COMP_STM', 'V_COMP_STM','S_PL','U_COMP_STM_6KM','V_COMP_STM_6KM']: data = data*1.93 # m/s > kt + elif array in ['DEWPOINT_2M', 'T2', 't2m', 'AFWA_WCHILL', 'AFWA_HEATIDX']: data = (data - 273.15)*1.8 + 32.0 # K > F + elif array in ['PREC_ACC_NC', 'PREC_ACC_C', 'AFWA_PWAT', 'PWAT', 'precipw', 'AFWA_RAIN', 'AFWA_SNOWFALL', 'AFWA_SNOW', 'AFWA_ICE', 'AFWA_FZRA','AFWA_RAIN_HRLY','AFWA_ICE_HRLY','AFWA_SNOWFALL_HRLY', 'AFWA_FZRA_HRLY']: data = data*0.0393701 # mm > in elif array in ['RAINNC', 'GRPL_MAX', 'SNOW_ACC_NC', 'AFWA_HAIL', 'HAILCAST_DIAM_MAX']: data = data*0.0393701 # mm > in elif array in ['T_PL', 'TD_PL', 'SFC_LI']: data = data - 273.15 # K > C - elif array in ['AFWA_MSLP', 'MSLP']: data = data*0.01 # Pa > hPa + elif array in ['AFWA_MSLP', 'MSLP', 'mslp']: data = data*0.01 # Pa > hPa elif array in ['ECHOTOP']: data = data*3.28084# m > ft elif array in ['UP_HELI_MIN']: data = np.abs(data) elif array in ['AFWA_VIS', 'VISIBILITY']: data = (data*0.001)/1.61 # m > mi - elif array in ['SBCINH', 'MLCINH', 'W_DN_MAX']: data = data*-1.0 # make cin positive + elif array in ['SBCINH', 'MLCINH', 'W_DN_MAX', 'sbcin', 'mlcin']: data = data*-1.0 # make cin positive elif array in ['PVORT_320K']: data = data*1000000 # multiply by 1e6 elif array in ['SBT123_GDS3_NTAT','SBT124_GDS3_NTAT','GOESE_WV','GOESE_IR']: data = data -273.15 # K -> C elif array in ['HAIL_MAXK1', 'HAIL_MAX2D']: data = data*39.3701 # m -> inches From 8dd64768234450848f0083058849d76dd90b9c67 Mon Sep 17 00:00:00 2001 From: ahijevyc Date: Wed, 10 Apr 2019 15:08:24 -0600 Subject: [PATCH 16/68] get original grid from WRF Mask speed_kts beyond 300 nm Only warn about anti-cyclonic vmax if vmax > 17 m/s. --- atcf.py | 129 ++++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 97 insertions(+), 32 deletions(-) diff --git a/atcf.py b/atcf.py index f1eb336..9de784c 100644 --- a/atcf.py +++ b/atcf.py @@ -1,6 +1,7 @@ import pandas as pd import pdb import re +import pint import csv import os, sys from netCDF4 import Dataset @@ -247,6 +248,9 @@ def read(ifile = ifile, debug=False, fullcircle=False): df['rad3'] = 0 df['rad4'] = 0 + + # Tried converting to MultiIndex DataFrame but it led to all sorts of problems. + return df def x2s(x): @@ -305,23 +309,35 @@ def dist_bearing(lon1,lons,lat1,lats): def get_max_ext_of_wind(speed_kts, distance_km, bearing, raw_vmax_kts, quads=quads, thresh_kts=thresh_kts, debug=False): + # speed_kts is converted to masked array. Masked where distance >= 300 nm rad_nm = {} # Put in dictionary "rad_nm" where rad_nm = { # 34: {'NE':rad1, 'SE':rad2, 'SW':rad3, 'NW':rad4}, # 50: {'NE':rad1, 'SE':rad2, 'SW':rad3, 'NW':rad4}, # 64: {'NE':rad1, 'SE':rad2, 'SW':rad3, 'NW':rad4} # } + + rad_nm['raw_vmax_kts'] = raw_vmax_kts rad_nm['thresh_kts'] = thresh_kts rad_nm['quads'] = quads + + # Originally had distance_km < 800, but Chris D. suggested 300nm in Sep 2018 email + # This was to deal with Irma and the unrelated 34 knot onshore flow in Georgia + # Looking at HURDAT2 R34 sizes (since 2004), ex-tropical storm Karen 2015 had 710nm. + # Removing EX storms, the max was 480 nm in Hurricane Sandy 2012 + speed_kts = np.ma.array(speed_kts, mask = distance_km >= 300./km2nm) + for wind_thresh_kts in thresh_kts[thresh_kts < raw_vmax_kts]: + ithresh = speed_kts >= wind_thresh_kts + # warn if max_dist_of_wind_threshold is on edge of domain + imax = np.argmax(distance_km * ithresh) + iedge = np.unravel_index(imax, distance_km.shape) + if iedge[0] == distance_km.shape[0]-1 or iedge[1] == distance_km.shape[1]-1 or any(iedge) == 0: + print("get_max_ext_of_wind(): R"+str(wind_thresh_kts)+" at edge of domain",iedge,"shape:",distance_km.shape) rad_nm[wind_thresh_kts] = {} for quad,az in quads.items(): - # Originally had distance_km < 800, but Chris D. suggested 300nm in Sep 2018 email - # This was to deal with Irma and the unrelated 34 knot onshore flow in Georgia - # Looking at HURDAT2 R34 sizes (since 2004), ex-tropical storm Karen 2015 had 710nm. - # Removing EX storms, the max was 480 nm in Hurricane Sandy 2012 - iquad = (az <= bearing) & (bearing < az+90) & (speed_kts >= wind_thresh_kts) & (distance_km < 300./km2nm) + iquad = (az <= bearing) & (bearing < az+90) & (speed_kts >= wind_thresh_kts) rad_nm[wind_thresh_kts][quad] = 0 if np.sum(iquad) > 0: max_dist_of_wind_threshold = np.max(distance_km[iquad]) * km2nm @@ -334,7 +350,6 @@ def get_max_ext_of_wind(speed_kts, distance_km, bearing, raw_vmax_kts, quads=qua def derived_winds(u10, v10, mslp, lonCell, latCell, row, vmax_search_radius=250., mslp_search_radius=100., debug=False): - # Given a row (with row.lon and row.lat)... # Derive cell distances and bearings @@ -353,15 +368,15 @@ def derived_winds(u10, v10, mslp, lonCell, latCell, row, vmax_search_radius=250. if row.lat < 0: Vt = -Vt - # Restrict Vmax search + # Restrict Vmax search to a certain radius (vmax_search_radius) vmaxrad = distance_km < vmax_search_radius ispeed_max = np.argmax(speed_kts[vmaxrad]) raw_vmax_kts = speed_kts[vmaxrad].max() - # Check if tangential component of max wind is negative (anti-cyclonic) - if Vt[vmaxrad][ispeed_max] < 0: + # If vmax > 17, check if tangential component of max wind is negative (anti-cyclonic) + if row.vmax > 17 and Vt[vmaxrad][ispeed_max] < 0: print("center", row.valid_time, row.lat, row.lon) - print("max wind is anti-cyclonic!", Vt[vmaxrad][ispeed_max]) + print("max wind is anti-cyclonic! (unknown units)", Vt[vmaxrad][ispeed_max]) print("max wind lat/lon", latCell[vmaxrad][ispeed_max], lonCell[vmaxrad][ispeed_max]) print("max wind U/V", u10[vmaxrad][ispeed_max], v10[vmaxrad][ispeed_max]) if debug: pdb.set_trace() @@ -400,16 +415,23 @@ def add_wind_rad_lines(row, rad_nm, fullcircle=False, debug=False): for thresh in thresh_kts[thresh_kts < raw_vmax_kts]: if any(rad_nm[thresh].values()): newrow = row.copy() - newrow[['windcode','rad','rad1','rad2','rad3','rad4']] = ['NEQ', thresh, rad_nm[thresh]['NE'], rad_nm[thresh]['SE'], rad_nm[thresh]['SW'], rad_nm[thresh]['NW']] - # Append row with 34, 50, or 64 knot radii - lines = lines.append(newrow) + newrow.rad = thresh if fullcircle: # Append row with full circle 34, 50, or 64 knot radius # MET-TC will not derive this on its own - see email from John Halley-Gotway Oct 11, 2018 # Probably shouldn't have AAA and NEQ in same file. - newrow = row.copy() - newrow[['windcode','rad','rad1']] = ['AAA',thresh, np.nanmax(list(rad_nm[thresh].values()))] - lines = lines.append(newrow) + newrow[['windcode','rad1']] = ['AAA',np.nanmax(list(rad_nm[thresh].values()))] + newrow[['rad2','rad3','rad4']] = np.nan + else: + newrow['windcode'] = 'NEQ' + newrow[['rad1','rad2','rad3','rad4']] = [ + rad_nm[thresh]['NE'], + rad_nm[thresh]['SE'], + rad_nm[thresh]['SW'], + rad_nm[thresh]['NW'] + ] + # Append row with 34, 50, or 64 knot radii + lines = lines.append(newrow) return lines @@ -419,31 +441,30 @@ def update_df(df, row, raw_vmax_kts, raw_RMW_nm, raw_minp, rad_nm, debug=False): # Called by origgrid and origmesh if debug: - print('before', row[['valid_time','lon','lat', 'vmax', 'minp', 'rmw']]) - row.vmax = raw_vmax_kts - row.minp = raw_minp - row.rmw = raw_RMW_nm + print("atcf.update_df: before update_df\n", row[['valid_time','lon','lat', 'vmax', 'minp', 'rmw']]) + row["vmax"] = raw_vmax_kts + row["minp"] = raw_minp + row["rmw"] = raw_RMW_nm # Add note of original mesh = True in user data (not defined) column - row.userdata += 'origmeshTrue' + if 'origmeshTrue' not in row.userdata: + if debug: + print(row, " is already from original mesh.") + row.userdata += 'origmeshTrue' if debug: print('after', row[['vmax', 'minp', 'rmw']]) - # Can't get rid of SettingWithCopyWarning! df.loc[row.name,:] = row - # Append 34/50/64 knot lines to DataFrame newlines = add_wind_rad_lines(row, rad_nm, debug=debug) - # If there are new lines, drop the old one and append new ones. + # If there are new lines, drop the old one and append new ones. if not newlines.empty: + if debug: + print("dropping", row.name) df.drop(row.name, inplace=True) - df = df.append(newlines) - - - - # Sort DataFrame by index (deal with appended wind radii lines) - # sort by rad too - df = df.sort_index().sort_values(['initial_time','fhr','rad']) + if debug: + print("appending ", newlines) + df = df.append(newlines, sort=False) return df @@ -527,7 +548,7 @@ def write(ofile, df, fullcircle=False, debug=False): # replace ', XX,' with ', XX,' # replace 'nan' with ' ' junk = [j.replace(', ', ', ', 3).replace(', XX,',', XX,').replace('nan',' ') for j in junk] - #delete space before windcode + #delete space before windcode (e.g. NEQ) junk = [j[:68]+j[69:] for j in junk] #delete space before initials junk = [j[:133]+j[134:] for j in junk] @@ -546,6 +567,49 @@ def write(ofile, df, fullcircle=False, debug=False): f.close() print("wrote", ofile) +def origgridWRF(df, griddir, grid="d03", debug=False): + # Get vmax, minp, radius of max wind, max radii of wind thresholds from WRF by Alex Kowaleski + + WRFmember = df.model.str.extract(r'WF(\d\d)', flags=re.IGNORECASE) + # column 0 will have match or null + if pd.isnull(WRFmember[0]).any(): + if debug: + print('Assuming WRF ensemble member, but not all model strings match WF\d\d') + print(df) + sys.exit(1) + ens = int(WRFmember[0][0]) # strip leading zero + if ens < 1: + sys.exit(2) + for index, row in df.iterrows(): + gridfile = "EPS_"+str(ens)+"/"+ "E"+str(ens)+"_"+row.initial_time.strftime('%m%d%H') + \ + "_"+grid+"_"+ row.valid_time.strftime('%Y-%m-%d_%H:%M:%S') +"_ll.nc" + print('opening ' + griddir + gridfile) + nc = Dataset(griddir + gridfile, "r") + lon = nc.variables['lon'][:] + lat = nc.variables['lat'][:] + lonCell,latCell = np.meshgrid(lon, lat) + iTime = 0 + u10 = nc.variables['u10'][iTime,:,:] + v10 = nc.variables['v10'][iTime,:,:] + mslpvar = nc.variables['slp'] + if mslpvar.units != 'hPa': + print("atcf.origgridWRF: unexpected units for mslp: "+mslpvar.units) + sys.exit(1) + mslp = mslpvar[iTime,:,:] * 100. + print('closing ' + griddir + gridfile) + nc.close() + + if debug: + print("Extract vmax, RMW, minp, and radii of wind thresholds from row", row.name) + raw_vmax_kts, raw_RMW_nm, raw_minp, rad_nm = derived_winds(u10, v10, mslp, lonCell, latCell, row, debug=debug) + df = update_df(df, row, raw_vmax_kts, raw_RMW_nm, raw_minp, rad_nm, debug=debug) + + # Sort DataFrame by index (deal with appended wind radii lines) + # sort by rad too + df = df.sort_index().sort_values(['initial_time','fhr','rad']) + return df + + def origgrid(df, griddir, debug=False): # Get vmax, minp, radius of max wind, max radii of wind thresholds from ECMWF grid, not from tracker. @@ -555,6 +619,7 @@ def origgrid(df, griddir, debug=False): # ECMWF ensemble member in directory named "ens_xx" (where xx is the 2-digit ensemble member). # File path is "ens_xx/${gs}yyyymmddhh.xx.nc", where ${gs} is the grid spacing (0p15, 0p25, or 0p5). + # TODO: Why group rows by initial_time and model? Why not process each row independently? for run_id, group in df.groupby(['initial_time', 'model']): initial_time, model = run_id m = re.search(r'EE(\d\d)', model) From 0d1f7648a28ae7ef615314f3dfe0592b6a30ebae Mon Sep 17 00:00:00 2001 From: ahijevyc Date: Wed, 10 Apr 2019 15:11:56 -0600 Subject: [PATCH 17/68] field names and levels for MPAS ensemble hwt2017 --- fieldinfo.py | 132 +++++++++++++++++++++------------------------------ 1 file changed, 53 insertions(+), 79 deletions(-) diff --git a/fieldinfo.py b/fieldinfo.py index ea462e8..b1bc4dc 100755 --- a/fieldinfo.py +++ b/fieldinfo.py @@ -30,85 +30,71 @@ def readNCLcm(name): fieldinfo = { # surface and convection-related entries - 'precip' :{ 'levels' : [0,0.01,0.05,0.1,0.2,0.3,0.4,0.5,0.75,1,1.5,2,2.5,3.0], 'cmap': [readNCLcm('precip2_17lev')[i] for i in (0,1,2,4,5,6,7,8,10,12,13,14,15)], 'fname':['PREC_ACC_NC'] }, -# 'precip-24hr' :{ 'levels' : [0,0.01,0.05,0.1,0.25,0.5,0.75,1.0,1.25,1.5,2.0,3.0,4.0,5.0,6.0,7.0], 'cmap': [readNCLcm('precip2_17lev')[i] for i in (0,1,2,4,5,6,7,8,10,12,13,14,15,16,17)], 'fname':['PREC_ACC_NC'] }, - 'precip-24hr' :{ 'levels' : [0,0.01,0.05,0.1,0.25,0.5,0.75,1.0,1.25,1.5,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0,12.0,13.0], 'cmap': [readNCLcm('precip2_17lev')[i] for i in (0,1,2,4,5,6,7,8,10,12,13,14,15,16,17)]+['#777777', '#AAAAAA', '#CCCCCC', '#EEEEEE']+[readNCLcm('sunshine_diff_12lev')[i] for i in (4,2,1)], 'fname':['PREC_ACC_NC'] }, -# 'precipacc' :{ 'levels' : [0,0.01,0.05,0.1,0.25,0.5,0.75,1.0,1.25,1.5,2.0,3.0,4.0,5.0,6.0,7.0], 'cmap': [readNCLcm('precip2_17lev')[i] for i in (0,1,2,4,5,6,7,8,10,12,13,14,15,16,17)], 'fname':['RAINNC'] }, - 'precipacc':{ 'levels' : [0,0.01,0.05,0.1,0.25,0.5,0.75,1.0,1.25,1.5,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0,12.0,13.0], 'cmap': [readNCLcm('precip2_17lev')[i] for i in (0,1,2,4,5,6,7,8,10,12,13,14,15,16,17)]+['#777777', '#AAAAAA', '#CCCCCC', '#EEEEEE']+[readNCLcm('sunshine_diff_12lev')[i] for i in (4,2,1)], 'fname':['RAINNC'] }, + 'precip' :{ 'levels' : [0,0.01,0.05,0.1,0.2,0.3,0.4,0.5,0.75,1,1.5,2,2.5,3.0], 'cmap': [readNCLcm('precip2_17lev')[i] for i in (0,1,2,4,5,6,7,8,10,12,13,14,15)], 'fname':['rainnc'] }, + 'precip-24hr' :{ 'levels' : [0,0.01,0.05,0.1,0.25,0.5,0.75,1.0,1.25,1.5,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0,12.0,13.0], 'cmap': [readNCLcm('precip2_17lev')[i] for i in (0,1,2,4,5,6,7,8,10,12,13,14,15,16,17)]+['#777777', '#AAAAAA', '#CCCCCC', '#EEEEEE']+[readNCLcm('sunshine_diff_12lev')[i] for i in (4,2,1)], 'fname':['rainnc'] }, + 'precipacc':{ 'levels' : [0,0.01,0.05,0.1,0.25,0.5,0.75,1.0,1.25,1.5,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0,12.0,13.0], 'cmap': [readNCLcm('precip2_17lev')[i] for i in (0,1,2,4,5,6,7,8,10,12,13,14,15,16,17)]+['#777777', '#AAAAAA', '#CCCCCC', '#EEEEEE']+[readNCLcm('sunshine_diff_12lev')[i] for i in (4,2,1)], 'fname':['rainnc'] }, 'sbcape' :{ 'levels' : [100,250,500,750,1000,1250,1500,1750,2000,2500,3000,3500,4000,4500,5000,5500,6000], - 'cmap' : ['#eeeeee', '#dddddd', '#cccccc', '#aaaaaa']+readNCLcm('precip2_17lev')[3:-1], 'fname': ['SBCAPE'], 'filename':'upp' }, + 'cmap' : ['#eeeeee', '#dddddd', '#cccccc', '#aaaaaa']+readNCLcm('precip2_17lev')[3:-1], 'fname': ['sbcape'], 'filename':'diag' }, 'mlcape' :{ 'levels' : [100,250,500,750,1000,1250,1500,1750,2000,2500,3000,3500,4000,4500,5000,5500,6000], - 'cmap' : ['#eeeeee', '#dddddd', '#cccccc', '#aaaaaa']+readNCLcm('precip2_17lev')[3:-1], 'fname': ['MLCAPE'], 'filename':'upp' }, - 'mucape' :{ 'levels' : [100,250,500,750,1000,1250,1500,1750,2000,2500,3000,3500,4000,4500,5000,5500,6000], - 'cmap' : ['#eeeeee', '#dddddd', '#cccccc', '#aaaaaa']+readNCLcm('precip2_17lev')[3:-1], 'fname': ['MUCAPE'], 'filename':'upp' }, - 'sbcinh' :{ 'levels' : [50,75,100,150,200,250,500], 'cmap': readNCLcm('topo_15lev')[1:], 'fname': ['SBCINH'], 'filename':'upp' }, - 'mlcinh' :{ 'levels' : [50,75,100,150,200,250,500], 'cmap': readNCLcm('topo_15lev')[1:], 'fname': ['MLCINH'], 'filename':'upp' }, + 'cmap' : ['#eeeeee', '#dddddd', '#cccccc', '#aaaaaa']+readNCLcm('precip2_17lev')[3:-1], 'fname': ['mlcape'], 'filename':'diag' }, + 'mucape' :{ 'levels' : [10,25,50,100,250,500,750,1000,1250,1500,1750,2000,2500,3000,3500,4000,4500,5000,5500,6000], + 'cmap' : ['#eeeeee', '#dddddd', '#cccccc', '#aaaaaa']+readNCLcm('precip2_17lev')[3:-1], 'fname': ['mucape'], 'filename':'diag' }, + 'sbcinh' :{ 'levels' : [50,75,100,150,200,250,500], 'cmap': readNCLcm('topo_15lev')[1:], 'fname': ['sbcin'], 'filename':'diag' }, + 'mlcinh' :{ 'levels' : [50,75,100,150,200,250,500], 'cmap': readNCLcm('topo_15lev')[1:], 'fname': ['mlcin'], 'filename':'diag' }, 'pwat' :{ 'levels' : [0.25,0.5,0.75,1.0,1.25,1.5,1.75,2.0,2.5,3.0,3.5,4.0], 'cmap' : ['#dddddd', '#cccccc', '#e1e1d3', '#e1d5b1', '#ffffd5', '#e5ffa7', '#addd8e', '#41ab5d', '#007837', '#005529', '#0029b1'], - 'fname' : ['PWAT'], 'filename':'upp'}, - 'hailk1' :{ 'levels' : [0.25,0.5,0.75,1.0,1.25,1.5,1.75,2.0,2.5,3.0,3.5,4.0], 'cmap' : ['#eeeeee', '#dddddd', '#cccccc', '#aaaaaa']+readNCLcm('precip2_17lev')[3:-1], 'fname':['HAIL_MAXK1'], 'filename': 'diag' }, - 'hail2d' :{ 'levels' : [0.25,0.5,0.75,1.0,1.25,1.5,1.75,2.0,2.5,3.0,3.5,4.0], 'cmap' : ['#eeeeee', '#dddddd', '#cccccc', '#aaaaaa']+readNCLcm('precip2_17lev')[3:-1], 'fname':['HAIL_MAX2D'], 'filename': 'diag' }, - 'afhail' :{ 'levels' : [0.25,0.5,0.75,1.0,1.25,1.5,1.75,2.0,2.5,3.0,3.5,4.0], 'cmap' : ['#eeeeee', '#dddddd', '#cccccc', '#aaaaaa']+readNCLcm('precip2_17lev')[3:-1], 'fname': ['HAILCAST_DIAM_MAX'], 'filename':'diag' }, - 't2' :{ 'levels' : [-35,-30,-25,-20,-15,-10,-5,0,5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100,105,110,115,120], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['T2'] }, - 't2depart' :{ 'levels' : [-35,-30,-25,-20,-15,-10,-5,0,5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100,105,110,115,120], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['T2'] }, - 'mslp' :{ 'levels' : [960,964,968,972,976,980,984,988,992,996,1000,1004,1008,1012,1016,1020,1024,1028,1032,1036,1040,1044,1048,1052], 'cmap':readNCLcm('nice_gfdl')[3:193], 'fname':['MSLP'], 'filename': 'upp' }, - 'mslp-tc' :{ 'levels' : [908,912,916,920,924,928,932,936,940,944,948,952,956,960,964,968,972,976,980,984,988,992,996,1000,1004,1008,1012,1016,1020,1024,1028,1032,1036,1040,1044,1048,1052], 'cmap':readNCLcm('nice_gfdl')[3:193], 'fname':['MSLP'], 'filename': 'upp' }, - 'td2' :{ 'levels' : [-20,-15,-10,-5,0,5,10,15,20,25,30,35,40,45,50,55,60,64,68,72,76,80,84], - 'cmap':['#f3e7e8','#d0a0a4','#ad5960', '#8b131d', '#8b4513','#ad7c59', '#c5a289','#dcc7b8','#eeeeee', '#dddddd', '#bbbbbb', '#e1e1d3', '#e1d5b1','#ccb77a','#ffffe5','#f7fcb9', '#addd8e', '#41ab5d', '#006837', '#004529', '#195257', '#4c787c'], + 'fname' : ['precipw'], 'filename':'diag'}, + 't2' :{ 'levels' : [-35,-30,-25,-20,-15,-10,-5,0,5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100,105,110,115,120], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['t2m'], 'filename':'diag' }, + 't2depart' :{ 'levels' : [-35,-30,-25,-20,-15,-10,-5,0,5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100,105,110,115,120], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['t2m'] }, + #'t2' :{ 'levels' : [-35,-30,-25,-20,-15,-10,-5,0,5,10,15,20,25,30,32,35,40,45,50,55,60,65,70,75,80,85,90,95,100,105,110,115,120], \ + #'cmap': readNCLcm('MPL_Greys')[20:121:15][::-1]+[readNCLcm('nice_gfdl')[i] for i in (40,50,60,65,70,75,85,100,110,115,120,130,135,140,145,150,155,167,177,190,197)]+readNCLcm('MPL_copper')[20:120:20][::-1], 'fname': ['t2m'] }, + 't2-0c' :{ 'levels' : [32], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['t2m'] }, + 'mslp' :{ 'levels' : [960,964,968,972,976,980,984,988,992,996,1000,1004,1008,1012,1016,1020,1024,1028,1032,1036,1040,1044,1048,1052], 'cmap':readNCLcm('nice_gfdl')[3:193], 'fname':['mslp'], 'filename': 'diag' }, + 'td2' :{ 'levels' : [-35,-30,-25,-20,-15,-10,-5,0,5,10,15,20,25,30,35,40,45,50,55,60,64,68,72,76,80,84], + 'cmap':['#ad598a', '#c589ac','#dcb8cd','#e7cfd1','#d0a0a4','#ad5960', '#8b131d', '#8b4513','#ad7c59', '#c5a289','#dcc7b8','#eeeeee', '#dddddd', '#bbbbbb', '#e1e1d3', '#e1d5b1','#ccb77a','#ffffe5','#f7fcb9', '#addd8e', '#41ab5d', '#006837', '#004529', '#195257', '#4c787c'], + #'cmap':['#f3e7e8','#d0a0a4','#ad5960', '#8b131d', '#96572a','#ad7c59', '#c5a289','#dcc7b8','#eeeeee', '#dddddd', '#bbbbbb', '#e1e1d3', '#e1d5b1','#ccb77a','#ffffe5','#f7fcb9', '#addd8e', '#41ab5d', '#006837', '#004529', '#195257', '#4c787c'], 'fname' : ['DEWPOINT_2M'], 'filename':'upp'}, - 'td2depart' :{ 'levels' : [-20,-15,-10,-5,0,5,10,15,20,25,30,35,40,45,50,55,60,64,68,72,76,80,84], - 'cmap':['#f3e7e8','#d0a0a4','#ad5960', '#8b131d', '#8b4513','#ad7c59', '#c5a289','#dcc7b8','#eeeeee', '#dddddd', '#bbbbbb', '#e1e1d3', '#e1d5b1','#ccb77a','#ffffe5','#f7fcb9', '#addd8e', '#41ab5d', '#006837', '#004529', '#195257', '#4c787c'], + #'td2' :{ 'levels' : [20,25,30,35,40,45,50,55,60,64,68,72,76,80,84], 'cmap' : ['#eeeeee', '#dddddd', '#bbbbbb', '#e1e1d3', '#e1d5b1','#ccb77a','#ffffe5','#f7fcb9', '#addd8e', '#41ab5d', '#006837', '#004529', '#195257', '#4c787c'], + # 'fname' : ['DEWPOINT_2M'], 'filename':'upp'}, + 'td2depart' :{ 'levels' : [20,25,30,35,40,45,50,55,60,64,68,72,76,80,84], 'cmap' : ['#eeeeee', '#dddddd', '#bbbbbb', '#e1e1d3', '#e1d5b1','#ccb77a','#ffffe5','#f7fcb9', '#addd8e', '#41ab5d', '#006837', '#004529', '#195257', '#4c787c'], 'fname' : ['DEWPOINT_2M'], 'filename':'upp'}, - 'thetae' :{ 'levels' : [300,305,310,315,320,325,330,335,340,345,350,355,360], 'cmap': ['#eeeeee', '#dddddd', '#cccccc', '#aaaaaa']+readNCLcm('precip2_17lev')[3:-1], 'fname' : ['T2', 'Q2', 'PSFC'], 'filename': 'diag'}, - 'rh2m' :{ 'levels' : [0,10,20,30,40,50,60,70,80,90,100,110], 'cmap': readNCLcm('precip2_17lev')[:17][::-1], 'fname': ['T2', 'PSFC', 'Q2'], 'filename': 'diag'}, - 'heatindex' :{ 'levels' : [65,70,75,80,85,90,95,100,105,110,115,120,125,130], 'cmap': readNCLcm('MPL_hot')[::-1], 'fname': ['AFWA_HEATIDX'], 'filename':'diag' }, -# 'pblh' :{ 'levels' : [0,250,500,750,1000,1250,1500,1750,2000,2250,2500,2750,3000], 'cmap': readNCLcm('precip2_17lev')[3:-1], 'fname': ['C_PBLH'], 'filename':'diag' }, + 'thetae' :{ 'levels' : [300,305,310,315,320,325,330,335,340,345,350,355,360], 'cmap': ['#eeeeee', '#dddddd', '#cccccc', '#aaaaaa']+readNCLcm('precip2_17lev')[3:-1], 'fname' : ['t2m', 'q2', 'surface_pressure'], 'filename': 'diag'}, + 'rh2m' :{ 'levels' : [0,10,20,30,40,50,60,70,80,90,100,110], 'cmap': readNCLcm('precip2_17lev')[:17][::-1], 'fname': ['t2m', 'surface_pressure', 'q2'], 'filename': 'diag'}, 'pblh' :{ 'levels' : [0,250,500,750,1000,1250,1500,1750,2000,2500,3000,3500,4000], - 'cmap': ['#eeeeee', '#dddddd', '#cccccc', '#bbbbbb', '#44aaee','#88bbff', '#aaccff', '#bbddff', '#efd6c1', '#e5c1a1', '#eebb32', '#bb9918'], 'fname': ['PBLH'] }, - 'hmuh' :{ 'levels' : [10,25,50,75,100,125,150,175,200,250,300,400,500], 'cmap': readNCLcm('prcp_1')[1:15], 'fname': ['UP_HELI_MAX'], 'filename':'diag'}, - 'hmuh-neg' :{ 'levels' : [10,25,50,75,100,125,150,175,200,250,300,400,500], 'cmap': readNCLcm('prcp_1')[1:15], 'fname': ['UP_HELI_MIN'], 'filename':'diag'}, - 'hmuh03' :{ 'levels' : [10,25,50,75,100,125,150,175,200,250,300,400,500], 'cmap': readNCLcm('prcp_1')[1:15], 'fname': ['UP_HELI_MAX03'], 'filename':'diag'}, - 'rvort1' :{ 'levels' : [0.005,0.006,0.007,0.008,0.009,0.01,0.011,0.012,0.013,0.014,0.015], 'cmap': readNCLcm('prcp_1')[1:15], 'fname': ['RVORT1_MAX'], 'filename':'diag'}, - 'hmup' :{ 'levels' : [4,6,8,10,12,14,16,18,20,24,28,32,36,40,44,48], 'cmap': readNCLcm('prcp_1')[1:16], 'fname': ['W_UP_MAX'], 'filename':'diag' }, + 'cmap': ['#eeeeee', '#dddddd', '#cccccc', '#bbbbbb', '#44aaee','#88bbff', '#aaccff', '#bbddff', '#efd6c1', '#e5c1a1', '#eebb32', '#bb9918'], 'fname': ['hpbl'] }, + 'hmuh' :{ 'levels' : [10,25,50,75,100,125,150,175,200,250,300,400,500], 'cmap': readNCLcm('prcp_1')[1:15], 'fname': ['updraft_helicity_max'], 'filename':'diag'}, + 'hmneguh' :{ 'levels' : [10,25,50,75,100,125,150,175,200,250,300,400,500], 'cmap': readNCLcm('prcp_1')[1:15], 'fname': ['UP_HELI_MIN'], 'filename':'diag'}, + 'hmuh03' :{ 'levels' : [10,25,50,75,100,125,150,175,200,250,300,400,500], 'cmap': readNCLcm('prcp_1')[1:15], 'fname': ['updraft_helicity_max03'], 'filename':'diag'}, + 'hmuh01' :{ 'levels' : [5,10,25,50,75,100,125,150,175,200,250,300,400,500], 'cmap': readNCLcm('prcp_1')[1:15], 'fname': ['updraft_helicity_max01'], 'filename':'diag'}, + #'rvort1' :{ 'levels' : [0.005,0.006,0.007,0.008,0.009,0.01,0.012,0.014,0.016,0.018,0.02,0.021], 'cmap': readNCLcm('prcp_1')[:15], 'fname': ['rvort1_max'], 'filename':'diag'}, + 'rvort1' :{ 'levels' : [0.005,0.006,0.007,0.008,0.009,0.01,0.011,0.012,0.013,0.014,0.015], 'cmap': readNCLcm('prcp_1')[1:15], 'fname': ['rvort1_max'], 'filename':'diag'}, + 'sspf' :{ 'levels' : [10,25,50,75,100,125,150,175,200,250,300,400,500], 'cmap': readNCLcm('prcp_1')[:15], 'fname': ['updraft_helicity_max','WSPD10MAX','HAIL_MAXK1'], 'filename':'diag'}, + 'hmup' :{ 'levels' : [4,6,8,10,12,14,16,18,20,24,28,32,36,40,44,48], 'cmap': readNCLcm('prcp_1')[1:16], 'fname': ['w_velicity_max'], 'filename':'diag' }, #'hmdn' :{ 'levels' : [-19,-17,-15,-13,-11,-9,-7,-5,-3,-1,0], 'cmap': readNCLcm('prcp_1')[16:1:-1]+['#ffffff'], 'fname': ['W_DN_MAX'], 'filename':'diag' }, 'hmdn' :{ 'levels' : [2,3,4,6,8,10,12,14,16,18,20,22,24,26,28,30], 'cmap': readNCLcm('prcp_1')[1:16], 'fname': ['W_DN_MAX'], 'filename':'diag' }, 'hmwind' :{ 'levels' : [10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42], 'cmap': readNCLcm('prcp_1')[:16], 'fname': ['WSPD10MAX'], 'filename':'diag' }, + #'hmwind' :{ 'levels' : [10,12,14,16,18,20,22,24,26,28,30,32,34], 'cmap': readNCLcm('prcp_1')[1:15], 'fname': ['WSPD10MAX'], 'filename':'diag' }, #'hmwind' :{ 'levels' : [20,25,30,35,40,45,50,55,60,65,70,75,80,85], 'cmap': readNCLcm('prcp_1')[1:16], 'fname': ['WSPD10MAX'], 'filename':'diag' }, 'hmgrp' :{ 'levels' : [0.01,0.1,0.25,0.5,0.75,1.0,1.5,2.0,2.5,3.0,4.0,5.0], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['GRPL_MAX'], 'filename':'diag' }, -# 'cref' :{ 'levels' : [5,10,15,20,25,30,35,40,45,50,55,60,65,70], 'cmap': readcm('cmap_rad.rgb')[2:16], 'fname': ['REFL_10CM'], 'arraylevel':'max' }, - 'cref' :{ 'levels' : [5,10,15,20,25,30,35,40,45,50,55,60,65,70], 'cmap': readcm('cmap_rad.rgb')[1:14], 'fname': ['REFL_MAX_COL'], 'filename':'upp' }, + #'cref' :{ 'levels' : [5,10,15,20,25,30,35,40,45,50,55,60,65,70], 'cmap': readcm('cmap_rad.rgb')[1:14], 'fname': ['REFL_10CM'], 'arraylevel':'max' }, + 'cref' :{ 'levels' : [5,10,15,20,25,30,35,40,45,50,55,60,65,70], 'cmap': readcm('cmap_rad.rgb')[1:14], 'fname': ['REFD_COM'], 'filename':'diag' }, 'lmlref' :{ 'levels' : [5,10,15,20,25,30,35,40,45,50,55,60,65,70], 'cmap': readcm('cmap_rad.rgb')[1:14], 'fname': ['REFL_10CM'], 'arraylevel':0 }, - 'ref1km' :{ 'levels' : [5,10,15,20,25,30,35,40,45,50,55,60,65,70], 'cmap': readcm('cmap_rad.rgb')[1:14], 'fname': ['REFL_1KM_AGL'], 'filename':'upp' }, + #'ref1km' :{ 'levels' : [5,10,15,20,25,30,35,40,45,50,55,60,65,70], 'cmap': readcm('cmap_rad.rgb')[1:14], 'fname': ['REFL_1KM_AGL'], 'filename':'upp' }, + 'ref1km' :{ 'levels' : [5,10,15,20,25,30,35,40,45,50,55,60,65,70], 'cmap': readcm('cmap_rad.rgb')[1:14], 'fname': ['refl10cm_max'], 'filename':'diag' }, 'echotop' :{ 'levels' : [1000,5000,10000,15000,20000,25000,30000,35000,40000,45000,50000,55000,60000,65000], 'cmap': readNCLcm('precip3_16lev')[1::], 'fname': ['ECHOTOP'], 'filename':'diag' }, - 'srh3' :{ 'levels' : [50,100,150,200,250,300,400,500], 'cmap': readNCLcm('perc2_9lev'), 'fname': ['SR_HELICITY_3KM'], 'filename' : 'upp' }, - 'srh1' :{ 'levels' : [50,100,150,200,250,300,400,500], 'cmap': readNCLcm('perc2_9lev'), 'fname': ['SR_HELICITY_1KM'], 'filename' : 'upp' }, + 'srh3' :{ 'levels' : [50,100,150,200,250,300,400,500], 'cmap': readNCLcm('perc2_9lev'), 'fname': ['srh_0_3km'], 'filename' : 'upp' }, + 'srh1' :{ 'levels' : [50,100,150,200,250,300,400,500], 'cmap': readNCLcm('perc2_9lev'), 'fname': ['srh_0_1km'], 'filename' : 'upp' }, 'shr06mag' :{ 'levels' : [30,35,40,45,50,55,60,65,70,75,80], 'cmap': readNCLcm('perc2_9lev'), 'fname': ['UBSHR6', 'VBSHR6'], 'filename':'upp' }, 'shr01mag' :{ 'levels' : [10,15,20,25,30,35,40,45,50,55], 'cmap': readNCLcm('perc2_9lev'), 'fname': ['UBSHR1', 'VBSHR1'], 'filename':'upp' }, - 'zlfc' :{ 'levels' : [0,250,500,750,1000,1250,1500,2000,2500,3000,3500,4000,5000], 'cmap': [readNCLcm('nice_gfdl')[i] for i in [3,20,37,54,72,89,106,123,141,158,175,193]], 'fname': ['AFWA_ZLFC'], 'filename':'diag' }, - 'zlcl' :{ 'levels' : [0,250,500,750,1000,1250,1500,2000,2500,3000,3500,4000,5000], 'cmap': [readNCLcm('nice_gfdl')[i] for i in [3,20,37,54,72,89,106,123,141,158,175,193]], 'fname': ['LCL_HEIGHT'], 'filename':'upp' }, + 'zlfc' :{ 'levels' : [0,250,500,750,1000,1250,1500,2000,2500,3000,3500,4000,5000], 'cmap': [readNCLcm('nice_gfdl')[i] for i in [3,20,37,54,72,89,106,123,141,158,175,193]], 'fname': ['lfc'], 'filename':'diag' }, + 'zlcl' :{ 'levels' : [0,250,500,750,1000,1250,1500,2000,2500,3000,3500,4000,5000], 'cmap': [readNCLcm('nice_gfdl')[i] for i in [3,20,37,54,72,89,106,123,141,158,175,193]], 'fname': ['lcl'], 'filename':'diag' }, 'ltg1' :{ 'levels' : [0.1,0.5,1,1.5,2,2.5,3,4,5,6,7,8,10,12], 'cmap': readNCLcm('prcp_1')[:15], 'fname': ['LTG1_MAX'], 'filename':'diag' }, 'ltg2' :{ 'levels' : [0.1,0.5,1,1.5,2,2.5,3,4,5,6,7,8,10,12], 'cmap': readNCLcm('prcp_1')[:15], 'fname': ['LTG2_MAX'], 'filename':'diag' }, 'ltg3' :{ 'levels' : [0.1,0.5,1,1.5,2,2.5,3,4,5,6,7,8,10,12], 'cmap': readNCLcm('prcp_1')[:15], 'fname': ['LTG3_MAX'], 'filename':'diag' }, - 'liftidx' :{ 'levels' : [-14,-12,-10,-8,-6,-4,-2,0,2,4,6,8], 'cmap': readNCLcm('nice_gfdl')[193:3:-1]+['#ffffff'], 'fname': ['SFC_LI'], 'filename':'upp'}, 'bmin' :{ 'levels' : [-20,-16,-12,-10,-8,-6,-4,-2,-1,-0.5,0,0.5], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['MLBMIN'], 'filename':'upp' }, 'pbmin' :{ 'levels' : [0,30,60,90,120,150,180],'cmap': ['#dddddd', '#aaaaaa']+readNCLcm('precip2_17lev')[3:-1], 'fname': ['MLPBMIN','PBMIN_SFC'], 'filename':'upp' }, - 'goesch3' :{ 'levels' : [-80,-78,-76,-74,-72,-70,-68,-66,-64,-62,-60,-58,-56,-54,-52,-50,-48,-46,-44,-42,-40,-38,-36,-34,-32,-30,-28,-26,-24,-22,-20,-18,-16,-14,-12,-10], 'cmap': readcm('cmap_sat2.rgb')[38:1:-1], 'fname': ['GOESE_WV'], 'filename':'upp' }, - 'goesch4' :{ 'levels' : [-80,-76,-72,-68,-64,-60,-56,-52,-48,-44,-40,-36,-32,-28,-24,-20,-16,-12,-8,-4,0,4,8,12,16,20,24,28,32,36,40], 'cmap': readcm('cmap_satir.rgb')[32:1:-1], 'fname': ['GOESE_IR'], 'filename':'upp' }, - #'afwavis' :{ 'levels' : [0.0,0.1,0.25,0.5,1.0,2.0,3.0,4.0,5.0,6.0,8.0,10.0,12.0], 'cmap': readNCLcm('nice_gfdl')[3:175]+['#ffffff'], 'fname': ['AFWA_VIS'], 'filename':'diag' }, - 'afwavis' :{ 'levels' : [0.0,0.1,0.25,0.5,1.0,2.0,3.0,4.0,5.0,6.0,8.0,10.0,12.0], 'cmap': readNCLcm('nice_gfdl')[3:175]+['#ffffff'], 'fname': ['VISIBILITY'], 'filename':'upp' }, # winter fields - 'snow' :{ 'levels' : [0.01,0.1,0.25,0.5,0.75,1,1.5,2,2.5,3,4,5], 'cmap':['#dddddd','#aaaaaa']+[readNCLcm('precip3_16lev')[i] for i in [1,3,5,8,10,12,14,16]]+['#FF99FF'], 'fname':['AFWA_SNOWFALL_HRLY'] }, # CSS mod - 'snow-6hr' :{ 'levels' : [0.25,0.5,0.75,1,2,3,4,5,6,8,10,12], 'cmap':['#dddddd','#aaaaaa']+[readNCLcm('precip3_16lev')[i] for i in [1,3,5,8,10,12,14,16]]+['#FF99FF'], 'fname':['AFWA_SNOWFALL_HRLY'] }, # CSS mod - 'snow-12hr' :{ 'levels' : [0.5,1,2,3,6,8,10,12,14,16,18,20], 'cmap':['#dddddd','#aaaaaa']+[readNCLcm('precip3_16lev')[i] for i in [1,3,5,8,10,12,14,16]]+['#FF99FF'], 'fname':['AFWA_SNOWFALL_HRLY'] }, # CSS mod - 'snow-24hr' :{ 'levels' : [1,3,6,8,10,12,15,18,21,24,30,36], 'cmap':['#dddddd','#aaaaaa']+[readNCLcm('precip3_16lev')[i] for i in [1,3,5,8,10,12,14,16]]+['#FF99FF'], 'fname':['AFWA_SNOWFALL_HRLY'] }, # CSS mod - 'snowacc' :{ 'levels' : [0.01,0.1,0.5,1,2,3,4,5,6,8,10,12,18,24,36,48,60], 'cmap':['#dddddd','#aaaaaa']+[readNCLcm('precip3_16lev')[i] for i in [1,2,3,4,5,6,8,10,11,12,13,15,16]]+['#FF99FF'], 'fname':['AFWA_SNOWFALL'], 'filename':'diag'}, # CSS mod colortable -# 'snowliq' :{ 'levels' : [0.1,0.5,1,1.5,2,2.5,3,4,5,6], 'cmap':readNCLcm('precip3_16lev')[1:], 'fname':['AFWA_SNOW'], 'filename':'diag'}, - 'ice' :{ 'levels' : [0.01,0.05,0.1,0.15,0.2,0.25,0.3,0.4,0.5,0.75,1,1.25], 'cmap':[readNCLcm('precip3_16lev')[i] for i in [1,2,3,4,5,6,8,10,11,12,13,15,16]], 'fname':['AFWA_ICE_HRLY'], 'filename':'wrfout'}, - 'iceacc' :{ 'levels' : [0.01,0.05,0.1,0.15,0.2,0.25,0.3,0.4,0.5,0.75,1,1.25], 'cmap':[readNCLcm('precip3_16lev')[i] for i in [1,2,3,4,5,6,8,10,11,12,13,15,16]], 'fname':['AFWA_ICE'], 'filename':'diag'}, - 'fzra' :{ 'levels' : [0.01,0.05,0.1,0.15,0.2,0.25,0.3,0.4,0.5,0.75,1,1.25], 'cmap':[readNCLcm('precip3_16lev')[i] for i in [1,2,3,4,5,6,8,10,11,12,13,15,16]], 'fname':['AFWA_FZRA_HRLY'] }, # CSS added, hrly - 'fzraacc' :{ 'levels' : [0.01,0.05,0.1,0.15,0.2,0.25,0.3,0.4,0.5,0.75,1,1.25], 'cmap':[readNCLcm('precip3_16lev')[i] for i in [1,2,3,4,5,6,8,10,11,12,13,15,16]], 'fname':['AFWA_FZRA'], 'filename':'diag'}, - 'windchill' :{ 'levels' : [-40,-35,-30,-25,-20,-15,-10,-5,0,5,10,15,20,25,30,35,40,45], 'cmap':readNCLcm('GMT_ocean')[20:], 'fname':['AFWA_WCHILL'], 'filename':'diag'}, 'freezelev' :{ 'levels' : [0, 500, 1000, 1500, 2000, 2500, 3000, 3500, 4000, 4500, 5000], 'cmap':readNCLcm('nice_gfdl')[3:193], 'fname':['FZLEV'], 'filename':'diag'}, 'thck1000-500' :{ 'levels' : [480,486,492,498,504,510,516,522,528,534,540,546,552,558,564,570,576,582,588,592,600], 'cmap':readNCLcm('perc2_9lev'), 'fname':['GHT_PL', 'GHT_PL'], 'arraylevel':[0,5], 'filename':'diag'}, # CSS mod 'thck1000-850' :{ 'levels' : [82,85,88,91,94,97,100,103,106,109,112,115,118,121,124,127,130,133,136,139,142,145,148,151,154,157,160], 'cmap':readNCLcm('perc2_9lev'), 'fname':['GHT_PL', 'GHT_PL'], 'arraylevel':[0,2], 'filename':'diag'}, # CSS mod @@ -116,8 +102,8 @@ def readNCLcm(name): # pressure level entries 'hgt250' :{ 'levels' : [9700,9760,9820,9880,9940,10000,10060,10120,10180,10240,10300,10360,10420,10480,10540,10600,10660,10720,10780,10840,10900,10960,11020], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['GHT_PL'], 'filename':'diag', 'arraylevel':8 }, 'hgt300' :{ 'levels' : [8400,8460,8520,8580,8640,8700,8760,8820,8880,8940,9000,9060,9120,9180,9240,9300,9360,9420,9480,9540,9600,9660,9720,9780,9840,9900,9960,10020], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['GHT_PL'], 'filename':'diag', 'arraylevel':7 }, - 'hgt500' :{ 'levels' : [4800,4860,4920,4980,5040,5100,5160,5220,5280,5340,5400,5460,5520,5580,5640,5700,5760,5820,5880,5940,6000], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['GHT_PL'], 'filename':'diag', 'arraylevel':5 }, - 'hgt700' :{ 'levels' : [2700,2730,2760,2790,2820,2850,2880,2910,2940,2970,3000,3030,3060,3090,3120,3150,3180,3210,3240,3270,3300], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['GHT_PL'], 'filename':'diag', 'arraylevel':3 }, + 'hgt500' :{ 'levels' : [4800,4860,4920,4980,5040,5100,5160,5220,5280,5340,5400,5460,5520,5580,5640,5700,5760,5820,5880,5940,6000], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['height_500hPa'], 'filename':'diag'}, + 'hgt700' :{ 'levels' : [2700,2730,2760,2790,2820,2850,2880,2910,2940,2970,3000,3030,3060,3090,3120,3150,3180,3210,3240,3270,3300], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['height_700hPa'], 'filename':'diag'}, 'hgt850' :{ 'levels' : [1200,1230,1260,1290,1320,1350,1380,1410,1440,1470,1500,1530,1560,1590,1620,1650], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['GHT_PL'], 'filename':'diag', 'arraylevel':2 }, 'hgt925' :{ 'levels' : [550,580,610,640,670,700,730,760,790,820,850,880,910,940,970,1000,1030], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['GHT_PL'], 'filename':'diag', 'arraylevel':1 }, 'speed250' :{ 'levels' : [10,20,30,40,50,60,70,80,90,100,110,120,130,140,150,160,170], 'cmap': readNCLcm('wind_17lev'), 'fname': ['S_PL'], 'filename':'diag', 'arraylevel':8 }, @@ -137,43 +123,30 @@ def readNCLcm(name): 'temp700' :{ 'levels' : [-36,-33,-30,-27,-24,-21,-18,-15,-12,-9,-6,-3,0,3,6,9,12,15,18,21], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['T_PL'], 'filename':'diag', 'arraylevel':3 }, 'temp850' :{ 'levels' : [-30,-27,-24,-21,-18,-15,-12,-9,-6,-3,0,3,6,9,12,15,18,21,24,27,30], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['T_PL'], 'filename':'diag', 'arraylevel':2 }, 'temp925' :{ 'levels' : [-24,-21,-18,-15,-12,-9,-6,-3,0,3,6,9,12,15,18,21,24,27,30,33], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['T_PL'], 'filename':'diag', 'arraylevel':1 }, - #'td300' :{ 'levels' : [-65,-60,-55,-50,-45,-40,-35,-30], 'cmap' : readNCLcm('nice_gfdl'), 'fname': ['TD_PL'], 'filename':'diag', 'arraylevel':7 }, - #'td500' :{ 'levels' : [-50,-45,-40,-35,-30,-25,-20,-15,-10], 'cmap' : readNCLcm('nice_gfdl'), 'fname': ['TD_PL'], 'filename':'diag', 'arraylevel':5 }, 'td700' :{ 'levels' : [-30,-25,-20,-15,-10,-5,0,5,10], 'cmap' : readNCLcm('nice_gfdl')[3:193], 'fname': ['TD_PL'], 'filename':'diag', 'arraylevel':3 }, 'td850' :{ 'levels' : [-40,-35,-30,-25,-20,-15,-10,-5,0,5,10,15,20,25,30], 'cmap' : readNCLcm('nice_gfdl')[3:193], 'fname': ['TD_PL'], 'filename':'diag', 'arraylevel':2 }, 'td925' :{ 'levels' : [-30,-25,-20,-15,-10,-5,0,5,10,15,20,25,30], 'cmap' : readNCLcm('nice_gfdl')[3:193], 'fname': ['TD_PL'], 'filename':'diag', 'arraylevel':1 }, 'rh300' :{ 'levels' : [0,10,20,30,40,50,60,70,80,90,100], 'cmap' : [readNCLcm('MPL_PuOr')[i] for i in (2,18,34,50)]+[readNCLcm('MPL_Greens')[j] for j in (2,17,50,75,106,125)], 'fname': ['RH_PL'], 'filename':'diag', 'arraylevel':7 }, 'rh500' :{ 'levels' : [0,10,20,30,40,50,60,70,80,90,100], 'cmap' : [readNCLcm('MPL_PuOr')[i] for i in (2,18,34,50)]+[readNCLcm('MPL_Greens')[j] for j in (2,17,50,75,106,125)], 'fname': ['RH_PL'], 'filename':'diag', 'arraylevel':5 }, -# 'rh700' :{ 'levels' : [0,10,20,30,40,50,60,70,80,90,100], 'cmap' : [readNCLcm('MPL_PuOr')[i] for i in (2,18,34,50)]+[readNCLcm('MPL_Greens')[j] for j in (2,17,50,75,106,125)], 'fname': ['RH_PL'], 'filename':'diag', 'arraylevel':3 }, 'rh700' :{ 'levels' : [0,10,20,30,40,50,60,70,80,90,100], 'cmap' : readNCLcm('CBR_drywet'), 'fname': ['RH_PL'], 'filename':'diag', 'arraylevel':3 }, 'rh850' :{ 'levels' : [0,10,20,30,40,50,60,70,80,90,100], 'cmap' : readNCLcm('CBR_drywet'), 'fname': ['RH_PL'], 'filename':'diag', 'arraylevel':2 }, -# 'rh850' :{ 'levels' : [0,10,20,30,40,50,60,70,80,90,100], 'cmap' : [readNCLcm('MPL_PuOr')[i] for i in (2,18,34,50)]+[readNCLcm('MPL_Greens')[j] for j in (2,17,50,75,106,125)], 'fname': ['RH_PL'], 'filename':'diag', 'arraylevel':2 }, 'rh925' :{ 'levels' : [0,10,20,30,40,50,60,70,80,90,100], 'cmap' : readNCLcm('CBR_drywet'), 'fname': ['RH_PL'], 'filename':'diag', 'arraylevel':1 }, -# 'rh925' :{ 'levels' : [0,10,20,30,40,50,60,70,80,90,100], 'cmap' : [readNCLcm('MPL_PuOr')[i] for i in (2,18,34,50)]+[readNCLcm('MPL_Greens')[j] for j in (2,17,50,75,106,125)], 'fname': ['RH_PL'], 'filename':'diag', 'arraylevel':1 }, 'avo500' :{ 'levels' : [0,9,12,15,18,21,24,27,30,33], 'cmap': readNCLcm('prcp_1'), 'fname': ['AVORT_PL'], 'filename':'diag', 'arraylevel':5 }, 'pvort320k' :{ 'levels' : [0,0.1,0.2,0.3,0.4,0.5,0.75,1,1.5,2,3,4,5,7,10], 'cmap' : ['#ffffff','#eeeeee','#dddddd','#cccccc','#bbbbbb','#d1c5b1','#e1d5b9','#f1ead3','#003399','#0033FF','#0099FF','#00CCFF','#8866FF','#9933FF','#660099'], 'fname': ['PVORT_320K'], 'filename':'upp' }, 'bunkmag' :{ 'levels' : [20,25,30,35,40,45,50,55,60], 'cmap':readNCLcm('wind_17lev')[1:], 'fname':['U_COMP_STM_6KM', 'V_COMP_STM_6KM'], 'filename':'upp' }, - 'speed10m' :{ 'levels' : [3,6,9,12,15,18,21,24,27,30,33,36,39,42,45,48,51], 'cmap': readNCLcm('wind_17lev')[1:],'fname' : ['U10', 'V10'], 'filename':'diag'}, - 'speed10m-tc' :{ 'levels' : [6,12,18,24,30,36,42,48,54,60,66,72,78,84,90,96,102], 'cmap': readNCLcm('wind_17lev')[1:],'fname' : ['U10', 'V10'], 'filename':'diag'}, - 'stp' :{ 'levels' : [0.5,0.75,1.0,1.5,2.0,3.0,4.0,5.0,6.0,7.0,8.0], 'cmap':readNCLcm('perc2_9lev'), 'fname':['SBCAPE','LCL_HEIGHT','SR_HELICITY_1KM','UBSHR6','VBSHR6'], 'arraylevel':[None,None,None,None,None], 'filename':'upp'}, - #'sigsvr :{ 'levels' : [1e5,2e5,3e5,4e5,5e5,6e5,8e5,10e5], 'cmap':readNCLcm('prcp_1'), 'fname':['MLCAPE','UHSHR], 'filename':'upp'} - 'ptype' :{ 'levels' : [0.01,0.1,0.25,0.5,0.75,1,1.5,2,2.5,3,3.5,4], 'cmap':['#dddddd','#aaaaaa']+readNCLcm('precip3_16lev')[1:], 'fname':['AFWA_RAIN_HRLY', 'AFWA_FZRA_HRLY', 'AFWA_ICE_HRLY', 'AFWA_SNOWFALL_HRLY'], 'filename':'wrfout'}, - 'ptypes' :{ 'levels' : [fifths,fifths,fifths,fifths], 'cmap':['green','blue','orange','red'], 'fname':['AFWA_RAIN_HRLY', 'AFWA_SNOWFALL_HRLY', 'AFWA_ICE_HRLY','AFWA_FZRA_HRLY'], 'filename':'wrfout'}, - 'ptypes-upp' :{ 'levels' : [fifths,fifths,fifths,fifths], 'cmap':['green','blue','orange','red'], 'fname':['UPP_CRAIN', 'UPP_CSNOW', 'UPP_CICEP', 'UPP_CFRZR'], 'filename':'upp'}, - 'ptypes-gsd' :{ 'levels' : [fifths,fifths,fifths,fifths], 'cmap':['green','blue','orange','red'], 'fname':[ 'CRAIN', 'CSNOW', 'CICEP', 'CFRZR'], 'filename':'upp'}, + 'speed10m' :{ 'levels' : [3,6,9,12,15,18,21,24,27,30,33,36,39,42,45,48,51], 'cmap': readNCLcm('wind_17lev')[1:],'fname' : ['u10', 'v10'], 'filename':'diag'}, + 'speed10m-tc' :{ 'levels' : [6,12,18,24,30,36,42,48,54,60,66,72,78,84,90,96,102], 'cmap': readNCLcm('wind_17lev')[1:],'fname' : ['u10', 'v10'], 'filename':'diag'}, + 'stp' :{ 'levels' : [0.5,0.75,1.0,1.5,2.0,3.0,4.0,5.0,6.0,7.0,8.0], 'cmap':readNCLcm('perc2_9lev'), 'fname':['sbcape','lcl','srh_0_1km','UBSHR6','VBSHR6'], 'arraylevel':[None,None,None,None,None], 'filename':'diag'}, + 'uhratio' :{ 'levels' : [0.1,0.3,0.5,0.7,0.9,1.0,1.1,1.2,1.3,1.4,1.5], 'cmap':readNCLcm('perc2_9lev'), 'fname':['updraft_helicity_max03', 'updraft_helicity_max'], 'filename':'diag'}, 'winter' :{ 'levels' : [0.01,0.1,0.25,0.5,0.75,1,1.5,2,2.5,3,3.5,4], 'cmap':['#dddddd','#aaaaaa']+readNCLcm('precip3_16lev')[1:], 'fname':['AFWA_RAIN_HRLY', 'AFWA_FZRA_HRLY', 'AFWA_ICE_HRLY', 'AFWA_SNOWFALL_HRLY'], 'filename':'wrfout'}, - 'crefuh' :{ 'levels' : [5,10,15,20,25,30,35,40,45,50,55,60,65,70], 'cmap': readcm('cmap_rad.rgb')[0:13], 'fname': ['REFL_MAX_COL', 'MAX_UPDRAFT_HELICITY'], 'filename':'upp' }, + 'crefuh' :{ 'levels' : [5,10,15,20,25,30,35,40,45,50,55,60,65,70], 'cmap': readcm('cmap_rad.rgb')[0:13], 'fname': ['refl10cm_max', 'updraft_helicity_max'], 'filename':'diag' }, # wind barb entries - 'wind10m' :{ 'fname' : ['U10', 'V10'], 'filename':'diag', 'skip':40 }, - 'wind250' :{ 'fname' : ['U_PL', 'V_PL'], 'filename':'diag', 'arraylevel':8, 'skip':40 }, - 'wind300' :{ 'fname' : ['U_PL', 'V_PL'], 'filename':'diag', 'arraylevel':7, 'skip':40 }, - 'wind500' :{ 'fname' : ['U_PL', 'V_PL'], 'filename':'diag', 'arraylevel':5, 'skip':40 }, - 'wind700' :{ 'fname' : ['U_PL', 'V_PL'], 'filename':'diag', 'arraylevel':3, 'skip':40 }, - 'wind850' :{ 'fname' : ['U_PL', 'V_PL'], 'filename':'diag', 'arraylevel':2, 'skip':40 }, - 'wind925' :{ 'fname' : ['U_PL', 'V_PL'], 'filename':'diag', 'arraylevel':1, 'skip':40 }, + 'wind10m' :{ 'fname' : ['u10', 'v10'], 'filename':'diag', 'skip':40 }, + 'wind500' :{ 'fname' : ['uzonal_500hPa', 'umeridional_500hPa'], 'filename':'diag', 'skip':40 }, + 'wind700' :{ 'fname' : ['uzonal_700hPa', 'umeridional_700hPa'], 'filename':'diag', 'skip':40 }, 'shr06' :{ 'fname' : ['UBSHR6','VBSHR6'], 'filename': 'upp', 'skip':40 }, 'shr01' :{ 'fname' : ['UBSHR1', 'VBSHR1'], 'filename': 'upp', 'skip':40 }, 'bunkers' :{ 'fname' : ['U_COMP_STM_6KM', 'V_COMP_STM_6KM'], 'filename': 'upp', 'skip':40 }, @@ -187,6 +160,7 @@ def readNCLcm(name): 'SGP' :{ 'corners': [25.3,-107.00,36.00,-88.70], 'fig_width':1080 }, 'NGP' :{ 'corners': [40.00,-105.0,50.30,-82.00], 'fig_width':1080 }, 'CGP' :{ 'corners': [33.00,-107.50,45.00,-86.60], 'fig_width':1080 }, + 'TEST':{ 'corners': [36.00,-100.50,45.00,-96.60], 'fig_width':1080 }, 'SW' :{ 'corners': [28.00,-121.50,44.39,-102.10], 'fig_width':1080 }, 'NW' :{ 'corners': [37.00,-124.40,51.60,-102.10], 'fig_width':1080 }, 'SE' :{ 'corners': [26.10,-92.75,36.00,-71.00], 'fig_width':1080 }, From 66d95582be3c139d10e387fc4ea156947873de59 Mon Sep 17 00:00:00 2001 From: ahijevyc Date: Wed, 10 Apr 2019 15:12:46 -0600 Subject: [PATCH 18/68] Interpolate to latlon for contour and barbs Chop out lat-lon box of MPAS for speed. Deal with 1D and 2D arrays for spatial dimensions and refer to them as *spatial_dimensions (i.e. use the * splat operator to expand the tuple into comma-separated arguments). --- webplot.py | 70 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 47 insertions(+), 23 deletions(-) diff --git a/webplot.py b/webplot.py index 7582c92..db4c014 100755 --- a/webplot.py +++ b/webplot.py @@ -8,11 +8,12 @@ from scipy import interpolate import subprocess import pdb +from scipy.interpolate import griddata from fieldinfo import * # To use MFDataset, first convert netcdf4 to netcdf4-classic with nccopy # for example, nccopy -d nc7 /glade/p/nsc/nmmm0046/schwartz/MPAS_ens_15-3km_mesh/POST/2017050100/ens_1/diag_latlon_g193.2017-05-02_00.00.00.nc /glade/scratch/ahijevyc/hwt2017/2017050100/ens_1/diag_latlon_g193.2017-05-02_00.00.00.nc from netCDF4 import Dataset, MFDataset -# use xarray because it can handle muliple files with NETCDF4 non-classic. netCDF4 MFDataset can't do that. +# xarray can handle muliple files with NETCDF4 non-classic. netCDF4 MFDataset can't do that. import xarray class webPlot: @@ -55,7 +56,10 @@ def loadMap(self, overlay=False): # load lat/lons LATLON_FILE = os.getenv('LATLON_FILE', PYTHON_SCRIPTS_DIR+'/rt2015_latlon_d02.nc') #self.lats, self.lons = readGrid(LATLON_FILE) - self.lats, self.lons = readGridMPAS() + self.lats, self.lons, self.grid_spacing_km = readGridMPAS() + self.ibox = (self.m.lonmin-10 <= self.lons ) & (self.lons < self.m.lonmax+10) & (self.m.latmin-10 <= self.lats) & (self.lats < self.m.latmax+10) + self.lats = self.lats[self.ibox] + self.lons = self.lons[self.ibox] self.x, self.y = self.m(self.lons,self.lats) def readEnsemble(self): @@ -325,10 +329,10 @@ def plotFill(self): # smooth some of the fill fields if self.opts['fill']['name'] == 'avo500': self.data['fill'][0] = ndimage.gaussian_filter(self.data['fill'][0], sigma=4) if self.opts['fill']['name'] == 'pbmin': self.data['fill'][0] = ndimage.gaussian_filter(self.data['fill'][0], sigma=2) - + # Sometimes you get a warning kwarg tri is ignored. # Tried removing tri=True but got IndexError: too many indices for array - cs1 = self.m.contourf(self.x, self.y, self.data['fill'][0], levels=levels, cmap=cmap, norm=norm, tri=True, extend='max', ax=self.ax) #MPAS + cs1 = self.m.contourf(self.x, self.y, self.data['fill'][0][self.ibox], levels=levels, cmap=cmap, norm=norm, tri=True, extend='max', ax=self.ax) #MPAS #cs1 = self.m.contourf(self.x, self.y, self.data['fill'][0], levels=levels, cmap=cmap, norm=norm, extend='max', ax=self.ax) self.plotColorbar(cs1, levels, tick_labels, extend, extendfrac) @@ -404,9 +408,9 @@ def plotReflectivityUH(self): norm = colors.BoundaryNorm(levels, cmap.N) tick_labels = levels[:-1] - cs1 = self.m.contourf(self.x, self.y, self.data['fill'][0], levels=levels, cmap=cmap, norm=norm, extend='max', ax=self.ax) - self.m.contourf(self.x, self.y, self.data['fill'][1], levels=[75,1000], colors='black', ax=self.ax, alpha=0.3) - self.m.contour(self.x, self.y, self.data['fill'][1], levels=[75], colors='k', linewidth=0.5, ax=self.ax) + cs1 = self.m.contourf(self.x, self.y, self.data['fill'][0], levels=levels, cmap=cmap, tri=True, norm=norm, extend='max', ax=self.ax) + self.m.contourf(self.x, self.y, self.data['fill'][1], levels=[75,1000], colors='black', tri=True, ax=self.ax, alpha=0.3) + self.m.contour(self.x, self.y, self.data['fill'][1], levels=[75], colors='k', tri=True, linewidth=0.5, ax=self.ax) #maxuh = self.data['fill'][1].max() #self.ax.text(0.03,0.03,'Domain-wide UH max %0.f'%maxuh ,ha="left",va="top",bbox=dict(boxstyle="square",lw=0.5,fc="white"), transform=self.ax.transAxes) @@ -421,13 +425,25 @@ def plotColorbar(self, cs, levels, tick_labels, extend='neither', extendfrac=0.0 cb = plt.colorbar(cs, cax=cax, orientation='horizontal', extend=extend, extendfrac=extendfrac, ticks=tick_labels) cb.outline.set_linewidth(0.5) + + def latlonGrid(self, data): + delta_deg = self.grid_spacing_km / 111 + nlon = (self.m.lonmax - self.m.lonmin)/delta_deg + nlat = (self.m.latmax - self.m.latmin)/delta_deg + # TODO: maybe do in map coordinates instead of latlon to avoid the need to specify latlon=True in the contour and barb cases. + x2d, y2d = np.meshgrid(np.linspace(self.m.lonmin, self.m.lonmax, nlon), np.linspace(self.m.latmin,self.m.latmax,nlat)) + z2d = griddata((self.lons, self.lats), data, (x2d, y2d), method='nearest') + return (x2d, y2d, z2d) + def plotContour(self): - if self.opts['contour']['name'] in ['t2-0c']: data = ndimage.gaussian_filter(self.data['contour'][0], sigma=2) - else: data = ndimage.gaussian_filter(self.data['contour'][0], sigma=10) if self.opts['contour']['name'] in ['sbcinh','mlcinh']: linewidth, alpha = 0.5, 0.75 else: linewidth, alpha = 1.5, 1.0 - cs2 = self.m.contour(self.x, self.y, data, levels=self.opts['contour']['levels'], colors='k', linewidths=linewidth, ax=self.ax, alpha=alpha) + x2d, y2d, data = self.latlonGrid(self.data['contour'][0].values[self.ibox]) + if self.opts['contour']['name'] in ['t2-0c']: data = ndimage.gaussian_filter(data, sigma=2) + else: data = ndimage.gaussian_filter(data, sigma=10) + + cs2 = self.m.contour(x2d, y2d, data, latlon=True, levels=self.opts['contour']['levels'], colors='k', linewidths=linewidth, ax=self.ax, alpha=alpha) plt.clabel(cs2, fontsize='small', fmt='%i') def plotBarbs(self): @@ -437,8 +453,13 @@ def plotBarbs(self): if self.opts['fill']['name'] == 'crefuh': alpha=0.5 else: alpha=1.0 - cs2 = self.m.barbs(self.x[::skip,::skip], self.y[::skip,::skip], self.data['barb'][0][::skip,::skip], self.data['barb'][1][::skip,::skip], \ - color='black', alpha=alpha, length=5.5, linewidth=0.25, sizes={'emptybarb':0.05}, ax=self.ax) + # skip interval was intended for 2-D fields + if len(self.x.shape) == 2: + cs2 = self.m.barbs(self.x[::skip,::skip], self.y[::skip,::skip], self.data['barb'][0][::skip,::skip], self.data['barb'][1][::skip,::skip], color='black', alpha=alpha, length=5.5, linewidth=0.25, sizes={'emptybarb':0.05}, ax=self.ax) + if len(self.x.shape) == 1: + x2d, y2d, u2d = self.latlonGrid(self.data['barb'][0].values[self.ibox]) + x2d, y2d, v2d = self.latlonGrid(self.data['barb'][1].values[self.ibox]) + cs2 = self.m.barbs(x2d[::skip,::skip], y2d[::skip,::skip], u2d[::skip,::skip], v2d[::skip,::skip], latlon=True, color='black', alpha=alpha, length=5.5, linewidth=0.25, sizes={'emptybarb':0.05}, ax=self.ax) def plotStreamlines(self): speed = np.sqrt(self.data['barb'][0]**2 + self.data['barb'][1]**2) @@ -865,7 +886,7 @@ def readEnsemble(wrfinit, timerange=None, fields=None, debug=False, ENS_SIZE=10) #data = np.swapaxes(data,0,1) # flip first two axes so time is first # if all times are in one file, then need to reshape and extract desired times - #data = data.reshape((10,49,data.shape[1],data.shape[2])) # reshape + #data = data.reshape((10,49,*spatial_dimensions)) # reshape #data = data[:,timerange[0]:timerange[1]+1,:,:] # extract desired times #data = np.swapaxes(data,0,1) # flip first two axes so time is first #data = data.reshape((10*((timerange[1]+1)-timerange[0])),data.shape[2],data.shape[3]) #reshape again @@ -908,6 +929,7 @@ def readEnsemble(wrfinit, timerange=None, fields=None, debug=False, ENS_SIZE=10) datadict[f] = [] for data in datalist: # perform mean/max/variance/etc to reduce 3D array to 2D + spatial_dimensions = data.shape[1:] # works for 1D meshes like MPAS and 2D grids like WRF if (fieldtype == 'mean'): data = np.mean(data, axis=0) elif (fieldtype == 'pmm'): data = compute_pmm(data) elif (fieldtype == 'max'): data = np.amax(data, axis=0) @@ -915,26 +937,26 @@ def readEnsemble(wrfinit, timerange=None, fields=None, debug=False, ENS_SIZE=10) elif (fieldtype == 'var'): data = np.std(data, axis=0) elif (fieldtype == 'maxstamp'): for i in missing_list[filename]: data = np.insert(data, i, np.nan, axis=0) #insert nan for missing files - data = np.reshape(data, (data.shape[0]/ENS_SIZE,ENS_SIZE,data.shape[1],data.shape[2])) + data = np.reshape(data, (data.shape[0]/ENS_SIZE,ENS_SIZE,*spatial_dimensions)) data = np.nanmax(data, axis=0) elif (fieldtype == 'summean'): for i in missing_list[filename]: data = np.insert(data, i, np.nan, axis=0) #insert nan for missing files - data = np.reshape(data, (data.shape[0]/ENS_SIZE,ENS_SIZE,data.shape[1],data.shape[2])) + data = np.reshape(data, (data.shape[0]/ENS_SIZE,ENS_SIZE,*spatial_dimensions)) data = np.nansum(data, axis=0) data = np.nanmean(data, axis=0) elif (fieldtype == 'maxmean'): for i in missing_list[filename]: data = np.insert(data, i, np.nan, axis=0) #insert nan for missing files - data = np.reshape(data, (data.shape[0]/ENS_SIZE,ENS_SIZE,data.shape[1],data.shape[2])) + data = np.reshape(data, (data.shape[0]/ENS_SIZE,ENS_SIZE,*spatial_dimensions)) data = np.nanmax(data, axis=0) data = np.nanmean(data, axis=0) elif (fieldtype == 'summax'): for i in missing_list[filename]: data = np.insert(data, i, np.nan, axis=0) #insert nan for missing files - data = np.reshape(data, (data.shape[0]/ENS_SIZE,ENS_SIZE,data.shape[1],data.shape[2])) + data = np.reshape(data, (data.shape[0]/ENS_SIZE,ENS_SIZE,*spatial_dimensions)) data = np.nansum(data, axis=0) data = np.nanmax(data, axis=0) elif (fieldtype[0:3] == 'mem'): for i in missing_list[filename]: data = np.insert(data, i, np.nan, axis=0) #insert nan for missing files - data = np.reshape(data, (data.shape[0]/ENS_SIZE,ENS_SIZE,data.shape[1],data.shape[2])) + data = np.reshape(data, (data.shape[0]/ENS_SIZE,ENS_SIZE,*spatial_dimensions)) print(fieldname) if fieldname in ['precip', 'precipacc']: print('where we should be') @@ -945,7 +967,6 @@ def readEnsemble(wrfinit, timerange=None, fields=None, debug=False, ENS_SIZE=10) if fieldtype in ['prob', 'neprob', 'probgt', 'neprobgt']: data = (data>=thresh).astype('float') elif fieldtype in ['problt', 'neproblt']: data = (data=thresh).astype('float') for i in missing_list[filename]: data = np.insert(data, i, np.nan, axis=0) - data = np.reshape(data, (data.shape[0]/ENS_SIZE,ENS_SIZE,data.shape[1],data.shape[2])) + data = np.reshape(data, (data.shape[0]/ENS_SIZE,ENS_SIZE,*spatial_dimensions)) data = compute_prob3d(data, roi=14, sigma=float(fields['sigma']), type='gaussian') if debug: print('field '+ fieldname+ ' has shape', data.shape, 'max', data.max(), 'min', data.min()) @@ -980,9 +1001,12 @@ def readGridMPAS(): fh = Dataset("/glade/p/mmm/parc/schwartz/MPAS/15-3km_mesh/init.nc", "r") lats = fh.variables['latCell'][:] lons = fh.variables['lonCell'][:] - lats, lons = lats*57.29578, lons*57.29578 #convert radians to degrees + areaCell = fh.variables['areaCell'][:] # units m^2 + max_resolution_km = 2. * np.sqrt(areaCell.min()/np.pi/1000/1000) fh.close() - return (lats, lons) + lats, lons = np.degrees(lats), np.degrees(lons) #convert radians to degrees + lons[lons >= 180] = lons[lons >= 180] - 360 + return (lats, lons, max_resolution_km) def saveNewMap(domstr='CONUS', wrfout=None): # if domstr is not in the dictionary, then use provided wrfout to create new domain @@ -1025,7 +1049,7 @@ def saveNewMap(domstr='CONUS', wrfout=None): #x,y,w,h = 0.01, 0.8/float(fig_height), 0.98, 0.98*fig_width*m.aspect/float(fig_height) #too much padding at top x,y,w,h = 0.01, 0.7/float(fig_height), 0.98, 0.98*fig_width*m.aspect/float(fig_height) ax = fig.add_axes([x,y,w,h]) - for i in ax.spines.itervalues(): i.set_linewidth(0.5) + for i in list(ax.spines.values()): i.set_linewidth(0.5) m.drawcoastlines(linewidth=0.5, ax=ax) m.drawstates(linewidth=0.25, ax=ax) From 4b76a9e3bb5a0b2302ecc7375dc88edf9f1db750 Mon Sep 17 00:00:00 2001 From: ahijevyc Date: Wed, 10 Apr 2019 15:16:58 -0600 Subject: [PATCH 19/68] change buffer from 10 to 1 degrees when chopping out lat-lon box of interest --- webplot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webplot.py b/webplot.py index db4c014..f101e8c 100755 --- a/webplot.py +++ b/webplot.py @@ -57,7 +57,7 @@ def loadMap(self, overlay=False): LATLON_FILE = os.getenv('LATLON_FILE', PYTHON_SCRIPTS_DIR+'/rt2015_latlon_d02.nc') #self.lats, self.lons = readGrid(LATLON_FILE) self.lats, self.lons, self.grid_spacing_km = readGridMPAS() - self.ibox = (self.m.lonmin-10 <= self.lons ) & (self.lons < self.m.lonmax+10) & (self.m.latmin-10 <= self.lats) & (self.lats < self.m.latmax+10) + self.ibox = (self.m.lonmin-1 <= self.lons ) & (self.lons < self.m.lonmax+1) & (self.m.latmin-1 <= self.lats) & (self.lats < self.m.latmax+1) self.lats = self.lats[self.ibox] self.lons = self.lons[self.ibox] self.x, self.y = self.m(self.lons,self.lats) From 85df1dd0dce0c9058e602b4f7b38268e0ddd66b9 Mon Sep 17 00:00:00 2001 From: ahijevyc Date: Wed, 10 Apr 2019 15:58:16 -0600 Subject: [PATCH 20/68] Ran 2to3 --- fieldinfo.py | 4 ++-- webplot.py | 32 ++++++++++++++++---------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/fieldinfo.py b/fieldinfo.py index 2e26083..d81541a 100755 --- a/fieldinfo.py +++ b/fieldinfo.py @@ -2,7 +2,7 @@ def readcm(name): '''Read colormap from file formatted as 0-1 RGB CSV''' rgb = [] fh = open(name, 'r') - for line in fh.read().splitlines(): rgb.append(map(float,line.split())) + for line in fh.read().splitlines(): rgb.append(list(map(float,line.split()))) return rgb def readNCLcm(name): @@ -16,7 +16,7 @@ def readNCLcm(name): else: fh = open('%s/%s.rgb'%(rgb_dir_ch,name), 'r') for line in fh.read().splitlines(): - if appending: rgb.append(map(float,line.split())) + if appending: rgb.append(list(map(float,line.split()))) if ''.join(line.split()) in ['#rgb',';RGB']: appending = True maxrgb = max([ x for y in rgb for x in y ]) if maxrgb > 1: rgb = [ [ x/255.0 for x in a ] for a in rgb ] diff --git a/webplot.py b/webplot.py index 0a77211..3eb2245 100755 --- a/webplot.py +++ b/webplot.py @@ -2,7 +2,7 @@ import matplotlib.pyplot as plt from mpl_toolkits.basemap import * from datetime import * -import cPickle as pickle +import pickle as pickle import os, sys, time, argparse import scipy.ndimage as ndimage import subprocess @@ -18,7 +18,7 @@ def __init__(self): self.debug = self.opts['debug'] self.autolevels = self.opts['autolevels'] self.domain = self.opts['domain'] - if ',' in self.opts['timerange']: self.shr, self.ehr = map(int, self.opts['timerange'].split(',')) + if ',' in self.opts['timerange']: self.shr, self.ehr = list(map(int, self.opts['timerange'].split(','))) else: self.shr, self.ehr = int(self.opts['timerange']), int(self.opts['timerange']) self.createFilename() self.ENS_SIZE = int(os.getenv('ENS_SIZE', 10)) @@ -319,7 +319,7 @@ def parseargs(): parser = argparse.ArgumentParser(description='Web plotting script for NCAR ensemble') parser.add_argument('-d', '--date', default=datetime.utcnow().strftime('%Y%m%d00'), help='initialization datetime (YYYYMMDDHH)') parser.add_argument('-tr', '--timerange', required=True, help='time range of forecasts (START,END)') - parser.add_argument('-f', '--fill', help='fill field (FIELD_PRODUCT_THRESH), field keys:'+','.join(fieldinfo.keys())) + parser.add_argument('-f', '--fill', help='fill field (FIELD_PRODUCT_THRESH), field keys:'+','.join(list(fieldinfo.keys()))) parser.add_argument('-c', '--contour', help='contour field (FIELD_PRODUCT_THRESH)') parser.add_argument('-b', '--barb', help='barb field (FIELD_PRODUCT_THRESH)') parser.add_argument('-bs', '--barbskip', help='barb skip interval') @@ -410,15 +410,15 @@ def makeEnsembleList(wrfinit, timerange, ENS_SIZE): def readEnsemble(wrfinit, timerange=None, fields=None, debug=False, ENS_SIZE=10): ''' Reads in desired fields and returns 2-D arrays of data for each field (barb/contour/field) ''' - if debug: print fields + if debug: print(fields) datadict = {} file_list, missing_list = makeEnsembleList(wrfinit, timerange, ENS_SIZE) #construct list of files # loop through fill field, contour field, barb field and retrieve required data for f in ['fill', 'contour', 'barb']: - if not fields[f].keys(): continue - if debug: print 'Reading field:', fields[f]['name'], 'from', fields[f]['filename'] + if not list(fields[f].keys()): continue + if debug: print('Reading field:', fields[f]['name'], 'from', fields[f]['filename']) # save some variables for use in this function filename = fields[f]['filename'] @@ -429,13 +429,13 @@ def readEnsemble(wrfinit, timerange=None, fields=None, debug=False, ENS_SIZE=10) if fieldtype[0:3]=='mem': member = int(fieldtype[3:]) # open Multi-file netcdf dataset - if debug: print file_list[filename] + if debug: print(file_list[filename]) fh = MFDataset(file_list[filename]) # loop through each field, wind fields will have two fields that need to be read datalist = [] for n,array in enumerate(arrays): - if debug: print 'Reading', array + if debug: print('Reading', array) #read in 3D array (times*members,ny,nx) from file object if 'arraylevel' in fields[f]: @@ -467,7 +467,7 @@ def readEnsemble(wrfinit, timerange=None, fields=None, debug=False, ENS_SIZE=10) datalist.append(data) # these are derived fields, we don't have in any of the input files but we can compute - print datalist[0].shape + print(datalist[0].shape) if 'name' in fields[f]: if fieldname in ['shr06mag', 'shr01mag', 'bunkmag','speed10m']: datalist = [np.sqrt(datalist[0]**2 + datalist[1]**2)] elif fieldname == 'stp': datalist = [computestp(datalist)] @@ -509,9 +509,9 @@ def readEnsemble(wrfinit, timerange=None, fields=None, debug=False, ENS_SIZE=10) elif (fieldtype[0:3] == 'mem'): for i in missing_list[filename]: data = np.insert(data, i, np.nan, axis=0) #insert nan for missing files data = np.reshape(data, (data.shape[0]/ENS_SIZE,ENS_SIZE,data.shape[1],data.shape[2])) - print fieldname + print(fieldname) if fieldname in ['precip', 'precipacc']: - print 'where we should be' + print('where we should be') data = np.nanmax(data, axis=0) else: data = np.nanmax(data, axis=0) data = data[member-1,:] @@ -529,7 +529,7 @@ def readEnsemble(wrfinit, timerange=None, fields=None, debug=False, ENS_SIZE=10) for i in missing_list[filename]: data = np.insert(data, i, np.nan, axis=0) data = np.reshape(data, (data.shape[0]/ENS_SIZE,ENS_SIZE,data.shape[1],data.shape[2])) data = compute_prob3d(data, roi=14, sigma=float(fields['sigma']), type='gaussian') - if debug: print 'field', fieldname, 'has shape', data.shape, 'max', data.max(), 'min', data.min() + if debug: print('field', fieldname, 'has shape', data.shape, 'max', data.max(), 'min', data.min()) # attach data arrays for each type of field (e.g. { 'fill':[data], 'barb':[data,data] }) datadict[f].append(data) @@ -587,7 +587,7 @@ def saveNewMap(domstr='CONUS', wrfout=None): #x,y,w,h = 0.01, 0.8/float(fig_height), 0.98, 0.98*fig_width*m.aspect/float(fig_height) #too much padding at top x,y,w,h = 0.01, 0.7/float(fig_height), 0.98, 0.98*fig_width*m.aspect/float(fig_height) ax = fig.add_axes([x,y,w,h]) - for i in ax.spines.itervalues(): i.set_linewidth(0.5) + for i in ax.spines.values(): i.set_linewidth(0.5) m.drawcoastlines(linewidth=0.5, ax=ax) m.drawstates(linewidth=0.25, ax=ax) @@ -626,14 +626,14 @@ def compute_neprob(ensemble, roi=0, sigma=0.0, type='gaussian'): return ens_mean def compute_prob3d(ensemble, roi=0, sigma=0.0, type='gaussian'): - print ensemble.shape + print(ensemble.shape) y,x = np.ogrid[-roi:roi+1, -roi:roi+1] kernel = x**2 + y**2 <= roi**2 ens_roi = ndimage.filters.maximum_filter(ensemble, footprint=kernel[np.newaxis,np.newaxis,:]) - print ens_roi.shape + print(ens_roi.shape) ens_mean = np.nanmean(ens_roi, axis=1) - print ens_mean.shape + print(ens_mean.shape) ens_mean = ndimage.filters.gaussian_filter(ens_mean, [2,20,20]) return ens_mean[3,:] From abec4a8758c28bb85ac328e3ca0d2806ca045728 Mon Sep 17 00:00:00 2001 From: ahijevyc Date: Thu, 11 Apr 2019 17:09:47 -0600 Subject: [PATCH 21/68] added parentheses to print statement --- make_webplot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/make_webplot.py b/make_webplot.py index a19f9ab..cb3bf1b 100755 --- a/make_webplot.py +++ b/make_webplot.py @@ -3,7 +3,7 @@ import sys, time, os from webplot import webPlot, readGrid, drawOverlay, saveNewMap -def log(msg): print time.ctime(time.time()),':', msg +def log(msg): print(time.ctime(time.time()),':', msg) log('Begin Script'); stime = time.time() From 738a99cce2b3eed160ce3a3c275c889e0aa08865 Mon Sep 17 00:00:00 2001 From: ahijevyc Date: Fri, 12 Apr 2019 09:19:37 -0600 Subject: [PATCH 22/68] removed reference to overlay --- make_webplot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/make_webplot.py b/make_webplot.py index cb3bf1b..6fbdede 100755 --- a/make_webplot.py +++ b/make_webplot.py @@ -1,7 +1,7 @@ #!/usr/bin/env python import sys, time, os -from webplot import webPlot, readGrid, drawOverlay, saveNewMap +from webplot import webPlot, readGrid, saveNewMap def log(msg): print(time.ctime(time.time()),':', msg) From 9ce92eed92c6bec154a48408ebdd05140d4435b1 Mon Sep 17 00:00:00 2001 From: ahijevyc Date: Fri, 12 Apr 2019 09:20:01 -0600 Subject: [PATCH 23/68] Maybe remake pickle files; use python 2 or 3. Fixed tab, pickle read, no overplot Replaced tabs with spaces. Added binary flag to pickle read ('r' to 'rb')--for python3. Removed reference to 'over' option (for overplot). --- webplot.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/webplot.py b/webplot.py index 3eb2245..3f39cd4 100755 --- a/webplot.py +++ b/webplot.py @@ -19,7 +19,7 @@ def __init__(self): self.autolevels = self.opts['autolevels'] self.domain = self.opts['domain'] if ',' in self.opts['timerange']: self.shr, self.ehr = list(map(int, self.opts['timerange'].split(','))) - else: self.shr, self.ehr = int(self.opts['timerange']), int(self.opts['timerange']) + else: self.shr, self.ehr = int(self.opts['timerange']), int(self.opts['timerange']) self.createFilename() self.ENS_SIZE = int(os.getenv('ENS_SIZE', 10)) @@ -40,7 +40,7 @@ def createFilename(self): def loadMap(self): # load pickle file containing figure and axes objects (should be pregenerated) PYTHON_SCRIPTS_DIR = os.getenv('PYTHON_SCRIPTS_DIR', '.') - self.fig, self.ax, self.m = pickle.load(open('%s/%s.pk'%(PYTHON_SCRIPTS_DIR,self.domain), 'r')) + self.fig, self.ax, self.m = pickle.load(open('%s/%s.pk'%(PYTHON_SCRIPTS_DIR,self.domain), 'rb')) # get lat/lons from file here LATLON_FILE = os.getenv('LATLON_FILE', PYTHON_SCRIPTS_DIR+'/latlonfile.nc') @@ -51,7 +51,6 @@ def readEnsemble(self): self.data, self.missing_members = readEnsemble(self.initdate, timerange=[self.shr,self.ehr], fields=self.opts, debug=self.debug, ENS_SIZE=self.ENS_SIZE) def plotTitleTimes(self): - if self.opts['over']: return fontdict = {'family':'monospace', 'size':12, 'weight':'bold'} # place title and times above corners of map @@ -309,7 +308,7 @@ def saveFigure(self, trans=False): elif self.opts['fill']['ensprod'] in ['prob', 'neprob', 'probgt', 'problt', 'neprobgt', 'neproblt']: ncolors = 48 elif self.opts['fill']['name'] in ['crefuh']: ncolors = 48 else: ncolors = 255 - command = 'pngquant %d %s --ext=.png --force'%(ncolors,self.outfile) + command = '/glade/u/home/sobash/pngquant/pngquant %d %s --ext=.png --force'%(ncolors,self.outfile) ret = subprocess.check_call(command.split()) plt.clf() @@ -331,7 +330,6 @@ def parseargs(): parser.add_argument('--debug', action='store_true', help='turn on debugging') opts = vars(parser.parse_args()) - if opts['interp']: opts['over'] = True # opts = { 'date':date, 'timerange':timerange, 'fill':'sbcape_prob_25', 'ensprod':'mean' ... } # now, convert underscore delimited fill, contour, and barb args into dicts @@ -429,7 +427,7 @@ def readEnsemble(wrfinit, timerange=None, fields=None, debug=False, ENS_SIZE=10) if fieldtype[0:3]=='mem': member = int(fieldtype[3:]) # open Multi-file netcdf dataset - if debug: print(file_list[filename]) + if debug: print(file_list[filename]) fh = MFDataset(file_list[filename]) # loop through each field, wind fields will have two fields that need to be read From 2c2ce17ebbc8148ce67fa61446462e73b58f0567 Mon Sep 17 00:00:00 2001 From: ahijevyc Date: Fri, 12 Apr 2019 16:51:58 -0600 Subject: [PATCH 24/68] fixed typo in date variable --- webplot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webplot.py b/webplot.py index f101e8c..3b662bc 100755 --- a/webplot.py +++ b/webplot.py @@ -673,7 +673,7 @@ def makeEnsembleList(wrfinit, timerange, ENS_SIZE): yyyymmddhh = wrfinit.strftime('%Y%m%d%H') for mem in range(1,ENS_SIZE+1): wrfout = '%s/%s/wrf_rundir/ens_%d/wrfout_d02_%s'%(EXP_DIR,yyyymmddhh,mem,wrfvalidstr) - diag = '%s/%s/wrf_rundir/ens_%d/diags_d02.%s.nc'%(EXP_DIR,yyymmddhh,mem,wrfvalidstr) + diag = '%s/%s/wrf_rundir/ens_%d/diags_d02.%s.nc'%(EXP_DIR,yyyymmddhh,mem,wrfvalidstr) #diag = '/glade/scratch/sobash/FOR_MORRIS/%s/mem%d/diags_d02_f%03d.nc'%(yyyymmddhh,mem,hr) upp = '%s/%s/post_rundir/mem_%d/fhr_%d/WRFTWO%02d.nc'%(EXP_DIR,yyyymmddhh,mem,hr,hr) #ens1 = '/glade/p/nmm0001/romine/rt2015/ens_1km/%s/mem%02d_%s.nc'%(yyyymmddhh,mem,yyyymmddhh) From 4b809f9ce4560425855ba9c47ef8c1f97ba113dd Mon Sep 17 00:00:00 2001 From: ahijevyc Date: Fri, 12 Apr 2019 16:52:13 -0600 Subject: [PATCH 25/68] get init_time from MET file --- get_model_time.py | 1 + 1 file changed, 1 insertion(+) diff --git a/get_model_time.py b/get_model_time.py index aa8ddb7..9cf4e70 100644 --- a/get_model_time.py +++ b/get_model_time.py @@ -32,6 +32,7 @@ def valid(ncfilename, diagnostic_name): valid_time = initialization_time + forecast_lead_time elif 'valid_time' in x.ncattrs(): valid_time = datetime.datetime.strptime(x.valid_time, '%Y%m%d_%H%M%S') + initialization_time = datetime.datetime.strptime(x.init_time, '%Y%m%d_%H%M%S') elif 'START_DATE' in global_atts: # Like ds300 NCAR WRF ensemble diags files initialization_time = datetime.datetime.strptime(nc.START_DATE, '%Y-%m-%d_%H:%M:%S') From 3cc537837fb30e077da7cf1ff7582ecb7b13eeb9 Mon Sep 17 00:00:00 2001 From: ahijevyc Date: Tue, 23 Jul 2019 09:12:19 -0600 Subject: [PATCH 26/68] binary flag for pickle dump --- webplot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webplot.py b/webplot.py index 3f39cd4..611d6e0 100755 --- a/webplot.py +++ b/webplot.py @@ -590,9 +590,9 @@ def saveNewMap(domstr='CONUS', wrfout=None): m.drawcoastlines(linewidth=0.5, ax=ax) m.drawstates(linewidth=0.25, ax=ax) m.drawcountries(ax=ax) - #m.drawcounties(linewidth=0.1, ax=ax) + m.drawcounties(linewidth=0.1, ax=ax) - pickle.dump((fig,ax,m), open('rt2015_%s.pk'%domstr, 'w')) + pickle.dump((fig,ax,m), open('rt2015_%s.pk'%domstr, 'wb')) def compute_pmm(ensemble): mem, dy, dx = ensemble.shape From c71d5ae33288674fbc4d54b9152c8c31e86f4b5a Mon Sep 17 00:00:00 2001 From: ahijevyc Date: Tue, 23 Jul 2019 09:18:48 -0600 Subject: [PATCH 27/68] adapt to hwt2017 mpas ensemble 48-hr precip table specifiy diagnostic file source for t2depart and t2-0c thetapv vertical velocity field name changed reflectivity field name changed specify diagnostic file source for srh No vertical dimension for pressure-level height, speed and rh. Top and bottom level for shear derivation. Many other wind barb levels. Every 50th instead of every 40th wind barb. North America domain --- fieldinfo.py | 141 +++++++++++++++++++++++++-------------------------- 1 file changed, 68 insertions(+), 73 deletions(-) diff --git a/fieldinfo.py b/fieldinfo.py index b1bc4dc..6518fb9 100755 --- a/fieldinfo.py +++ b/fieldinfo.py @@ -30,137 +30,132 @@ def readNCLcm(name): fieldinfo = { # surface and convection-related entries - 'precip' :{ 'levels' : [0,0.01,0.05,0.1,0.2,0.3,0.4,0.5,0.75,1,1.5,2,2.5,3.0], 'cmap': [readNCLcm('precip2_17lev')[i] for i in (0,1,2,4,5,6,7,8,10,12,13,14,15)], 'fname':['rainnc'] }, - 'precip-24hr' :{ 'levels' : [0,0.01,0.05,0.1,0.25,0.5,0.75,1.0,1.25,1.5,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0,12.0,13.0], 'cmap': [readNCLcm('precip2_17lev')[i] for i in (0,1,2,4,5,6,7,8,10,12,13,14,15,16,17)]+['#777777', '#AAAAAA', '#CCCCCC', '#EEEEEE']+[readNCLcm('sunshine_diff_12lev')[i] for i in (4,2,1)], 'fname':['rainnc'] }, - 'precipacc':{ 'levels' : [0,0.01,0.05,0.1,0.25,0.5,0.75,1.0,1.25,1.5,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0,12.0,13.0], 'cmap': [readNCLcm('precip2_17lev')[i] for i in (0,1,2,4,5,6,7,8,10,12,13,14,15,16,17)]+['#777777', '#AAAAAA', '#CCCCCC', '#EEEEEE']+[readNCLcm('sunshine_diff_12lev')[i] for i in (4,2,1)], 'fname':['rainnc'] }, + 'precip' :{ 'levels' : [0,0.01,0.05,0.1,0.2,0.3,0.4,0.5,0.75,1,1.5,2,2.5,3.0], 'cmap': [readNCLcm('precip2_17lev')[i] for i in (0,1,2,4,5,6,7,8,10,12,13,14,15)], 'fname':['rainnc'], 'filename':'diag' }, + 'precip-24hr' :{ 'levels' : [0,0.01,0.05,0.1,0.25,0.5,0.75,1.0,1.25,1.5,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0,12.0,13.0], 'cmap': [readNCLcm('precip2_17lev')[i] for i in (0,1,2,4,5,6,7,8,10,12,13,14,15,16,17)]+['#777777', '#AAAAAA', '#CCCCCC', '#EEEEEE']+[readNCLcm('sunshine_diff_12lev')[i] for i in (4,2,1)], 'fname':['rainnc'], 'filename':'diag' }, + 'precip-48hr' :{ 'levels' : [0,0.01,0.05,0.1,0.25,0.5,0.75,1.0,1.25,1.5,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0,12.0,13.0], 'cmap': [readNCLcm('precip2_17lev')[i] for i in (0,1,2,4,5,6,7,8,10,12,13,14,15,16,17)]+['#777777', '#AAAAAA', '#CCCCCC', '#EEEEEE']+[readNCLcm('sunshine_diff_12lev')[i] for i in (4,2,1)], 'fname':['rainnc'], 'filename':'diag' }, + 'precipacc' :{ 'levels' : [0,0.01,0.05,0.1,0.25,0.5,0.75,1.0,1.25,1.5,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0,11.0,12.0,13.0], 'cmap': [readNCLcm('precip2_17lev')[i] for i in (0,1,2,4,5,6,7,8,10,12,13,14,15,16,17)]+['#777777', '#AAAAAA', '#CCCCCC', '#EEEEEE']+[readNCLcm('sunshine_diff_12lev')[i] for i in (4,2,1)], 'fname':['rainnc'], 'filename':'diag' }, 'sbcape' :{ 'levels' : [100,250,500,750,1000,1250,1500,1750,2000,2500,3000,3500,4000,4500,5000,5500,6000], 'cmap' : ['#eeeeee', '#dddddd', '#cccccc', '#aaaaaa']+readNCLcm('precip2_17lev')[3:-1], 'fname': ['sbcape'], 'filename':'diag' }, 'mlcape' :{ 'levels' : [100,250,500,750,1000,1250,1500,1750,2000,2500,3000,3500,4000,4500,5000,5500,6000], 'cmap' : ['#eeeeee', '#dddddd', '#cccccc', '#aaaaaa']+readNCLcm('precip2_17lev')[3:-1], 'fname': ['mlcape'], 'filename':'diag' }, 'mucape' :{ 'levels' : [10,25,50,100,250,500,750,1000,1250,1500,1750,2000,2500,3000,3500,4000,4500,5000,5500,6000], - 'cmap' : ['#eeeeee', '#dddddd', '#cccccc', '#aaaaaa']+readNCLcm('precip2_17lev')[3:-1], 'fname': ['mucape'], 'filename':'diag' }, + 'cmap' : ['#eeeeee', '#dddddd', '#cccccc', '#aaaaaa']+readNCLcm('precip2_17lev')[3:-1], 'fname': ['cape'], 'filename':'diag' }, 'sbcinh' :{ 'levels' : [50,75,100,150,200,250,500], 'cmap': readNCLcm('topo_15lev')[1:], 'fname': ['sbcin'], 'filename':'diag' }, 'mlcinh' :{ 'levels' : [50,75,100,150,200,250,500], 'cmap': readNCLcm('topo_15lev')[1:], 'fname': ['mlcin'], 'filename':'diag' }, 'pwat' :{ 'levels' : [0.25,0.5,0.75,1.0,1.25,1.5,1.75,2.0,2.5,3.0,3.5,4.0], 'cmap' : ['#dddddd', '#cccccc', '#e1e1d3', '#e1d5b1', '#ffffd5', '#e5ffa7', '#addd8e', '#41ab5d', '#007837', '#005529', '#0029b1'], 'fname' : ['precipw'], 'filename':'diag'}, 't2' :{ 'levels' : [-35,-30,-25,-20,-15,-10,-5,0,5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100,105,110,115,120], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['t2m'], 'filename':'diag' }, - 't2depart' :{ 'levels' : [-35,-30,-25,-20,-15,-10,-5,0,5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100,105,110,115,120], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['t2m'] }, - #'t2' :{ 'levels' : [-35,-30,-25,-20,-15,-10,-5,0,5,10,15,20,25,30,32,35,40,45,50,55,60,65,70,75,80,85,90,95,100,105,110,115,120], \ - #'cmap': readNCLcm('MPL_Greys')[20:121:15][::-1]+[readNCLcm('nice_gfdl')[i] for i in (40,50,60,65,70,75,85,100,110,115,120,130,135,140,145,150,155,167,177,190,197)]+readNCLcm('MPL_copper')[20:120:20][::-1], 'fname': ['t2m'] }, - 't2-0c' :{ 'levels' : [32], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['t2m'] }, + 't2depart' :{ 'levels' : [-35,-30,-25,-20,-15,-10,-5,0,5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100,105,110,115,120], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['t2m'], 'filename':'diag' }, + 't2-0c' :{ 'levels' : [32], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['t2m'], 'filename':'diag' }, 'mslp' :{ 'levels' : [960,964,968,972,976,980,984,988,992,996,1000,1004,1008,1012,1016,1020,1024,1028,1032,1036,1040,1044,1048,1052], 'cmap':readNCLcm('nice_gfdl')[3:193], 'fname':['mslp'], 'filename': 'diag' }, 'td2' :{ 'levels' : [-35,-30,-25,-20,-15,-10,-5,0,5,10,15,20,25,30,35,40,45,50,55,60,64,68,72,76,80,84], 'cmap':['#ad598a', '#c589ac','#dcb8cd','#e7cfd1','#d0a0a4','#ad5960', '#8b131d', '#8b4513','#ad7c59', '#c5a289','#dcc7b8','#eeeeee', '#dddddd', '#bbbbbb', '#e1e1d3', '#e1d5b1','#ccb77a','#ffffe5','#f7fcb9', '#addd8e', '#41ab5d', '#006837', '#004529', '#195257', '#4c787c'], - #'cmap':['#f3e7e8','#d0a0a4','#ad5960', '#8b131d', '#96572a','#ad7c59', '#c5a289','#dcc7b8','#eeeeee', '#dddddd', '#bbbbbb', '#e1e1d3', '#e1d5b1','#ccb77a','#ffffe5','#f7fcb9', '#addd8e', '#41ab5d', '#006837', '#004529', '#195257', '#4c787c'], - 'fname' : ['DEWPOINT_2M'], 'filename':'upp'}, - #'td2' :{ 'levels' : [20,25,30,35,40,45,50,55,60,64,68,72,76,80,84], 'cmap' : ['#eeeeee', '#dddddd', '#bbbbbb', '#e1e1d3', '#e1d5b1','#ccb77a','#ffffe5','#f7fcb9', '#addd8e', '#41ab5d', '#006837', '#004529', '#195257', '#4c787c'], - # 'fname' : ['DEWPOINT_2M'], 'filename':'upp'}, + 'fname' : ['dewpoint_surface'], 'filename':'diag'}, 'td2depart' :{ 'levels' : [20,25,30,35,40,45,50,55,60,64,68,72,76,80,84], 'cmap' : ['#eeeeee', '#dddddd', '#bbbbbb', '#e1e1d3', '#e1d5b1','#ccb77a','#ffffe5','#f7fcb9', '#addd8e', '#41ab5d', '#006837', '#004529', '#195257', '#4c787c'], - 'fname' : ['DEWPOINT_2M'], 'filename':'upp'}, + 'fname' : ['dewpoint_surface'], 'filename':'diag'}, 'thetae' :{ 'levels' : [300,305,310,315,320,325,330,335,340,345,350,355,360], 'cmap': ['#eeeeee', '#dddddd', '#cccccc', '#aaaaaa']+readNCLcm('precip2_17lev')[3:-1], 'fname' : ['t2m', 'q2', 'surface_pressure'], 'filename': 'diag'}, + 'thetapv' :{ 'levels' : np.arange(278,386,4), 'cmap': readNCLcm('WhiteBlueGreenYellowRed'), 'fname' : ['theta_pv'], 'filename': 'diag'}, 'rh2m' :{ 'levels' : [0,10,20,30,40,50,60,70,80,90,100,110], 'cmap': readNCLcm('precip2_17lev')[:17][::-1], 'fname': ['t2m', 'surface_pressure', 'q2'], 'filename': 'diag'}, 'pblh' :{ 'levels' : [0,250,500,750,1000,1250,1500,1750,2000,2500,3000,3500,4000], - 'cmap': ['#eeeeee', '#dddddd', '#cccccc', '#bbbbbb', '#44aaee','#88bbff', '#aaccff', '#bbddff', '#efd6c1', '#e5c1a1', '#eebb32', '#bb9918'], 'fname': ['hpbl'] }, + 'cmap': ['#eeeeee', '#dddddd', '#cccccc', '#bbbbbb', '#44aaee','#88bbff', '#aaccff', '#bbddff', '#efd6c1', '#e5c1a1', '#eebb32', '#bb9918'], 'fname': ['hpbl'], 'filename': 'diag'}, 'hmuh' :{ 'levels' : [10,25,50,75,100,125,150,175,200,250,300,400,500], 'cmap': readNCLcm('prcp_1')[1:15], 'fname': ['updraft_helicity_max'], 'filename':'diag'}, 'hmneguh' :{ 'levels' : [10,25,50,75,100,125,150,175,200,250,300,400,500], 'cmap': readNCLcm('prcp_1')[1:15], 'fname': ['UP_HELI_MIN'], 'filename':'diag'}, 'hmuh03' :{ 'levels' : [10,25,50,75,100,125,150,175,200,250,300,400,500], 'cmap': readNCLcm('prcp_1')[1:15], 'fname': ['updraft_helicity_max03'], 'filename':'diag'}, 'hmuh01' :{ 'levels' : [5,10,25,50,75,100,125,150,175,200,250,300,400,500], 'cmap': readNCLcm('prcp_1')[1:15], 'fname': ['updraft_helicity_max01'], 'filename':'diag'}, - #'rvort1' :{ 'levels' : [0.005,0.006,0.007,0.008,0.009,0.01,0.012,0.014,0.016,0.018,0.02,0.021], 'cmap': readNCLcm('prcp_1')[:15], 'fname': ['rvort1_max'], 'filename':'diag'}, 'rvort1' :{ 'levels' : [0.005,0.006,0.007,0.008,0.009,0.01,0.011,0.012,0.013,0.014,0.015], 'cmap': readNCLcm('prcp_1')[1:15], 'fname': ['rvort1_max'], 'filename':'diag'}, 'sspf' :{ 'levels' : [10,25,50,75,100,125,150,175,200,250,300,400,500], 'cmap': readNCLcm('prcp_1')[:15], 'fname': ['updraft_helicity_max','WSPD10MAX','HAIL_MAXK1'], 'filename':'diag'}, - 'hmup' :{ 'levels' : [4,6,8,10,12,14,16,18,20,24,28,32,36,40,44,48], 'cmap': readNCLcm('prcp_1')[1:16], 'fname': ['w_velicity_max'], 'filename':'diag' }, - #'hmdn' :{ 'levels' : [-19,-17,-15,-13,-11,-9,-7,-5,-3,-1,0], 'cmap': readNCLcm('prcp_1')[16:1:-1]+['#ffffff'], 'fname': ['W_DN_MAX'], 'filename':'diag' }, - 'hmdn' :{ 'levels' : [2,3,4,6,8,10,12,14,16,18,20,22,24,26,28,30], 'cmap': readNCLcm('prcp_1')[1:16], 'fname': ['W_DN_MAX'], 'filename':'diag' }, - 'hmwind' :{ 'levels' : [10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42], 'cmap': readNCLcm('prcp_1')[:16], 'fname': ['WSPD10MAX'], 'filename':'diag' }, + 'hmup' :{ 'levels' : [4,6,8,10,12,14,16,18,20,24,28,32,36,40,44,48], 'cmap': readNCLcm('prcp_1')[1:16], 'fname': ['w_velocity_max'], 'filename':'diag' }, + #'hmdn' :{ 'levels' : [-19,-17,-15,-13,-11,-9,-7,-5,-3,-1,0], 'cmap': readNCLcm('prcp_1')[16:1:-1]+['#ffffff'], 'fname': ['w_velocity_min'], 'filename':'diag' }, + 'hmdn' :{ 'levels' : [2,3,4,6,8,10,12,14,16,18,20,22,24,26,28,30], 'cmap': readNCLcm('prcp_1')[1:16], 'fname': ['w_velocity_min'], 'filename':'diag' }, + #'hmwind' :{ 'levels' : [10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42], 'cmap': readNCLcm('prcp_1')[:16], 'fname': ['WSPD10MAX'], 'filename':'diag' }, + 'hmwind' :{ 'levels' : [10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42], 'cmap': readNCLcm('prcp_1')[:16], 'fname': ['wind_speed_level1_max'], 'filename':'diag' }, #'hmwind' :{ 'levels' : [10,12,14,16,18,20,22,24,26,28,30,32,34], 'cmap': readNCLcm('prcp_1')[1:15], 'fname': ['WSPD10MAX'], 'filename':'diag' }, #'hmwind' :{ 'levels' : [20,25,30,35,40,45,50,55,60,65,70,75,80,85], 'cmap': readNCLcm('prcp_1')[1:16], 'fname': ['WSPD10MAX'], 'filename':'diag' }, - 'hmgrp' :{ 'levels' : [0.01,0.1,0.25,0.5,0.75,1.0,1.5,2.0,2.5,3.0,4.0,5.0], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['GRPL_MAX'], 'filename':'diag' }, - #'cref' :{ 'levels' : [5,10,15,20,25,30,35,40,45,50,55,60,65,70], 'cmap': readcm('cmap_rad.rgb')[1:14], 'fname': ['REFL_10CM'], 'arraylevel':'max' }, - 'cref' :{ 'levels' : [5,10,15,20,25,30,35,40,45,50,55,60,65,70], 'cmap': readcm('cmap_rad.rgb')[1:14], 'fname': ['REFD_COM'], 'filename':'diag' }, - 'lmlref' :{ 'levels' : [5,10,15,20,25,30,35,40,45,50,55,60,65,70], 'cmap': readcm('cmap_rad.rgb')[1:14], 'fname': ['REFL_10CM'], 'arraylevel':0 }, - #'ref1km' :{ 'levels' : [5,10,15,20,25,30,35,40,45,50,55,60,65,70], 'cmap': readcm('cmap_rad.rgb')[1:14], 'fname': ['REFL_1KM_AGL'], 'filename':'upp' }, - 'ref1km' :{ 'levels' : [5,10,15,20,25,30,35,40,45,50,55,60,65,70], 'cmap': readcm('cmap_rad.rgb')[1:14], 'fname': ['refl10cm_max'], 'filename':'diag' }, - 'echotop' :{ 'levels' : [1000,5000,10000,15000,20000,25000,30000,35000,40000,45000,50000,55000,60000,65000], 'cmap': readNCLcm('precip3_16lev')[1::], 'fname': ['ECHOTOP'], 'filename':'diag' }, - 'srh3' :{ 'levels' : [50,100,150,200,250,300,400,500], 'cmap': readNCLcm('perc2_9lev'), 'fname': ['srh_0_3km'], 'filename' : 'upp' }, - 'srh1' :{ 'levels' : [50,100,150,200,250,300,400,500], 'cmap': readNCLcm('perc2_9lev'), 'fname': ['srh_0_1km'], 'filename' : 'upp' }, - 'shr06mag' :{ 'levels' : [30,35,40,45,50,55,60,65,70,75,80], 'cmap': readNCLcm('perc2_9lev'), 'fname': ['UBSHR6', 'VBSHR6'], 'filename':'upp' }, - 'shr01mag' :{ 'levels' : [10,15,20,25,30,35,40,45,50,55], 'cmap': readNCLcm('perc2_9lev'), 'fname': ['UBSHR1', 'VBSHR1'], 'filename':'upp' }, + 'hmgrp' :{ 'levels' : [0.01,0.1,0.25,0.5,0.75,1.0,1.5,2.0,2.5,3.0,4.0,5.0], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['grpl_max'], 'filename':'diag' }, + 'cref' :{ 'levels' : [5,10,15,20,25,30,35,40,45,50,55,60,65,70], 'cmap': readcm('cmap_rad.rgb')[1:14], 'fname': ['refl10cm_max'], 'filename':'diag' }, + 'ref1km' :{ 'levels' : [5,10,15,20,25,30,35,40,45,50,55,60,65,70], 'cmap': readcm('cmap_rad.rgb')[1:14], 'fname': ['refl10cm_1km'], 'filename':'diag' }, + 'srh3' :{ 'levels' : [50,100,150,200,250,300,400,500], 'cmap': readNCLcm('perc2_9lev'), 'fname': ['srh_0_3km'], 'filename' : 'diag' }, + 'srh1' :{ 'levels' : [50,100,150,200,250,300,400,500], 'cmap': readNCLcm('perc2_9lev'), 'fname': ['srh_0_1km'], 'filename' : 'diag' }, + 'shr06mag' :{ 'levels' : [30,35,40,45,50,55,60,65,70,75,80], 'cmap': readNCLcm('perc2_9lev'), 'fname': ['uzonal_6km', 'umeridional_6km', 'uzonal_surface', 'umeridional_surface'], 'filename':'diag'}, + 'shr01mag' :{ 'levels' : [30,35,40,45,50,55,60,65,70,75,80], 'cmap': readNCLcm('perc2_9lev'), 'fname': ['uzonal_1km', 'umeridional_1km', 'uzonal_surface', 'umeridional_surface'], 'filename':'diag'}, 'zlfc' :{ 'levels' : [0,250,500,750,1000,1250,1500,2000,2500,3000,3500,4000,5000], 'cmap': [readNCLcm('nice_gfdl')[i] for i in [3,20,37,54,72,89,106,123,141,158,175,193]], 'fname': ['lfc'], 'filename':'diag' }, 'zlcl' :{ 'levels' : [0,250,500,750,1000,1250,1500,2000,2500,3000,3500,4000,5000], 'cmap': [readNCLcm('nice_gfdl')[i] for i in [3,20,37,54,72,89,106,123,141,158,175,193]], 'fname': ['lcl'], 'filename':'diag' }, 'ltg1' :{ 'levels' : [0.1,0.5,1,1.5,2,2.5,3,4,5,6,7,8,10,12], 'cmap': readNCLcm('prcp_1')[:15], 'fname': ['LTG1_MAX'], 'filename':'diag' }, 'ltg2' :{ 'levels' : [0.1,0.5,1,1.5,2,2.5,3,4,5,6,7,8,10,12], 'cmap': readNCLcm('prcp_1')[:15], 'fname': ['LTG2_MAX'], 'filename':'diag' }, 'ltg3' :{ 'levels' : [0.1,0.5,1,1.5,2,2.5,3,4,5,6,7,8,10,12], 'cmap': readNCLcm('prcp_1')[:15], 'fname': ['LTG3_MAX'], 'filename':'diag' }, - 'bmin' :{ 'levels' : [-20,-16,-12,-10,-8,-6,-4,-2,-1,-0.5,0,0.5], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['MLBMIN'], 'filename':'upp' }, - 'pbmin' :{ 'levels' : [0,30,60,90,120,150,180],'cmap': ['#dddddd', '#aaaaaa']+readNCLcm('precip2_17lev')[3:-1], 'fname': ['MLPBMIN','PBMIN_SFC'], 'filename':'upp' }, + 'olrtoa' :{ 'levels' : range(70,340,10), 'cmap': readcm('cmap_satir.rgb')[32:1:-1], 'fname': ['olrtoa'], 'filename':'diag' }, # winter fields - 'freezelev' :{ 'levels' : [0, 500, 1000, 1500, 2000, 2500, 3000, 3500, 4000, 4500, 5000], 'cmap':readNCLcm('nice_gfdl')[3:193], 'fname':['FZLEV'], 'filename':'diag'}, 'thck1000-500' :{ 'levels' : [480,486,492,498,504,510,516,522,528,534,540,546,552,558,564,570,576,582,588,592,600], 'cmap':readNCLcm('perc2_9lev'), 'fname':['GHT_PL', 'GHT_PL'], 'arraylevel':[0,5], 'filename':'diag'}, # CSS mod 'thck1000-850' :{ 'levels' : [82,85,88,91,94,97,100,103,106,109,112,115,118,121,124,127,130,133,136,139,142,145,148,151,154,157,160], 'cmap':readNCLcm('perc2_9lev'), 'fname':['GHT_PL', 'GHT_PL'], 'arraylevel':[0,2], 'filename':'diag'}, # CSS mod # pressure level entries - 'hgt250' :{ 'levels' : [9700,9760,9820,9880,9940,10000,10060,10120,10180,10240,10300,10360,10420,10480,10540,10600,10660,10720,10780,10840,10900,10960,11020], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['GHT_PL'], 'filename':'diag', 'arraylevel':8 }, - 'hgt300' :{ 'levels' : [8400,8460,8520,8580,8640,8700,8760,8820,8880,8940,9000,9060,9120,9180,9240,9300,9360,9420,9480,9540,9600,9660,9720,9780,9840,9900,9960,10020], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['GHT_PL'], 'filename':'diag', 'arraylevel':7 }, + 'hgt200' :{ 'levels' : list(range(10900,12500,60)), 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['height_200hPa'], 'filename':'diag'}, + 'hgt250' :{ 'levels' : [9700,9760,9820,9880,9940,10000,10060,10120,10180,10240,10300,10360,10420,10480,10540,10600,10660,10720,10780,10840,10900,10960,11020], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['height_250hPa'], 'filename':'diag'}, + 'hgt300' :{ 'levels' : [8400,8460,8520,8580,8640,8700,8760,8820,8880,8940,9000,9060,9120,9180,9240,9300,9360,9420,9480,9540,9600,9660,9720,9780,9840,9900,9960,10020], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['height_300hPa'], 'filename':'diag'}, 'hgt500' :{ 'levels' : [4800,4860,4920,4980,5040,5100,5160,5220,5280,5340,5400,5460,5520,5580,5640,5700,5760,5820,5880,5940,6000], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['height_500hPa'], 'filename':'diag'}, 'hgt700' :{ 'levels' : [2700,2730,2760,2790,2820,2850,2880,2910,2940,2970,3000,3030,3060,3090,3120,3150,3180,3210,3240,3270,3300], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['height_700hPa'], 'filename':'diag'}, - 'hgt850' :{ 'levels' : [1200,1230,1260,1290,1320,1350,1380,1410,1440,1470,1500,1530,1560,1590,1620,1650], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['GHT_PL'], 'filename':'diag', 'arraylevel':2 }, - 'hgt925' :{ 'levels' : [550,580,610,640,670,700,730,760,790,820,850,880,910,940,970,1000,1030], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['GHT_PL'], 'filename':'diag', 'arraylevel':1 }, - 'speed250' :{ 'levels' : [10,20,30,40,50,60,70,80,90,100,110,120,130,140,150,160,170], 'cmap': readNCLcm('wind_17lev'), 'fname': ['S_PL'], 'filename':'diag', 'arraylevel':8 }, - 'speed300' :{ 'levels' : [10,20,30,40,50,60,70,80,90,100,110,120,130,140,150,160,170], 'cmap': readNCLcm('wind_17lev'), 'fname': ['S_PL'], 'filename':'diag', 'arraylevel':7 }, + 'hgt850' :{ 'levels' : [1200,1230,1260,1290,1320,1350,1380,1410,1440,1470,1500,1530,1560,1590,1620,1650], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['height_850hPa'], 'filename':'diag'}, + 'hgt925' :{ 'levels' : [550,580,610,640,670,700,730,760,790,820,850,880,910,940,970,1000,1030], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['height_925hPa'], 'filename':'diag'}, # RAS: adjusted these ranges - need to capture higher wind speeds - #'speed500' :{ 'levels' : [5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85], 'cmap': readNCLcm('wind_17lev'), 'fname': ['S_PL'], 'filename':'diag', 'arraylevel':5 }, - 'speed500' :{ 'levels' : [15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100], 'cmap': readNCLcm('wind_17lev'), 'fname': ['S_PL'], 'filename':'diag', 'arraylevel':5 }, - #'speed700' :{ 'levels' : [4,8,12,16,20,24,28,32,36,40,44,48,52,56,60,64,68], 'cmap': readNCLcm('wind_17lev'), 'fname': ['S_PL'], 'filename':'diag', 'arraylevel':3 }, - 'speed700' :{ 'levels' : [5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85], 'cmap': readNCLcm('wind_17lev'), 'fname': ['S_PL'], 'filename':'diag', 'arraylevel':3 }, - #'speed850' :{ 'levels' : [3,6,9,12,15,18,21,24,27,30,33,36,39,42,45,48,51], 'cmap': readNCLcm('wind_17lev'), 'fname': ['S_PL'], 'filename':'diag', 'arraylevel':2 }, - 'speed850' :{ 'levels' : [6,10,14,18,22,26,30,34,38,42,46,50,54,58,62,66,70,74], 'cmap': readNCLcm('wind_17lev'), 'fname': ['S_PL'], 'filename':'diag', 'arraylevel':2 }, - #'speed925' :{ 'levels' : [3,6,9,12,15,18,21,24,27,30,33,36,39,42,45,48,51], 'cmap': readNCLcm('wind_17lev'), 'fname': ['S_PL'], 'filename':'diag', 'arraylevel':1 }, - 'speed925' :{ 'levels' : [6,10,14,18,22,26,30,34,38,42,46,50,54,58,62,66,70,74], 'cmap': readNCLcm('wind_17lev'), 'fname': ['S_PL'], 'filename':'diag', 'arraylevel':1 }, - 'temp250' :{ 'levels' : [-65,-63,-61,-59,-57,-55,-53,-51,-49,-47,-45,-43,-41,-39,-37,-35,-33,-31,-29], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['T_PL'], 'filename':'diag', 'arraylevel':8 }, - 'temp300' :{ 'levels' : [-65,-63,-61,-59,-57,-55,-53,-51,-49,-47,-45,-43,-41,-39,-37,-35,-33,-31,-29], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['T_PL'], 'filename':'diag', 'arraylevel':7 }, - 'temp500' :{ 'levels' : [-41,-39,-37,-35,-33,-31,-29,-26,-23,-20,-17,-14,-11,-8,-5,-2], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['T_PL'], 'filename':'diag', 'arraylevel':5 }, - 'temp700' :{ 'levels' : [-36,-33,-30,-27,-24,-21,-18,-15,-12,-9,-6,-3,0,3,6,9,12,15,18,21], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['T_PL'], 'filename':'diag', 'arraylevel':3 }, - 'temp850' :{ 'levels' : [-30,-27,-24,-21,-18,-15,-12,-9,-6,-3,0,3,6,9,12,15,18,21,24,27,30], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['T_PL'], 'filename':'diag', 'arraylevel':2 }, - 'temp925' :{ 'levels' : [-24,-21,-18,-15,-12,-9,-6,-3,0,3,6,9,12,15,18,21,24,27,30,33], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['T_PL'], 'filename':'diag', 'arraylevel':1 }, - 'td700' :{ 'levels' : [-30,-25,-20,-15,-10,-5,0,5,10], 'cmap' : readNCLcm('nice_gfdl')[3:193], 'fname': ['TD_PL'], 'filename':'diag', 'arraylevel':3 }, - 'td850' :{ 'levels' : [-40,-35,-30,-25,-20,-15,-10,-5,0,5,10,15,20,25,30], 'cmap' : readNCLcm('nice_gfdl')[3:193], 'fname': ['TD_PL'], 'filename':'diag', 'arraylevel':2 }, - 'td925' :{ 'levels' : [-30,-25,-20,-15,-10,-5,0,5,10,15,20,25,30], 'cmap' : readNCLcm('nice_gfdl')[3:193], 'fname': ['TD_PL'], 'filename':'diag', 'arraylevel':1 }, - 'rh300' :{ 'levels' : [0,10,20,30,40,50,60,70,80,90,100], 'cmap' : [readNCLcm('MPL_PuOr')[i] for i in (2,18,34,50)]+[readNCLcm('MPL_Greens')[j] for j in (2,17,50,75,106,125)], 'fname': ['RH_PL'], 'filename':'diag', 'arraylevel':7 }, - 'rh500' :{ 'levels' : [0,10,20,30,40,50,60,70,80,90,100], 'cmap' : [readNCLcm('MPL_PuOr')[i] for i in (2,18,34,50)]+[readNCLcm('MPL_Greens')[j] for j in (2,17,50,75,106,125)], 'fname': ['RH_PL'], 'filename':'diag', 'arraylevel':5 }, - 'rh700' :{ 'levels' : [0,10,20,30,40,50,60,70,80,90,100], 'cmap' : readNCLcm('CBR_drywet'), 'fname': ['RH_PL'], 'filename':'diag', 'arraylevel':3 }, - 'rh850' :{ 'levels' : [0,10,20,30,40,50,60,70,80,90,100], 'cmap' : readNCLcm('CBR_drywet'), 'fname': ['RH_PL'], 'filename':'diag', 'arraylevel':2 }, - 'rh925' :{ 'levels' : [0,10,20,30,40,50,60,70,80,90,100], 'cmap' : readNCLcm('CBR_drywet'), 'fname': ['RH_PL'], 'filename':'diag', 'arraylevel':1 }, - 'avo500' :{ 'levels' : [0,9,12,15,18,21,24,27,30,33], 'cmap': readNCLcm('prcp_1'), 'fname': ['AVORT_PL'], 'filename':'diag', 'arraylevel':5 }, + 'speed200' :{ 'levels' : [25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100,105,110], 'cmap': readNCLcm('wind_17lev'), 'fname': ['uzonal_200hPa','umeridional_200hPa'], 'filename':'diag'}, + 'speed250' :{ 'levels' : [25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100,105,110], 'cmap': readNCLcm('wind_17lev'), 'fname': ['uzonal_250hPa','umeridional_250hPa'], 'filename':'diag'}, + 'speed500' :{ 'levels' : [15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100], 'cmap': readNCLcm('wind_17lev'), 'fname': ['uzonal_500hPa','umeridional_500hPa'], 'filename':'diag'}, + 'speed700' :{ 'levels' : [5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85], 'cmap': readNCLcm('wind_17lev'), 'fname': ['uzonal_700hPa','umeridional_700hPa'], 'filename':'diag'}, + 'speed850' :{ 'levels' : [6,10,14,18,22,26,30,34,38,42,46,50,54,58,62,66,70,74], 'cmap': readNCLcm('wind_17lev'), 'fname': ['uzonal_850hPa','umeridional_850hPa'], 'filename':'diag'}, + 'speed925' :{ 'levels' : [6,10,14,18,22,26,30,34,38,42,46,50,54,58,62,66,70,74], 'cmap': readNCLcm('wind_17lev'), 'fname': ['uzonal_925hPa','umeridional_925hPa'], 'filename':'diag'}, + 'temp200' :{ 'levels' : [-65,-63,-61,-59,-57,-55,-53,-51,-49,-47,-45,-43,-41,-39,-37,-35,-33,-31,-29], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['temperature_200hPa'], 'filename':'diag'}, + 'temp250' :{ 'levels' : [-65,-63,-61,-59,-57,-55,-53,-51,-49,-47,-45,-43,-41,-39,-37,-35,-33,-31,-29], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['temperature_250hPa'], 'filename':'diag'}, + 'temp300' :{ 'levels' : [-65,-63,-61,-59,-57,-55,-53,-51,-49,-47,-45,-43,-41,-39,-37,-35,-33,-31,-29], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['temperature_300hPa'], 'filename':'diag'}, + 'temp500' :{ 'levels' : [-41,-39,-37,-35,-33,-31,-29,-26,-23,-20,-17,-14,-11,-8,-5,-2], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['temperature_500hPa'], 'filename':'diag'}, + 'temp700' :{ 'levels' : [-36,-33,-30,-27,-24,-21,-18,-15,-12,-9,-6,-3,0,3,6,9,12,15,18,21], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['temperature_700hPa'], 'filename':'diag'}, + 'temp850' :{ 'levels' : [-30,-27,-24,-21,-18,-15,-12,-9,-6,-3,0,3,6,9,12,15,18,21,24,27,30], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['temperature_850hPa'], 'filename':'diag'}, + 'temp925' :{ 'levels' : [-24,-21,-18,-15,-12,-9,-6,-3,0,3,6,9,12,15,18,21,24,27,30,33], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['temperature_925hPa'], 'filename':'diag'}, + 'td700' :{ 'levels' : [-30,-25,-20,-15,-10,-5,0,5,10], 'cmap' : readNCLcm('nice_gfdl')[3:193], 'fname': ['dewpoint_700hPa'], 'filename':'diag'}, + 'td850' :{ 'levels' : [-40,-35,-30,-25,-20,-15,-10,-5,0,5,10,15,20,25,30], 'cmap' : readNCLcm('nice_gfdl')[3:193], 'fname': ['dewpoint_850hPa'], 'filename':'diag'}, + 'td925' :{ 'levels' : [-30,-25,-20,-15,-10,-5,0,5,10,15,20,25,30], 'cmap' : readNCLcm('nice_gfdl')[3:193], 'fname': ['dewpoint_925hPa'], 'filename':'diag'}, + 'vort500' :{ 'levels' : [0,9,12,15,18,21,24,27,30,33], 'cmap': readNCLcm('prcp_1'), 'fname': ['vorticity_500hPa'], 'filename':'diag'}, + 'vort850' :{ 'levels' : [0,9,12,15,18,21,24,27,30,33], 'cmap': readNCLcm('prcp_1'), 'fname': ['vorticity_850hPa'], 'filename':'diag'}, + 'vort700' :{ 'levels' : [0,9,12,15,18,21,24,27,30,33], 'cmap': readNCLcm('prcp_1'), 'fname': ['vorticity_700hPa'], 'filename':'diag'}, + 'vortpv' :{ 'levels' : [0,9,12,15,18,21,24,27,30,33], 'cmap': readNCLcm('prcp_1'), 'fname': ['vort_pv'], 'filename':'diag'}, + 'rh300' :{ 'levels' : [0,10,20,30,40,50,60,70,80,90,100], 'cmap' : [readNCLcm('MPL_PuOr')[i] for i in (2,18,34,50)]+[readNCLcm('MPL_Greens')[j] for j in (2,17,50,75,106,125)], 'fname': ['relhum_300hPa'], 'filename':'diag'}, + 'rh500' :{ 'levels' : [0,10,20,30,40,50,60,70,80,90,100], 'cmap' : [readNCLcm('MPL_PuOr')[i] for i in (2,18,34,50)]+[readNCLcm('MPL_Greens')[j] for j in (2,17,50,75,106,125)], 'fname': ['relhum_500hPa'], 'filename':'diag'}, + 'rh700' :{ 'levels' : [0,10,20,30,40,50,60,70,80,90,100], 'cmap' : readNCLcm('CBR_drywet'), 'fname': ['relhum_700hPa'], 'filename':'diag'}, + 'rh850' :{ 'levels' : [0,10,20,30,40,50,60,70,80,90,100], 'cmap' : readNCLcm('CBR_drywet'), 'fname': ['relhum_850hPa'], 'filename':'diag'}, + 'rh925' :{ 'levels' : [0,10,20,30,40,50,60,70,80,90,100], 'cmap' : readNCLcm('CBR_drywet'), 'fname': ['relhum_925hPa'], 'filename':'diag'}, 'pvort320k' :{ 'levels' : [0,0.1,0.2,0.3,0.4,0.5,0.75,1,1.5,2,3,4,5,7,10], 'cmap' : ['#ffffff','#eeeeee','#dddddd','#cccccc','#bbbbbb','#d1c5b1','#e1d5b9','#f1ead3','#003399','#0033FF','#0099FF','#00CCFF','#8866FF','#9933FF','#660099'], 'fname': ['PVORT_320K'], 'filename':'upp' }, - 'bunkmag' :{ 'levels' : [20,25,30,35,40,45,50,55,60], 'cmap':readNCLcm('wind_17lev')[1:], 'fname':['U_COMP_STM_6KM', 'V_COMP_STM_6KM'], 'filename':'upp' }, 'speed10m' :{ 'levels' : [3,6,9,12,15,18,21,24,27,30,33,36,39,42,45,48,51], 'cmap': readNCLcm('wind_17lev')[1:],'fname' : ['u10', 'v10'], 'filename':'diag'}, 'speed10m-tc' :{ 'levels' : [6,12,18,24,30,36,42,48,54,60,66,72,78,84,90,96,102], 'cmap': readNCLcm('wind_17lev')[1:],'fname' : ['u10', 'v10'], 'filename':'diag'}, - 'stp' :{ 'levels' : [0.5,0.75,1.0,1.5,2.0,3.0,4.0,5.0,6.0,7.0,8.0], 'cmap':readNCLcm('perc2_9lev'), 'fname':['sbcape','lcl','srh_0_1km','UBSHR6','VBSHR6'], 'arraylevel':[None,None,None,None,None], 'filename':'diag'}, + 'stp' :{ 'levels' : [0.5,0.75,1.0,1.5,2.0,3.0,4.0,5.0,6.0,7.0,8.0], 'cmap':readNCLcm('perc2_9lev'), 'fname':['sbcape','lcl','srh_0_1km','uzonal_6km','umeridional_6km','uzonal_surface','umeridional_surface'], 'filename':'diag'}, 'uhratio' :{ 'levels' : [0.1,0.3,0.5,0.7,0.9,1.0,1.1,1.2,1.3,1.4,1.5], 'cmap':readNCLcm('perc2_9lev'), 'fname':['updraft_helicity_max03', 'updraft_helicity_max'], 'filename':'diag'}, - 'winter' :{ 'levels' : [0.01,0.1,0.25,0.5,0.75,1,1.5,2,2.5,3,3.5,4], 'cmap':['#dddddd','#aaaaaa']+readNCLcm('precip3_16lev')[1:], 'fname':['AFWA_RAIN_HRLY', 'AFWA_FZRA_HRLY', 'AFWA_ICE_HRLY', 'AFWA_SNOWFALL_HRLY'], 'filename':'wrfout'}, - 'crefuh' :{ 'levels' : [5,10,15,20,25,30,35,40,45,50,55,60,65,70], 'cmap': readcm('cmap_rad.rgb')[0:13], 'fname': ['refl10cm_max', 'updraft_helicity_max'], 'filename':'diag' }, + 'crefuh' :{ 'levels' : [5,10,15,20,25,30,35,40,45,50,55,60,65,70], 'cmap': readcm('cmap_rad.rgb')[0:13], 'fname': ['refl10cm_max', 'updraft_helicity_max'], 'filename':'diag' }, # wind barb entries - 'wind10m' :{ 'fname' : ['u10', 'v10'], 'filename':'diag', 'skip':40 }, - 'wind500' :{ 'fname' : ['uzonal_500hPa', 'umeridional_500hPa'], 'filename':'diag', 'skip':40 }, - 'wind700' :{ 'fname' : ['uzonal_700hPa', 'umeridional_700hPa'], 'filename':'diag', 'skip':40 }, - 'shr06' :{ 'fname' : ['UBSHR6','VBSHR6'], 'filename': 'upp', 'skip':40 }, - 'shr01' :{ 'fname' : ['UBSHR1', 'VBSHR1'], 'filename': 'upp', 'skip':40 }, - 'bunkers' :{ 'fname' : ['U_COMP_STM_6KM', 'V_COMP_STM_6KM'], 'filename': 'upp', 'skip':40 }, + 'wind10m' :{ 'fname' : ['u10', 'v10'], 'filename':'diag', 'skip':50 }, + 'windsfc' :{ 'fname' : ['uzonal_surface','umeridional_surface'], 'filename': 'diag', 'skip':50 }, + 'wind1km' :{ 'fname' : ['uzonal_1km','umeridional_1km'], 'filename': 'diag', 'skip':50 }, + 'wind6km' :{ 'fname' : ['uzonal_6km','umeridional_6km'], 'filename': 'diag', 'skip':50 }, + 'windpv' :{ 'fname' : ['u_pv','v_pv'], 'filename': 'diag', 'skip':50 }, + 'shr06' :{ 'fname' : ['uzonal_6km','umeridional_6km','uzonal_surface','umeridional_surface'], 'filename': 'diag', 'skip':50 }, + 'shr01' :{ 'fname' : ['uzonal_1km','umeridional_1km','uzonal_surface','umeridional_surface'], 'filename': 'diag', 'skip':50 }, } +# Enter wind barb info for list of pressure levels +for plev in ['200', '250', '300', '500', '700', '850', '925']: + fieldinfo['wind'+plev] = { 'fname' : ['uzonal_'+plev+'hPa', 'umeridional_'+plev+'hPa'], 'filename':'diag', 'skip':50} + # Combine levels from RAIN, FZRA, ICE, and SNOW for plotting 1-hr accumulated precip for each type. Ahijevych added this #fieldinfo['ptypes']['levels'] = [fieldinfo['precip']['levels'][1:],fieldinfo['snow']['levels'],fieldinfo['ice']['levels'],fieldinfo['fzra']['levels']] # domains = { 'domainname': { 'corners':[ll_lat,ll_lon,ur_lat,ur_lon], 'figsize':[w,h] } } domains = { 'CONUS' :{ 'corners': [23.1593,-120.811,46.8857,-65.0212], 'fig_width': 1080 }, + 'NA' :{ 'corners': [15.00,-170.00,65.00,-50.00], 'fig_width':1080 }, 'SGP' :{ 'corners': [25.3,-107.00,36.00,-88.70], 'fig_width':1080 }, 'NGP' :{ 'corners': [40.00,-105.0,50.30,-82.00], 'fig_width':1080 }, 'CGP' :{ 'corners': [33.00,-107.50,45.00,-86.60], 'fig_width':1080 }, - 'TEST':{ 'corners': [36.00,-100.50,45.00,-96.60], 'fig_width':1080 }, 'SW' :{ 'corners': [28.00,-121.50,44.39,-102.10], 'fig_width':1080 }, 'NW' :{ 'corners': [37.00,-124.40,51.60,-102.10], 'fig_width':1080 }, 'SE' :{ 'corners': [26.10,-92.75,36.00,-71.00], 'fig_width':1080 }, From 7ceec963be5c3e0830e6120abef121af01f636d6 Mon Sep 17 00:00:00 2001 From: ahijevyc Date: Tue, 23 Jul 2019 09:42:18 -0600 Subject: [PATCH 28/68] Regrid MPAS while plotting Trim global mesh to cells inside lat/lon box. Interpolate vorticity from verticies to cells using mpas_vort_cell fortran program. Added toServer method to webplot class. Create output directory if needed. Save and load a lot more in the pickle file, like triangulation, weights, min_grid_spacing of mpas mesh. Subtract accumulated precipitation at different lead times for accumulated precipitation. Interpolate to lat-lon grid before spatially smoothing. In plotReflectivityUH, wrote a hack to plot zero contour if there are no other valid contours. Rotate vectors so the point the right way on the map projection. In some cases, convert xarray to numpy array. xarray is missing some methods that numpy arrays have, like flatten method. Unit conversions tied to new field names in hwt2017 output. When deriving max/min/variance/prob over threshold, etc. use a tuple with dimensions and use the splat operator *spatial_dimensions so it can deal with 2 dimensional fields (like WRF) or 1 dimensional fields (like MPAS). --- webplot.py | 421 +++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 298 insertions(+), 123 deletions(-) diff --git a/webplot.py b/webplot.py index 3b662bc..28635e0 100755 --- a/webplot.py +++ b/webplot.py @@ -2,13 +2,14 @@ import matplotlib.pyplot as plt from mpl_toolkits.basemap import * from datetime import * -import pickle as pickle +import pickle import os, sys, time, argparse import scipy.ndimage as ndimage from scipy import interpolate +from scipy.spatial import qhull import subprocess -import pdb -from scipy.interpolate import griddata +import mpas, mpas_vort_cell +import re from fieldinfo import * # To use MFDataset, first convert netcdf4 to netcdf4-classic with nccopy # for example, nccopy -d nc7 /glade/p/nsc/nmmm0046/schwartz/MPAS_ens_15-3km_mesh/POST/2017050100/ens_1/diag_latlon_g193.2017-05-02_00.00.00.nc /glade/scratch/ahijevyc/hwt2017/2017050100/ens_1/diag_latlon_g193.2017-05-02_00.00.00.nc @@ -45,25 +46,38 @@ def createFilename(self): #self.outfile = prefx+'_f'+'%03d'%self.shr+'-f'+'%03d'%self.ehr+'_'+self.domain+'_'+self.opts['date']+'.png' # 'test.png' # CSS self.outfile = prefx+'_f'+'%03d'%self.shr+'-f'+'%03d'%self.ehr+'_'+self.domain+'.png' # 'test.png' # CSS + # create yyyymmddhh/domain/ directory if needed + subdir_path = os.path.join(self.opts['date'], self.domain) + if not os.path.isdir(subdir_path): + print("webPlot.createFilename(): making new output directory "+subdir_path) + os.mkdir(subdir_path) + # prepend subdir_path to outfile. + self.outfile = os.path.join(subdir_path, self.outfile) + + def toServer(self, debug=False, url="nova.mmm.ucar.edu:/web/htdocs/projects/mpas/plots/."): + rsync_opts = '-RL' + if debug: + rsync_opts += 'v' # append a 'v' for verbose + #result = subprocess.run(['rsync', rsync_opts, '--timeout=10', '--bwlimit=3', self.outfile, url], check=True, stdout=subprocess.PIPE) # tried capture_output=True but this version of subprocess doesn't recognize it. + # send directly to koa instead of nova. Carter set up ssh keys. Not working. Get return status=14 + result = subprocess.run(['rsync', '-e', "'ssh -vi /home/ahijevyc/.ssh/id_rsa'", '-avR', self.outfile, url], check=True, stdout=subprocess.PIPE) + #The --contimeout option may only be used when connecting to an rsync daemon + if result.returncode != 0: + print(result) + def loadMap(self, overlay=False): - if overlay: - PYTHON_SCRIPTS_DIR = os.getenv('PYTHON_SCRIPTS_DIR', '/glade/u/home/sobash/RT2015_gpx') - self.fig, self.ax, self.m = pickle.load(open('%s/overlays/rt2015_overlay_%s.pk'%(PYTHON_SCRIPTS_DIR,self.domain), 'r')) - else: - PYTHON_SCRIPTS_DIR = os.getenv('PYTHON_SCRIPTS_DIR', '/glade/work/ahijevyc/share/rt_ensemble/python_scripts') - self.fig, self.ax, self.m = pickle.load(open('%s/rt2015_%s.pk'%(PYTHON_SCRIPTS_DIR,self.domain), 'rb')) - - # load lat/lons - LATLON_FILE = os.getenv('LATLON_FILE', PYTHON_SCRIPTS_DIR+'/rt2015_latlon_d02.nc') - #self.lats, self.lons = readGrid(LATLON_FILE) - self.lats, self.lons, self.grid_spacing_km = readGridMPAS() - self.ibox = (self.m.lonmin-1 <= self.lons ) & (self.lons < self.m.lonmax+1) & (self.m.latmin-1 <= self.lats) & (self.lats < self.m.latmax+1) - self.lats = self.lats[self.ibox] - self.lons = self.lons[self.ibox] - self.x, self.y = self.m(self.lons,self.lats) + if hasattr(self, 'domain'): # once called with empty junk webplot class, just to get lats/lons/x/y/ibox attributes + if overlay: + PYTHON_SCRIPTS_DIR = os.getenv('PYTHON_SCRIPTS_DIR', '/glade/u/home/sobash/RT2015_gpx') + self.fig, self.ax, self.m = pickle.load(open('%s/overlays/rt2015_overlay_%s.pk'%(PYTHON_SCRIPTS_DIR,self.domain), 'r')) + else: + PYTHON_SCRIPTS_DIR = os.getenv('PYTHON_SCRIPTS_DIR', '/glade/work/ahijevyc/share/rt_ensemble/python_scripts') + pklfile = '%s/rt2015_%s.pk'%(PYTHON_SCRIPTS_DIR,self.domain) + print("loading "+pklfile) + self.fig, self.ax, self.m, self.lons,self.lats,self.min_grid_spacing_km,self.delta_deg,self.lon2d,self.lat2d,self.x2d,self.y2d,self.ibox,self.x,self.y,self.vtx,self.wts = pickle.load(open(pklfile, 'rb')) def readEnsemble(self): - self.data, self.missing_members = readEnsemble(self.initdate, timerange=[self.shr,self.ehr], fields=self.opts, debug=self.debug, ENS_SIZE=self.ENS_SIZE) + self.data, self.missing_members = readEnsemble(self.initdate, self.domain, timerange=[self.shr,self.ehr], fields=self.opts, debug=self.debug, ENS_SIZE=self.ENS_SIZE) def plotDepartures(self): from collections import OrderedDict @@ -82,7 +96,10 @@ def plotDepartures(self): hr_et = (self.initdate + timedelta(hours=self.shr) - timedelta(hours=utcoffset-2)).hour # hour in eastern time # get interpolated forecast at climate station locations (in F) - fi = interpolate.RegularGridInterpolator((self.y[:,0], self.x[0,:]), self.data['fill'][0], fill_value=-9999, bounds_error=False, method='linear') + if len(self.x.shape) == 1: + fi = interpolate.RegularGridInterpolator((self.y2d[:,0], self.x2d[0,:]), self.latlonGrid(self.data['fill'][0]), fill_value=-9999, bounds_error=False, method='linear') + if len(self.x.shape) == 2: + fi = interpolate.RegularGridInterpolator((self.y[:,0], self.x[0,:]), self.data['fill'][0], fill_value=-9999, bounds_error=False, method='linear') with open('/glade/u/home/sobash/RT2015_gpx/hly-inventory.txt') as f: for line in f: stn = line.split() @@ -126,7 +143,7 @@ def plotDepartures(self): x, y = self.fig.transFigure.inverted().transform((x0+10,y0+10)) w, h = self.fig.get_size_inches()*self.fig.dpi cax = self.fig.add_axes([x,y,130/float(w),114/float(h)]) - cax.set_axis_bgcolor('#dddddd') + cax.set_facecolor('#dddddd') cax.set_xticks([]) cax.set_yticks([]) for i in list(cax.spines.values()): i.set_linewidth(0.5) @@ -269,14 +286,7 @@ def plotTitleTimes(self): x1, y1 = self.ax.transAxes.transform((1,1)) self.ax.text(x0, y1+10, self.title, fontdict=fontdict, transform=None) - initstr = self.initdate.strftime('Init: %a %Y-%m-%d %H UTC') - if ((self.ehr - self.shr) == 0): - validstr = (self.initdate+timedelta(hours=self.shr)).strftime('Valid: %a %Y-%m-%d %H UTC') - else: - validstr1 = (self.initdate+timedelta(hours=(self.shr-1))).strftime('%a %Y-%m-%d %H UTC') - validstr2 = (self.initdate+timedelta(hours=self.ehr)).strftime('%a %Y-%m-%d %H UTC') - validstr = "Valid: %s - %s"%(validstr1, validstr2) - + initstr, validstr = self.getInitValidStr() self.ax.text(x1, y1+20, initstr, horizontalalignment='right', transform=None) self.ax.text(x1, y1+5, validstr, horizontalalignment='right', transform=None) @@ -301,9 +311,9 @@ def plotFields(self): #self.plotStreamlines() self.plotBarbs() - #if self.opts['fill']['name'] in ['t2depart', 'td2depart']: self.plotDepartures() + if self.opts['fill']['name'] in ['t2depart', 'td2depart']: self.plotDepartures() - def plotFill(self): + def plotFill(self, debug=False): if self.opts['fill']['name'] == 'ptype': self.plotFill_ptype(); return if self.opts['fill']['name'][0:6] == 'ptypes': self.plotFill_ptypes(); return elif self.opts['fill']['name'] == 'crefuh': self.plotReflectivityUH(); return @@ -326,14 +336,29 @@ def plotFill(self): tick_labels = levels norm = colors.BoundaryNorm(levels, cmap.N) - # smooth some of the fill fields - if self.opts['fill']['name'] == 'avo500': self.data['fill'][0] = ndimage.gaussian_filter(self.data['fill'][0], sigma=4) - if self.opts['fill']['name'] == 'pbmin': self.data['fill'][0] = ndimage.gaussian_filter(self.data['fill'][0], sigma=2) - - # Sometimes you get a warning kwarg tri is ignored. - # Tried removing tri=True but got IndexError: too many indices for array - cs1 = self.m.contourf(self.x, self.y, self.data['fill'][0][self.ibox], levels=levels, cmap=cmap, norm=norm, tri=True, extend='max', ax=self.ax) #MPAS - #cs1 = self.m.contourf(self.x, self.y, self.data['fill'][0], levels=levels, cmap=cmap, norm=norm, extend='max', ax=self.ax) + data = self.data['fill'][0] + # regrid 1D mesh that needs to be smoothed + if self.opts['fill']['name'] in ['avo500', 'vort500', 'pbmin']: + print("plotFill: regridding 1D mesh "+self.opts['fill']['name']+" to lat lon") + data = self.latlonGrid(data) + # smooth some of the fill fields + if self.opts['fill']['name'] == 'avo500': data = ndimage.gaussian_filter(data, sigma=4) + if self.opts['fill']['name'] == 'vort500': data = ndimage.gaussian_filter(data, sigma=4) + if self.opts['fill']['name'] == 'pbmin' : data = ndimage.gaussian_filter(data, sigma=2) + cs1 = self.m.contourf(self.x2d, self.y2d, data, levels=levels, cmap=cmap, norm=norm, extend='max', ax=self.ax) + elif self.opts['fill']['ensprod'] in ['neprob', 'neprobgt', 'neproblt']: + # assume the data array has been interpolated to lat/lon + cs1 = self.m.contourf(self.x2d, self.y2d, data, levels=levels, cmap=cmap, norm=norm, extend='max', ax=self.ax) + else: + # use ibox and tri + # Sometimes you get a warning kwarg tri is ignored. + # Tried removing tri=True but got IndexError: too many indices for array + #print("plotFill: starting contourf with ",self.ibox.shape," array "+self.opts['fill']['name']) + #cs1 = self.m.contourf(self.x, self.y, self.data['fill'][0][self.ibox], tri=True, levels=levels, cmap=cmap, norm=norm, extend='max', ax=self.ax) #MPAS + data = self.latlonGrid(self.data['fill'][0]) + if debug: + print("plotFill: starting contourf with 2d array") + cs1 = self.m.contourf(self.x2d, self.y2d, data, levels=levels, cmap=cmap, norm=norm, extend='max', ax=self.ax) self.plotColorbar(cs1, levels, tick_labels, extend, extendfrac) @@ -408,9 +433,16 @@ def plotReflectivityUH(self): norm = colors.BoundaryNorm(levels, cmap.N) tick_labels = levels[:-1] - cs1 = self.m.contourf(self.x, self.y, self.data['fill'][0], levels=levels, cmap=cmap, tri=True, norm=norm, extend='max', ax=self.ax) - self.m.contourf(self.x, self.y, self.data['fill'][1], levels=[75,1000], colors='black', tri=True, ax=self.ax, alpha=0.3) - self.m.contour(self.x, self.y, self.data['fill'][1], levels=[75], colors='k', tri=True, linewidth=0.5, ax=self.ax) + cs1 = self.m.contourf(self.x2d, self.y2d, self.latlonGrid(self.data['fill'][0]), levels=levels, cmap=cmap, norm=norm, extend='max', ax=self.ax) + uh = self.latlonGrid(self.data['fill'][1]) + self.m.contourf(self.x2d, self.y2d, uh, levels=[100,1000], colors='black', ax=self.ax, alpha=0.3) + cs2 = self.m.contour( self.x2d, self.y2d, uh, levels=[100], colors='k', linewidths=0.5, ax=self.ax) + # for some reason the zero contour is plotted if there are no other valid contours + # are there some small negatives due to regridding? No. + if 0.0 in cs2.levels: + print("webplot.plotReflectivityUH has zero contour for some reason. Hide it") + for i in cs2.collections: + i.remove() #maxuh = self.data['fill'][1].max() #self.ax.text(0.03,0.03,'Domain-wide UH max %0.f'%maxuh ,ha="left",va="top",bbox=dict(boxstyle="square",lw=0.5,fc="white"), transform=self.ax.transAxes) @@ -425,41 +457,51 @@ def plotColorbar(self, cs, levels, tick_labels, extend='neither', extendfrac=0.0 cb = plt.colorbar(cs, cax=cax, orientation='horizontal', extend=extend, extendfrac=extendfrac, ticks=tick_labels) cb.outline.set_linewidth(0.5) + def interpolatetri(self, values, vtx, wts): + return np.einsum('nj,nj->n', np.take(values, vtx), wts) def latlonGrid(self, data): - delta_deg = self.grid_spacing_km / 111 - nlon = (self.m.lonmax - self.m.lonmin)/delta_deg - nlat = (self.m.latmax - self.m.latmin)/delta_deg - # TODO: maybe do in map coordinates instead of latlon to avoid the need to specify latlon=True in the contour and barb cases. - x2d, y2d = np.meshgrid(np.linspace(self.m.lonmin, self.m.lonmax, nlon), np.linspace(self.m.latmin,self.m.latmax,nlat)) - z2d = griddata((self.lons, self.lats), data, (x2d, y2d), method='nearest') - return (x2d, y2d, z2d) + # apply ibox to data + data = data[self.ibox] + if hasattr(self, "vtx") and hasattr(self, "wts"): + data_gridded = self.interpolatetri(data, self.vtx, self.wts) + data_gridded = np.reshape(data_gridded, self.lat2d.shape) + else: + print("latlonGrid: interpolating to latlon grid with griddata()") + data_gridded = interpolate.griddata((self.lons, self.lats), data, (self.lon2d, self.lat2d), method='nearest') + return data_gridded def plotContour(self): if self.opts['contour']['name'] in ['sbcinh','mlcinh']: linewidth, alpha = 0.5, 0.75 else: linewidth, alpha = 1.5, 1.0 - x2d, y2d, data = self.latlonGrid(self.data['contour'][0].values[self.ibox]) + data = self.latlonGrid(self.data['contour'][0]) if self.opts['contour']['name'] in ['t2-0c']: data = ndimage.gaussian_filter(data, sigma=2) - else: data = ndimage.gaussian_filter(data, sigma=10) + else: data = ndimage.gaussian_filter(data, sigma=25) - cs2 = self.m.contour(x2d, y2d, data, latlon=True, levels=self.opts['contour']['levels'], colors='k', linewidths=linewidth, ax=self.ax, alpha=alpha) + cs2 = self.m.contour(self.x2d, self.y2d, data, levels=self.opts['contour']['levels'], colors='k', linewidths=linewidth, ax=self.ax, alpha=alpha) plt.clabel(cs2, fontsize='small', fmt='%i') - def plotBarbs(self): + def plotBarbs(self, debug=False): skip = self.opts['barb']['skip'] - if self.domain != 'CONUS': skip = int(skip/2) + if self.domain != 'CONUS': skip = int(skip*0.45) + if self.domain == 'NA': skip = int(skip*2) if self.opts['fill']['name'] == 'crefuh': alpha=0.5 else: alpha=1.0 + + if debug: print("plotBarbs: starting barbs") # skip interval was intended for 2-D fields if len(self.x.shape) == 2: cs2 = self.m.barbs(self.x[::skip,::skip], self.y[::skip,::skip], self.data['barb'][0][::skip,::skip], self.data['barb'][1][::skip,::skip], color='black', alpha=alpha, length=5.5, linewidth=0.25, sizes={'emptybarb':0.05}, ax=self.ax) if len(self.x.shape) == 1: - x2d, y2d, u2d = self.latlonGrid(self.data['barb'][0].values[self.ibox]) - x2d, y2d, v2d = self.latlonGrid(self.data['barb'][1].values[self.ibox]) - cs2 = self.m.barbs(x2d[::skip,::skip], y2d[::skip,::skip], u2d[::skip,::skip], v2d[::skip,::skip], latlon=True, color='black', alpha=alpha, length=5.5, linewidth=0.25, sizes={'emptybarb':0.05}, ax=self.ax) + u2d = self.latlonGrid(self.data['barb'][0]) + v2d = self.latlonGrid(self.data['barb'][1]) + # rotate vectors so they represent the direction properly on the map projection + u10_rot, v10_rot, x, y = self.m.rotate_vector(u2d, v2d, self.lon2d, self.lat2d, returnxy=True) + #cs2 = self.m.barbs(self.x2d[::skip,::skip], self.y2d[::skip,::skip], u2d[::skip,::skip], v2d[::skip,::skip], color='black', alpha=alpha, length=5.5, linewidth=0.25, sizes={'emptybarb':0.05}, ax=self.ax) + cs2 = self.m.barbs(x[::skip,::skip], y[::skip,::skip], u10_rot[::skip,::skip], v10_rot[::skip,::skip], color='black', alpha=alpha, length=5.5, linewidth=0.25, sizes={'emptybarb':0.05}, ax=self.ax) def plotStreamlines(self): speed = np.sqrt(self.data['barb'][0]**2 + self.data['barb'][1]**2) @@ -473,7 +515,7 @@ def plotPaintball(self): colorlist = self.opts['fill']['colors'] levels = self.opts['fill']['levels'] for i in range(self.data['fill'][0].shape[0]): - cs = self.m.contourf(self.x, self.y, self.data['fill'][0][i,:], levels=levels, colors=[colorlist[i%len(colorlist)]], ax=self.ax, alpha=0.5) + cs = self.m.contourf(self.x, self.y, self.data['fill'][0][i,self.ibox], tri=True, levels=levels, colors=[colorlist[i%len(colorlist)]], ax=self.ax, alpha=0.5) rects.append(plt.Rectangle((0,0),1,1,fc=colorlist[i%len(colorlist)])) labels.append("member %d"%(i+1)) @@ -492,7 +534,7 @@ def plotSpaghetti(self): #plt.legend(proxy, ["member %d"%i for i in range(1,11)], ncol=5, loc='right', bbox_to_anchor=(1.0,-0.05), fontsize=11, \ # frameon=False, borderpad=0.25, borderaxespad=0.25, handletextpad=0.2) - def plotStamp(self): + def plotStamp(self, debug=False): fig_width_px, dpi = 1280, 90 fig = plt.figure(dpi=dpi) @@ -520,7 +562,8 @@ def plotStamp(self): w, h = (width_per_panel/float(fig_width))-spacing_w, (height_per_panel/float(fig_height))-spacing_h if member == 9: y = 0 - #print 'member', member, 'creating axes at', x, y + if debug: + print('member', member, 'creating axes at', x, y) thisax = fig.add_axes([x,y,w,h]) thisax.axis('on') @@ -532,7 +575,10 @@ def plotStamp(self): # plot, unless file that has fill field is missing, then skip if member not in self.missing_members[filename] and member < self.ENS_SIZE: - cs1 = self.m.contourf(self.x, self.y, self.data['fill'][0].isel(Time=memberidx), levels=levels, cmap=cmap, norm=norm, extend='max', ax=thisax) + data = self.latlonGrid(self.data['fill'][0][memberidx,:]) + if debug: + print("plotStamp: starting contourf with regridded array", memberidx) + cs1 = self.m.contourf(self.x2d, self.y2d, data, levels=levels, cmap=cmap, norm=norm, extend='max', ax=thisax) memberidx += 1 # use every other tick for large colortables, remove last tick label for both @@ -547,13 +593,7 @@ def plotStamp(self): # add init/valid text fontdict = {'family':'monospace', 'size':13, 'weight':'bold'} - initstr = self.initdate.strftime(' Init: %a %Y-%m-%d %H UTC') - if ((self.ehr - self.shr) == 0): - validstr = (self.initdate+timedelta(hours=self.shr)).strftime('Valid: %a %Y-%m-%d %H UTC') - else: - validstr1 = (self.initdate+timedelta(hours=(self.shr-1))).strftime('%a %Y-%m-%d %H UTC') - validstr2 = (self.initdate+timedelta(hours=self.ehr)).strftime('%a %Y-%m-%d %H UTC') - validstr = "Valid: %s - %s"%(validstr1, validstr2) + initstr, validstr = self.getInitValidStr() fig.text(0.51, 0.22, self.title, fontdict=fontdict, transform=fig.transFigure) fig.text(0.51, 0.22 - 25/float(fig_height_px), initstr, transform=fig.transFigure) @@ -564,6 +604,23 @@ def plotStamp(self): fig.figimage(plt.imread('ncar.png'), xo=x, yo=y+15, zorder=1000) plt.text(x+10, y+5, 'ensemble.ucar.edu', fontdict={'size':9, 'color':'#505050'}, transform=None) + + def getInitValidStr(self): + initstr = self.initdate.strftime(' Init: %a %Y-%m-%d %H UTC') + if ((self.ehr - self.shr) == 0): + validstr = (self.initdate+timedelta(hours=self.shr)).strftime('Valid: %a %Y-%m-%d %H UTC') + else: + # match precip or precip-24hr, but not precipacc + # accept precip-24hr, precip-48hr, precip-120hr, etc. + if self.opts['fill']['name'] == 'precip' or is_precip_diff(self.opts['fill']['name']): + # do not subtract 1 from start hour if array is difference of accumulated precipitation + validstr1 = (self.initdate+timedelta(hours=(self.shr))).strftime('%a %Y-%m-%d %H UTC') + else: + validstr1 = (self.initdate+timedelta(hours=(self.shr-1))).strftime('%a %Y-%m-%d %H UTC') + validstr2 = (self.initdate+timedelta(hours=self.ehr)).strftime('%a %Y-%m-%d %H UTC') + validstr = "Valid: %s - %s"%(validstr1, validstr2) + return initstr, validstr + def saveFigure(self, trans=False): # place NCAR logo 57 pixels below bottom of map, then save image if 'ensprod' in self.opts['fill']: # CSS needed incase not a fill plot @@ -580,7 +637,12 @@ def saveFigure(self, trans=False): elif self.opts['fill']['ensprod'] in ['prob', 'neprob', 'probgt', 'problt', 'neprobgt', 'neproblt']: ncolors = 48 elif self.opts['fill']['name'] in ['crefuh']: ncolors = 48 else: ncolors = 255 - command = '/glade/u/home/sobash/pngquant/pngquant %d %s --ext=.png --force'%(ncolors,self.outfile) + #command = '/glade/u/home/sobash/pngquant/pngquant %d %s --ext=.png --force'%(ncolors,self.outfile) + if os.environ['NCAR_HOST'] == "cheyenne": + bindir= '/glade/u/home/ahijevyc/bin_cheyenne/' + else: + bindir= '/glade/u/home/ahijevyc/bin/' + command = bindir + 'pngquant %d %s --ext=.png --force'%(ncolors,self.outfile) ret = subprocess.check_call(command.split()) plt.clf() @@ -620,12 +682,15 @@ def parseargs(): # assign contour levels and colors if (input[1] in ['prob', 'neprob', 'probgt', 'problt', 'neprobgt', 'neproblt', 'prob3d']): + if len(input)<3: + print("your -f option has less than 3 components. It needs name, ensprod, and thresh.") + sys.exit(1) thisdict['thresh'] = float(input[2]) if int(opts['sigma']) != 40: thisdict['levels'] = np.arange(0.1,1.1,0.1) else: thisdict['levels'] = [0.02,0.05,0.1,0.15,0.2,0.25,0.35,0.45,0.6] - thisdict['levels'] = np.arange(0.1,1.1,0.2) + #thisdict['levels'] = np.arange(0.1,1.1,0.2) thisdict['colors'] = readNCLcm('perc2_9lev') - thisdict['colors'] = ['#d9d9d9', '#bdbdbd', '#969696', '#636363', '#252525'] + #thisdict['colors'] = ['#d9d9d9', '#bdbdbd', '#969696', '#636363', '#252525'] # greyscale elif (input[1] in ['paintball', 'spaghetti']): thisdict['thresh'] = float(input[2]) thisdict['levels'] = [float(input[2]), 1000] @@ -675,6 +740,7 @@ def makeEnsembleList(wrfinit, timerange, ENS_SIZE): wrfout = '%s/%s/wrf_rundir/ens_%d/wrfout_d02_%s'%(EXP_DIR,yyyymmddhh,mem,wrfvalidstr) diag = '%s/%s/wrf_rundir/ens_%d/diags_d02.%s.nc'%(EXP_DIR,yyyymmddhh,mem,wrfvalidstr) #diag = '/glade/scratch/sobash/FOR_MORRIS/%s/mem%d/diags_d02_f%03d.nc'%(yyyymmddhh,mem,hr) + diag = '/glade/scratch/sobash/FOR_MORRIS/%s/mem%d/diags_d02_f%03d.nc'%(yyyymmddhh,mem,hr) upp = '%s/%s/post_rundir/mem_%d/fhr_%d/WRFTWO%02d.nc'%(EXP_DIR,yyyymmddhh,mem,hr,hr) #ens1 = '/glade/p/nmm0001/romine/rt2015/ens_1km/%s/mem%02d_%s.nc'%(yyyymmddhh,mem,yyyymmddhh) if os.path.exists(wrfout): file_list['wrfout'].append(wrfout) @@ -746,7 +812,7 @@ def makeEnsembleListHREF(wrfinit, timerange, ENS_SIZE): return (file_list, missing_list) -def makeEnsembleListMPAS(wrfinit, timerange, ENS_SIZE, debug=False): +def makeEnsembleListMPAS(wrfinit, timerange, ENS_SIZE, g193=False, debug=False): # create lists of files (and missing file indices) for various file types shr, ehr = timerange file_list = { 'wrfout':[], 'diag':[] } @@ -757,7 +823,9 @@ def makeEnsembleListMPAS(wrfinit, timerange, ENS_SIZE, debug=False): yyyymmddhh = wrfinit.strftime('%Y%m%d%H') for mem in range(1,ENS_SIZE+1): diag = '/glade/p/nsc/nmmm0046/schwartz/MPAS_ens_15-3km_mesh/POST/%s/ens_%d/diag.%s.nc'%(yyyymmddhh,mem,wrfvalidstr) - print(diag) + if g193: + diag = '/glade/p/nsc/nmmm0046/schwartz/MPAS_ens_15-3km_mesh/POST/%s/ens_%d/diag_latlon_g193.%s.nc'%(yyyymmddhh,mem,wrfvalidstr) + if debug: print(diag) if os.path.exists(diag): file_list['diag'].append(diag) else: missing_list['diag'].append(missing_index) missing_index += 1 @@ -831,15 +899,16 @@ def makeEnsembleListDA(wrfinit, timerange): missing_index += 1 return (file_list, missing_list) -def readEnsemble(wrfinit, timerange=None, fields=None, debug=False, ENS_SIZE=10): +def readEnsemble(wrfinit, domain, timerange=None, fields=None, debug=False, ENS_SIZE=10): ''' Reads in desired fields and returns 2-D arrays of data for each field (barb/contour/field) ''' - if debug: print(fields) + if debug: + print(fields) datadict = {} #file_list, missing_list = makeEnsembleList(wrfinit, timerange, ENS_SIZE) #construct list of files #file_list, missing_list = makeEnsembleListNSC(wrfinit, timerange) #construct list of files #file_list, missing_list = makeEnsembleListStan(wrfinit, timerange, ENS_SIZE) #construct list of files - file_list, missing_list = makeEnsembleListMPAS(wrfinit, timerange, ENS_SIZE, debug=debug) #construct list of files + file_list, missing_list = makeEnsembleListMPAS(wrfinit, timerange, ENS_SIZE, g193=False, debug=debug) #construct list of files #file_list, missing_list = makeEnsembleListArchive(wrfinit, timerange) #construct list of files #file_list, missing_list = makeEnsembleListHybrid(wrfinit, timerange) #construct list of files #file_list, missing_list = makeEnsembleListHREF(wrfinit, timerange, ENS_SIZE) #construct list of files @@ -859,10 +928,10 @@ def readEnsemble(wrfinit, timerange=None, fields=None, debug=False, ENS_SIZE=10) # open Multi-file netcdf dataset if debug: - pdb.set_trace() + print("opening xarray mfdataset ", file_list[filename]) fh = xarray.open_mfdataset(file_list[filename],concat_dim='Time') - # Dimension has different times and members. + # This concatenation dimension includes different times AND members. fh = fh.rename({'Time':'TimeMember'}) # loop through each field, wind fields will have two fields that need to be read @@ -881,6 +950,17 @@ def readEnsemble(wrfinit, timerange=None, fields=None, debug=False, ENS_SIZE=10) #else: data = fh.variables[array][:,level,:,:] data = fh.variables[array][:,:] + data = data.values # use the numpy array, not the full xarray object. + # Many things that come afterward assume a numpy array, like flatten method. + if fh.variables[array].dims[1] == 'nVertices': + if debug: + print("field on vertices, like vorticity_500hPa, put on cells") + fieldv = data + nEdgesOnCell, verticesOnCell = readMPASVertices() + # verticesOnCell is the transpose of what mpas_vort_cell1 expects + fieldc = mpas_vort_cell.mpas_vort_cell1(nEdgesOnCell, verticesOnCell.T, fieldv) + data = fieldc + #data = data.reshape((10,data.shape[1])) #data = np.swapaxes(data,0,1) # flip first two axes so time is first @@ -891,21 +971,26 @@ def readEnsemble(wrfinit, timerange=None, fields=None, debug=False, ENS_SIZE=10) #data = np.swapaxes(data,0,1) # flip first two axes so time is first #data = data.reshape((10*((timerange[1]+1)-timerange[0])),data.shape[2],data.shape[3]) #reshape again + + # change units for certain fields - if array in ['U_PL', 'V_PL', 'UBSHR6','VBSHR6','UBSHR1', 'VBSHR1', 'U10','u10','v10','V10', 'U_COMP_STM', 'V_COMP_STM','S_PL','U_COMP_STM_6KM','V_COMP_STM_6KM']: data = data*1.93 # m/s > kt - elif array in ['DEWPOINT_2M', 'T2', 't2m', 'AFWA_WCHILL', 'AFWA_HEATIDX']: data = (data - 273.15)*1.8 + 32.0 # K > F - elif array in ['PREC_ACC_NC', 'PREC_ACC_C', 'AFWA_PWAT', 'PWAT', 'precipw', 'AFWA_RAIN', 'AFWA_SNOWFALL', 'AFWA_SNOW', 'AFWA_ICE', 'AFWA_FZRA','AFWA_RAIN_HRLY','AFWA_ICE_HRLY','AFWA_SNOWFALL_HRLY', 'AFWA_FZRA_HRLY']: data = data*0.0393701 # mm > in - elif array in ['RAINNC', 'GRPL_MAX', 'SNOW_ACC_NC', 'AFWA_HAIL', 'HAILCAST_DIAM_MAX']: data = data*0.0393701 # mm > in + if array in ['u10','v10']: data = data*1.93 # m/s > kt + elif re.compile("^uzonal_\d\d+[A-Za-z]+").match(array): data = data*1.93 # m/s > kt + elif re.compile("^umeridional_\d\d+[A-Za-z]+").match(array): data = data*1.93 # m/s > kt + elif re.compile("^[uv]_pv$").match(array): data = data*1.93 # m/s > kt + elif array in ['dewpoint_surface', 't2m']: data = (data - 273.15)*1.8 + 32.0 # K > F + elif array in ['precipw']: data = data*0.0393701 # mm > in + elif array in ['rainnc', 'grpl_max', 'SNOW_ACC_NC']: data = data*0.0393701 # mm > in elif array in ['T_PL', 'TD_PL', 'SFC_LI']: data = data - 273.15 # K > C - elif array in ['AFWA_MSLP', 'MSLP', 'mslp']: data = data*0.01 # Pa > hPa - elif array in ['ECHOTOP']: data = data*3.28084# m > ft - elif array in ['UP_HELI_MIN']: data = np.abs(data) - elif array in ['AFWA_VIS', 'VISIBILITY']: data = (data*0.001)/1.61 # m > mi - elif array in ['SBCINH', 'MLCINH', 'W_DN_MAX', 'sbcin', 'mlcin']: data = data*-1.0 # make cin positive + elif re.compile("^temperature_\d\d+hPa$").match(array):data = data - 273.15 # K > C + elif re.compile("^dewpoint_\d\d+hPa$").match(array): data = data - 273.15 # K > C + elif array in ['mslp']: data = data*0.01 # Pa > hPa + elif array in ['UP_HELI_MIN']: data = np.abs(data) + elif array in ['w_velocity_min']: data = data*-1.0 elif array in ['PVORT_320K']: data = data*1000000 # multiply by 1e6 - elif array in ['SBT123_GDS3_NTAT','SBT124_GDS3_NTAT','GOESE_WV','GOESE_IR']: data = data -273.15 # K -> C - elif array in ['HAIL_MAXK1', 'HAIL_MAX2D']: data = data*39.3701 # m -> inches - elif array in ['PBMIN', 'PBMIN_SFC', 'BESTPBMIN', 'MLPBMIN', 'MUPBMIN']: data = data*0.01 # Pa -> hPa + elif re.compile("^vorticity_\d\d+hPa").match(array): data = data * 1e5 + elif array == "vort_pv": data = data * 1e5 + elif array in ['PBMIN', 'PBMIN_SFC', 'BESTPBMIN', 'MLPBMIN', 'MUPBMIN']: data = data*0.01 # Pa -> hPa # elif array in ['LTG1_MAX1', 'LTG2_MAX', 'LTG3_MAX']: data = data*0.20 # scale down excess values datalist.append(data) @@ -913,7 +998,14 @@ def readEnsemble(wrfinit, timerange=None, fields=None, debug=False, ENS_SIZE=10) # these are derived fields, we don't have in any of the input files but we can compute print(datalist[0].shape) if 'name' in fields[f]: - if fieldname in ['shr06mag', 'shr01mag', 'bunkmag','speed10m']: datalist = [np.sqrt(datalist[0]**2 + datalist[1]**2)] + if fieldname in ['shr06mag', 'shr01mag']: + # derive wind shear from top and bottom level + # Assume datalist is 4-element list: [bottom_u, bottom_v, top_u, top_v] + datalist = [np.sqrt((datalist[0]-datalist[2])**2 + (datalist[1]-datalist[3])**2)] + elif fieldname[0:5] == 'speed' or fieldname in ['bunkmag']: + # derive speed from u and v components. different than wrf. It already has speed in S_PL array. + datalist = [np.sqrt(datalist[0]**2 + datalist[1]**2)] + elif fieldname in ['shr06','shr01']: datalist = [datalist[0]-datalist[2], datalist[1]-datalist[3]] elif fieldname == 'uhratio': datalist = [compute_uhratio(datalist)] elif fieldname == 'stp': datalist = [computestp(datalist)] # GSR in fields are T(K), mixing ratio (kg/kg), and surface pressure (Pa) @@ -923,13 +1015,23 @@ def readEnsemble(wrfinit, timerange=None, fields=None, debug=False, ENS_SIZE=10) #elif fieldname == 'pbmin': datalist = [ datalist[1] - datalist[0][:,0,:] ] elif fieldname == 'pbmin': datalist = [ datalist[1] - datalist[0] ] # CSS changed above line for GRIB2 elif fieldname in ['thck1000-500', 'thck1000-850'] : datalist = [ datalist[1]*0.1 - datalist[0]*0.1 ] # CSS added for thicknesses - elif fieldname == 'winter': datalist = [datalist[1] + datalist[2] + datalist[3]] - elif fieldname == 'frzdepth': datalist = [computefrzdepth(datalist)] datadict[f] = [] for data in datalist: - # perform mean/max/variance/etc to reduce 3D array to 2D + + + if is_precip_diff(fieldname): + if debug: + print("Deriving accumulated precipitation. Subract ensemble at first time from ensemble at last time") + for i in missing_list[filename]: data = np.insert(data, i, np.nan, axis=0) #insert nan for missing files + # last and first time in the requested time range + ensemble_at_last_time = data[-ENS_SIZE:] + ensemble_at_first_time = data[:ENS_SIZE] + data = ensemble_at_last_time - ensemble_at_first_time + + # perform mean/max/variance/etc to reduce 3D array to 2D spatial_dimensions = data.shape[1:] # works for 1D meshes like MPAS and 2D grids like WRF + ntimes = int(data.shape[0]/ENS_SIZE) if (fieldtype == 'mean'): data = np.mean(data, axis=0) elif (fieldtype == 'pmm'): data = compute_pmm(data) elif (fieldtype == 'max'): data = np.amax(data, axis=0) @@ -937,46 +1039,53 @@ def readEnsemble(wrfinit, timerange=None, fields=None, debug=False, ENS_SIZE=10) elif (fieldtype == 'var'): data = np.std(data, axis=0) elif (fieldtype == 'maxstamp'): for i in missing_list[filename]: data = np.insert(data, i, np.nan, axis=0) #insert nan for missing files - data = np.reshape(data, (data.shape[0]/ENS_SIZE,ENS_SIZE,*spatial_dimensions)) + data = np.reshape(data, (ntimes,ENS_SIZE,*spatial_dimensions)) data = np.nanmax(data, axis=0) elif (fieldtype == 'summean'): for i in missing_list[filename]: data = np.insert(data, i, np.nan, axis=0) #insert nan for missing files - data = np.reshape(data, (data.shape[0]/ENS_SIZE,ENS_SIZE,*spatial_dimensions)) + data = np.reshape(data, (ntimes,ENS_SIZE,*spatial_dimensions)) data = np.nansum(data, axis=0) data = np.nanmean(data, axis=0) elif (fieldtype == 'maxmean'): for i in missing_list[filename]: data = np.insert(data, i, np.nan, axis=0) #insert nan for missing files - data = np.reshape(data, (data.shape[0]/ENS_SIZE,ENS_SIZE,*spatial_dimensions)) + data = np.reshape(data, (ntimes,ENS_SIZE,*spatial_dimensions)) data = np.nanmax(data, axis=0) data = np.nanmean(data, axis=0) elif (fieldtype == 'summax'): for i in missing_list[filename]: data = np.insert(data, i, np.nan, axis=0) #insert nan for missing files - data = np.reshape(data, (data.shape[0]/ENS_SIZE,ENS_SIZE,*spatial_dimensions)) + data = np.reshape(data, (ntimes,ENS_SIZE,*spatial_dimensions)) data = np.nansum(data, axis=0) data = np.nanmax(data, axis=0) elif (fieldtype[0:3] == 'mem'): for i in missing_list[filename]: data = np.insert(data, i, np.nan, axis=0) #insert nan for missing files - data = np.reshape(data, (data.shape[0]/ENS_SIZE,ENS_SIZE,*spatial_dimensions)) - print(fieldname) - if fieldname in ['precip', 'precipacc']: - print('where we should be') - data = np.nanmax(data, axis=0) - else: data = np.nanmax(data, axis=0) + data = np.reshape(data, (ntimes,ENS_SIZE,*spatial_dimensions)) + data = np.nanmax(data, axis=0) data = data[member-1,:] elif (fieldtype in ['prob', 'neprob', 'problt', 'probgt', 'neprobgt', 'neproblt']): if fieldtype in ['prob', 'neprob', 'probgt', 'neprobgt']: data = (data>=thresh).astype('float') elif fieldtype in ['problt', 'neproblt']: data = (data=thresh).astype('float') for i in missing_list[filename]: data = np.insert(data, i, np.nan, axis=0) - data = np.reshape(data, (data.shape[0]/ENS_SIZE,ENS_SIZE,*spatial_dimensions)) + data = np.reshape(data, (ntimes,ENS_SIZE,*spatial_dimensions)) data = compute_prob3d(data, roi=14, sigma=float(fields['sigma']), type='gaussian') if debug: print('field '+ fieldname+ ' has shape', data.shape, 'max', data.max(), 'min', data.min()) @@ -990,6 +1099,15 @@ def readEnsemble(wrfinit, timerange=None, fields=None, debug=False, ENS_SIZE=10) return (datadict, missing_list) + +def is_precip_diff(s): + # Is this string an accumulated precipitation difference field? + # Assume it is if it starts with "precip-", ends with "hr" + if s[0:7] == "precip-" and s[-2:] == "hr": + return True + return False + + def readGrid(file_dir): f = Dataset(file_dir, 'r') lats = f.variables['XLAT'][0,:] @@ -997,16 +1115,25 @@ def readGrid(file_dir): f.close() return (lats,lons) +def readMPASVertices(ifile="/glade/p/mmm/parc/schwartz/MPAS/15-3km_mesh/init.nc"): + # Used for regridding field from vertex to cell. latVertex and lonVertex not needed + fh = Dataset(ifile, "r") + nEdgesOnCell = fh.variables['nEdgesOnCell'][:] + verticesOnCell = fh.variables['verticesOnCell'][:] + fh.close() + return (nEdgesOnCell, verticesOnCell) + def readGridMPAS(): fh = Dataset("/glade/p/mmm/parc/schwartz/MPAS/15-3km_mesh/init.nc", "r") - lats = fh.variables['latCell'][:] - lons = fh.variables['lonCell'][:] + latCell = fh.variables['latCell'][:] + lonCell = fh.variables['lonCell'][:] areaCell = fh.variables['areaCell'][:] # units m^2 - max_resolution_km = 2. * np.sqrt(areaCell.min()/np.pi/1000/1000) + min_grid_spacing_km = 2. * np.sqrt(areaCell.min()/np.pi/1000/1000) + # min_grid_spacing_km used for grid spacing of interpolated lat-lon grid. fh.close() - lats, lons = np.degrees(lats), np.degrees(lons) #convert radians to degrees - lons[lons >= 180] = lons[lons >= 180] - 360 - return (lats, lons, max_resolution_km) + latCell, lonCell = np.degrees(latCell), np.degrees(lonCell) #convert radians to degrees + lonCell[lonCell >= 180] = lonCell[lonCell >= 180] - 360 + return (latCell, lonCell, min_grid_spacing_km) def saveNewMap(domstr='CONUS', wrfout=None): # if domstr is not in the dictionary, then use provided wrfout to create new domain @@ -1032,6 +1159,8 @@ def saveNewMap(domstr='CONUS', wrfout=None): ll_lat, ll_lon, ur_lat, ur_lon = domains[domstr]['corners'] fig_width = domains[domstr]['fig_width'] lat_1, lat_2, lon_0 = 32.0, 46.0, -101.0 + if domstr=='NA': + lon_0 = -115.0 dpi = 90 fig = plt.figure(dpi=dpi) @@ -1054,14 +1183,43 @@ def saveNewMap(domstr='CONUS', wrfout=None): m.drawcoastlines(linewidth=0.5, ax=ax) m.drawstates(linewidth=0.25, ax=ax) m.drawcountries(ax=ax) - m.drawcounties(linewidth=0.1, color='gray', ax=ax) - - pickle.dump((fig,ax,m), open('rt2015_%s.pk'%domstr, 'wb')) + # avoid this error + # UnicodeDecodeError: 'utf-8' codec can't decode byte 0xf1 in position 2: invalid continuation byte + #m.drawcounties(linewidth=0.1, color='gray', ax=ax) + + + # load lat/lons + lats, lons, min_grid_spacing_km = readGridMPAS() + delta_deg = min_grid_spacing_km / 111 + if m.lonmin > 180 or m.lonmax > 180: + lons[lons<0] = lons[lons<0] + 360. # change -180-0 to 180-360 to match m.lonmin and m.lonmax + # Replace m.lonmax with ur_lon? Otherwise, risk getting +179.999 when NW corner crosses the datetime + nlon = int((m.lonmax - m.lonmin)/delta_deg) + nlat = int((m.latmax - m.latmin)/delta_deg) + if nlon > 1500: + nlon = 1500 + if nlat > 1500: + nlat = 1500 + lon2d, lat2d = np.meshgrid(np.linspace(m.lonmin, m.lonmax, nlon), np.linspace(m.latmin,m.latmax,nlat)) + # Convert to map coordinates instead of latlon to avoid the need to specify latlon=True in contour and barb methods. + x2d, y2d = m(lon2d,lat2d) + # ibox: subscripts within lat/lon box # only used to speed up 1-D array triangulation and plotting + ibox = (m.lonmin-1 <= lons ) & (lons < m.lonmax+1) & (m.latmin-1 <= lats) & (lats < m.latmax+1) + lons = lons[ibox] + lats = lats[ibox] + x, y = m(lons,lats) + + # use .filled() to avoid error about masked arrays + vtx, wts = interp_weights(np.vstack((lons.filled(),lats.filled())).T,np.vstack((lon2d.flatten(), lat2d.flatten())).T) + + pickle.dump((fig,ax,m,lons,lats,min_grid_spacing_km,delta_deg,lon2d,lat2d,x2d,y2d,ibox,x,y,vtx,wts), open('/glade/work/ahijevyc/share/rt_ensemble/python_scripts/rt2015_%s.pk'%domstr, 'wb')) def drawOverlay(domstr='CONUS'): ll_lat, ll_lon, ur_lat, ur_lon = domains[domstr]['corners'] fig_width = domains[domstr]['fig_width'] lat_1, lat_2, lon_0 = 32.0, 46.0, -101.0 + if domstr=='NA': + lon_0 = -115.0 dpi = 90 fig = plt.figure(dpi=dpi) @@ -1087,20 +1245,23 @@ def drawOverlay(domstr='CONUS'): plt.savefig('overlay_counties_%s.png'%domstr, dpi=90, transparent=True) def compute_pmm(ensemble): - mem = ensemble.shape[0] + members = ensemble.shape[0] spatial_dimensions = ensemble.shape[1:] ens_mean = np.mean(ensemble, axis=0) - ens_dist = np.sort(ensemble.values.flatten())[::-1] - pmm = ens_dist[::mem] + ens_dist = np.sort(ensemble.flatten())[::-1] + pmm = ens_dist[::members] - ens_mean_index = np.argsort(ens_mean.values.flatten())[::-1] + ens_mean_index = np.argsort(ens_mean.flatten())[::-1] temp = np.empty_like(pmm) temp[ens_mean_index] = pmm - temp = np.where(ens_mean.values.flatten() > 0, temp, 0.0) + temp = np.where(ens_mean.flatten() > 0, temp, 0.0) return temp.reshape(spatial_dimensions) def compute_neprob(ensemble, roi=0, sigma=0.0, type='gaussian'): + if len(ensemble.shape) < 3: + print('compute_neprob: needs ensemble of 2D arrays, not 1D arrays') + sys.exit(1) y,x = np.ogrid[-roi:roi+1, -roi:roi+1] kernel = x**2 + y**2 <= roi**2 ens_roi = ndimage.filters.maximum_filter(ensemble, footprint=kernel[np.newaxis,:]) @@ -1138,7 +1299,9 @@ def computestp(data): srh_term = (data[2]/150.0) - shear06 = np.sqrt(data[3]**2 + data[4]**2) #this will be in knots (converted prior to fn) + ushear06 = data[3]-data[5] + vshear06 = data[4]-data[6] + shear06 = np.sqrt(ushear06**2 + vshear06**2) #this will be in knots (converted prior to fn) shear_term = (shear06/38.87) shear_term = np.where(shear06 > 58.32, 1.5, shear_term) shear_term = np.where(shear06 < 24.3, 0.0, shear_term) @@ -1196,6 +1359,18 @@ def compute_rh(data): rh = q2 / qsat return 100*rh +# from https://stackoverflow.com/questions/20915502/speedup-scipy-griddata-for-multiple-interpolations-between-two-irregular-grids +def interp_weights(xyz, uvw): + tri = qhull.Delaunay(xyz) + simplex = tri.find_simplex(uvw) + vertices = np.take(tri.simplices, simplex, axis=0) + temp = np.take(tri.transform, simplex, axis=0) + d = xyz.shape[1] + delta = uvw - temp[:, d] + bary = np.einsum('njk,nk->nj', temp[:, :d, :], delta) + return vertices, np.hstack((bary, 1 - bary.sum(axis=1, keepdims=True))) + + def showKeys(): print(list(fieldinfo.keys())) sys.exit() From 354fed81913c2b9cfb785b27bb43cfd4239e1292 Mon Sep 17 00:00:00 2001 From: ahijevyc Date: Tue, 23 Jul 2019 09:58:08 -0600 Subject: [PATCH 29/68] deleted t.py --- t.py | 237 ----------------------------------------------------------- 1 file changed, 237 deletions(-) delete mode 100644 t.py diff --git a/t.py b/t.py deleted file mode 100644 index e1a20c2..0000000 --- a/t.py +++ /dev/null @@ -1,237 +0,0 @@ -# Copyright (c) 2014,2015,2016,2017 MetPy Developers. -# Distributed under the terms of the BSD 3-Clause License. -# SPDX-License-Identifier: BSD-3-Clause -"""Make Skew-T Log-P based plots. - -Contain tools for making Skew-T Log-P plots, including the base plotting class, -`SkewT`, as well as a class for making a `Hodograph`. -""" - -import matplotlib -from matplotlib.axes import Axes -import matplotlib.axis as maxis -from matplotlib.collections import LineCollection -import matplotlib.colors as mcolors -from matplotlib.patches import Circle -from matplotlib.projections import register_projection -import matplotlib.spines as mspines -from matplotlib.ticker import MultipleLocator, NullFormatter, ScalarFormatter -import matplotlib.transforms as transforms -import numpy as np - -from ._util import colored_line -from ..calc import dewpoint, dry_lapse, moist_lapse, vapor_pressure -from ..calc.tools import _delete_masked_points -from ..interpolate import interpolate_1d -from ..package_tools import Exporter -from ..units import concatenate, units - -exporter = Exporter(globals()) - - -class SkewXTick(maxis.XTick): - r"""Make x-axis ticks for Skew-T plots. - - This class adds to the standard :class:`matplotlib.axis.XTick` dynamic checking - for whether a top or bottom tick is actually within the data limits at that part - and draw as appropriate. It also performs similar checking for gridlines. - """ - - def update_position(self, loc): - """Set the location of tick in data coords with scalar *loc*.""" - # This ensures that the new value of the location is set before - # any other updates take place. - self._loc = loc - super(SkewXTick, self).update_position(loc) - - def _has_default_loc(self): - return self.get_loc() is None - - def _need_lower(self): - return (self._has_default_loc() or - transforms.interval_contains(self.axes.lower_xlim, - self.get_loc())) - - def _need_upper(self): - return (self._has_default_loc() or - transforms.interval_contains(self.axes.upper_xlim, - self.get_loc())) - - @property - def gridOn(self): # noqa: N802 - """Control whether the gridline is drawn for this tick.""" - return (self._gridOn and (self._has_default_loc() or - transforms.interval_contains(self.get_view_interval(), - self.get_loc()))) - - @gridOn.setter - def gridOn(self, value): # noqa: N802 - self._gridOn = value - - @property - def tick1On(self): # noqa: N802 - """Control whether the lower tick mark is drawn for this tick.""" - return self._tick1On and self._need_lower() - - @tick1On.setter - def tick1On(self, value): # noqa: N802 - self._tick1On = value - - @property - def label1On(self): # noqa: N802 - """Control whether the lower tick label is drawn for this tick.""" - return self._label1On and self._need_lower() - - @label1On.setter - def label1On(self, value): # noqa: N802 - self._label1On = value - - @property - def tick2On(self): # noqa: N802 - """Control whether the upper tick mark is drawn for this tick.""" - return self._tick2On and self._need_upper() - - @tick2On.setter - def tick2On(self, value): # noqa: N802 - self._tick2On = value - - @property - def label2On(self): # noqa: N802 - """Control whether the upper tick label is drawn for this tick.""" - return self._label2On and self._need_upper() - - @label2On.setter - def label2On(self, value): # noqa: N802 - self._label2On = value - - def get_view_interval(self): - """Get the view interval.""" - return self.axes.xaxis.get_view_interval() - - -class SkewXAxis(maxis.XAxis): - r"""Make an x-axis that works properly for Skew-T plots. - - This class exists to force the use of our custom :class:`SkewXTick` as well - as provide a custom value for interview that combines the extents of the - upper and lower x-limits from the axes. - """ - - def _get_tick(self, major): - return SkewXTick(self.axes, None, '', major=major) - - def get_view_interval(self): - """Get the view interval.""" - return self.axes.upper_xlim[0], self.axes.lower_xlim[1] - - -class SkewSpine(mspines.Spine): - r"""Make an x-axis spine that works properly for Skew-T plots. - - This class exists to use the separate x-limits from the axes to properly - locate the spine. - """ - - def _adjust_location(self): - pts = self._path.vertices - if self.spine_type == 'top': - pts[:, 0] = self.axes.upper_xlim - else: - pts[:, 0] = self.axes.lower_xlim - - -class SkewXAxes(Axes): - r"""Make a set of axes for Skew-T plots. - - This class handles registration of the skew-xaxes as a projection as well as setting up - the appropriate transformations. It also makes sure we use our instances for spines - and x-axis: :class:`SkewSpine` and :class:`SkewXAxis`. It provides properties to - facilitate finding the x-limits for the bottom and top of the plot as well. - """ - - # The projection must specify a name. This will be used be the - # user to select the projection, i.e. ``subplot(111, - # projection='skewx')``. - name = 'skewx' - - def __init__(self, *args, **kwargs): - r"""Initialize `SkewXAxes`. - - Parameters - ---------- - args : Arbitrary positional arguments - Passed to :class:`matplotlib.axes.Axes` - - position: int, optional - The rotation of the x-axis against the y-axis, in degrees. - - kwargs : Arbitrary keyword arguments - Passed to :class:`matplotlib.axes.Axes` - - """ - # This needs to be popped and set before moving on - self.rot = kwargs.pop('rotation', 30) - Axes.__init__(self, *args, **kwargs) - - def _init_axis(self): - # Taken from Axes and modified to use our modified X-axis - self.xaxis = SkewXAxis(self) - self.spines['top'].register_axis(self.xaxis) - self.spines['bottom'].register_axis(self.xaxis) - self.yaxis = maxis.YAxis(self) - self.spines['left'].register_axis(self.yaxis) - self.spines['right'].register_axis(self.yaxis) - - def _gen_axes_spines(self, locations=None, offset=0.0, units='inches'): - # pylint: disable=unused-argument - spines = {'top': SkewSpine.linear_spine(self, 'top'), - 'bottom': mspines.Spine.linear_spine(self, 'bottom'), - 'left': mspines.Spine.linear_spine(self, 'left'), - 'right': mspines.Spine.linear_spine(self, 'right')} - return spines - - def _set_lim_and_transforms(self): - """Set limits and transforms. - - This is called once when the plot is created to set up all the - transforms for the data, text and grids. - - """ - # Get the standard transform setup from the Axes base class - Axes._set_lim_and_transforms(self) - - # Need to put the skew in the middle, after the scale and limits, - # but before the transAxes. This way, the skew is done in Axes - # coordinates thus performing the transform around the proper origin - # We keep the pre-transAxes transform around for other users, like the - # spines for finding bounds - self.transDataToAxes = (self.transScale + - (self.transLimits + - transforms.Affine2D().skew_deg(self.rot, 0))) - - # Create the full transform from Data to Pixels - self.transData = self.transDataToAxes + self.transAxes - - # Blended transforms like this need to have the skewing applied using - # both axes, in axes coords like before. - self._xaxis_transform = (transforms.blended_transform_factory( - self.transScale + self.transLimits, - transforms.IdentityTransform()) + - transforms.Affine2D().skew_deg(self.rot, 0)) + self.transAxes - - @property - def lower_xlim(self): - """Get the data limits for the x-axis along the bottom of the axes.""" - return self.axes.viewLim.intervalx - - @property - def upper_xlim(self): - """Get the data limits for the x-axis along the top of the axes.""" - return self.transDataToAxes.inverted().transform([[0., 1.], [1., 1.]])[:, 0] - - -# Now register the projection with matplotlib so the user can select -# it. -register_projection(SkewXAxes) - - From 2c3409ad7113ca406ad81d80ccbd0e4dd72062e2 Mon Sep 17 00:00:00 2001 From: ahijevyc Date: Tue, 23 Jul 2019 10:04:37 -0600 Subject: [PATCH 30/68] Azimuthal mean, percentile distance for wind radii Add function used to calculate wind radii to user data in last column. I give up formatting output cleverly. --- atcf.py | 427 +++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 296 insertions(+), 131 deletions(-) diff --git a/atcf.py b/atcf.py index 9de784c..2adc70f 100644 --- a/atcf.py +++ b/atcf.py @@ -50,7 +50,7 @@ def getcy(cys): def read(ifile = ifile, debug=False, fullcircle=False): # Read data into Pandas Dataframe - print('reading', ifile, 'fullcircle=', fullcircle) + print('Reading', ifile, 'fullcircle=', fullcircle) names = list(atcfcolumns) # make a copy of list, not a copy of the reference to the list. converters={ # The problem with CY is ATCF only reserves 2 characters for it. @@ -110,8 +110,8 @@ def read(ifile = ifile, debug=False, fullcircle=False): testline = next(reader) num_cols = len(testline) if debug: + print("test line num_cols:", num_cols) print(testline) - print('num_cols=',num_cols) del reader # Output from HWRF vortex tracker, fort.64 and fort.66 @@ -135,7 +135,7 @@ def read(ifile = ifile, debug=False, fullcircle=False): # fort.66 has track id in the 3rd column. if num_cols == 31: - print('assume fort.66-style with 31 columns in', ifile) + print('Assuming fort.66-style with 31 columns in', ifile) # There is a cyclogenesis ID column for fort.66 if debug: print('inserted ID for cyclogenesis in column 2') @@ -234,9 +234,6 @@ def read(ifile = ifile, debug=False, fullcircle=False): if col in ['initials', 'depth']: df[col] = 'X' - - if debug: - pdb.set_trace() if fullcircle: if debug: print("full circle wind radii") @@ -250,6 +247,7 @@ def read(ifile = ifile, debug=False, fullcircle=False): # Tried converting to MultiIndex DataFrame but it led to all sorts of problems. + # TODO: try append=True to avoid losing columns when you make them an index return df @@ -263,12 +261,12 @@ def x2s(x): def lat2s(lat): NS = 'N' if lat >= 0 else 'S' - lat = x2s(lat) + NS + ',' + lat = x2s(lat) + NS return lat def lon2s(lon): EW = 'E' if lon >= 0 else 'W' - lon = '%4s' % x2s(lon) + EW + ',' + lon = x2s(lon) + EW return lon # function to compute great circle distance between point lat1 and lon1 and arrays of points @@ -282,7 +280,7 @@ def dist_bearing(lon1,lons,lat1,lats): lat1 = np.radians(lat1) lats = np.radians(lats) # great circle distance. - arg = np.sin(lat1)*np.sin(lats)+np.cos(lat1)*np.cos(lats)*np.cos(lon1-lons) + arg = np.sin(lat1)*np.sin(lats)+np.cos(lat1)*np.cos(lats)*np.cos(lon1-lons) #arg = np.where(np.fabs(arg) < 1., arg, 0.999999) dlon = lons-lon1 @@ -299,24 +297,38 @@ def dist_bearing(lon1,lons,lat1,lats): return np.arccos(arg)* a, bearing - -ms2kts = 1.94384 -km2nm = 0.539957 +ms2kts = pint.UnitRegistry()["m/s"].to("knots").magnitude # 1.94384 +km2nm = pint.UnitRegistry()["km"].to("nautical_mile").magnitude # 0.539957 quads = {'NE':0, 'SE':90, 'SW':180, 'NW':270} thresh_kts = np.array([34, 50, 64]) +def get_azimuthal_mean(x, distance_km, binsize_km = 25.): + radius = np.arange(0, max(distance_km), binsize_km) + x_vs_radius = [] # maybe should be numpy array but can't remember syntax + for r in radius: + i = (r <= distance_km) & (distance_km < r+binsize_km) + npts = np.sum(i) + if npts == 0: + print("get_azimuthal_mean: no pts b/t", r, "and", r+binsize_km) + sys.exit(1) + if npts == 1: + print("get_azimuthal_mean: only " + "%d" % npts + " grid cell b/t", r, "and", r+binsize_km) + x_vs_radius.append(np.mean(x[i])) + return x_vs_radius, radius - -def get_max_ext_of_wind(speed_kts, distance_km, bearing, raw_vmax_kts, quads=quads, thresh_kts=thresh_kts, debug=False): +def get_ext_of_wind(speed_kts, distance_km, bearing, raw_vmax_kts, quads=quads, thresh_kts=thresh_kts, + rad_search_radius_nm=300., lonCell=None, latCell=None, debug=False, wind_radii_method='max'): + # speed_kts is converted to masked array. Masked where distance >= 300 nm - rad_nm = {} + rad_nm = {"wind_radii_method":wind_radii_method} # Put in dictionary "rad_nm" where rad_nm = { # 34: {'NE':rad1, 'SE':rad2, 'SW':rad3, 'NW':rad4}, # 50: {'NE':rad1, 'SE':rad2, 'SW':rad3, 'NW':rad4}, # 64: {'NE':rad1, 'SE':rad2, 'SW':rad3, 'NW':rad4} # } + rad_search_radius_km = rad_search_radius_nm / km2nm rad_nm['raw_vmax_kts'] = raw_vmax_kts rad_nm['thresh_kts'] = thresh_kts @@ -326,29 +338,69 @@ def get_max_ext_of_wind(speed_kts, distance_km, bearing, raw_vmax_kts, quads=qua # This was to deal with Irma and the unrelated 34 knot onshore flow in Georgia # Looking at HURDAT2 R34 sizes (since 2004), ex-tropical storm Karen 2015 had 710nm. # Removing EX storms, the max was 480 nm in Hurricane Sandy 2012 - speed_kts = np.ma.array(speed_kts, mask = distance_km >= 300./km2nm) + speed_kts = np.ma.array(speed_kts, mask = distance_km >= rad_search_radius_km) for wind_thresh_kts in thresh_kts[thresh_kts < raw_vmax_kts]: - ithresh = speed_kts >= wind_thresh_kts - # warn if max_dist_of_wind_threshold is on edge of domain - imax = np.argmax(distance_km * ithresh) + ithresh = speed_kts >= wind_thresh_kts # Boolean array same shape + imax = np.argmax(distance_km * ithresh) # arg must be same shape as subscript target iedge = np.unravel_index(imax, distance_km.shape) - if iedge[0] == distance_km.shape[0]-1 or iedge[1] == distance_km.shape[1]-1 or any(iedge) == 0: - print("get_max_ext_of_wind(): R"+str(wind_thresh_kts)+" at edge of domain",iedge,"shape:",distance_km.shape) + # warn if max_dist_of_wind_threshold is on edge of 2-d domain (like nested WRF grid) + if distance_km.ndim == 2: + if debug: + print("imax:", imax) + print("iedge:", iedge) + if iedge[0] == distance_km.shape[0]-1 or iedge[1] == distance_km.shape[1]-1 or any(iedge) == 0: + print("get_ext_of_wind(): R"+str(wind_thresh_kts)+" at edge of domain",iedge,"shape:",distance_km.shape) rad_nm[wind_thresh_kts] = {} + if debug: + print('get_ext_of_wind(): method ' + wind_radii_method) + print('get_ext_of_wind(): kts quad azimuth npts dist bearing lat lon') for quad,az in quads.items(): - iquad = (az <= bearing) & (bearing < az+90) & (speed_kts >= wind_thresh_kts) - rad_nm[wind_thresh_kts][quad] = 0 - if np.sum(iquad) > 0: - max_dist_of_wind_threshold = np.max(distance_km[iquad]) * km2nm - imax_dist_of_wind_threshold = np.argmax(distance_km[iquad]) - if debug: - print('get_max_ext_of_wind():', wind_thresh_kts, quad, '%3d-%3d'%(az,az+90), '%4d'%np.sum(iquad), '%10.6f'%max_dist_of_wind_threshold, '%10.6f'%bearing[iquad][imax_dist_of_wind_threshold]) - rad_nm[wind_thresh_kts][quad] = max_dist_of_wind_threshold + # Compute azimuthal mean + if wind_radii_method == "azimuthal_mean": + # I thought I wouldn't need (distance_km < rad_search_radius_km) because speed_kts was masked beyond + # the search radius. But it makes a difference. + iquad = (az <= bearing) & (bearing < az+90) & (distance_km < rad_search_radius_km) + speed_kts_vs_radius_km, radius_km = get_azimuthal_mean(speed_kts[iquad], distance_km[iquad], binsize_km = 25.) + rad_nm[wind_thresh_kts][quad] = 0. + if any(speed_kts_vs_radius_km >= wind_thresh_kts): + max_dist_of_wind_threshold_nm = np.max(radius_km[speed_kts_vs_radius_km >= wind_thresh_kts]) * km2nm + rad_nm[wind_thresh_kts][quad] = max_dist_of_wind_threshold_nm + if debug: + print('get_ext_of_wind():', "%3d "%wind_thresh_kts, quad, ' %3d-%3d'%(az,az+90), '%4d'%np.sum(iquad), + '%6.2fnm'%max_dist_of_wind_threshold_nm, end="") + print(radius_km) + print(speed_kts_vs_radius_km) + print() + else: + # I thought I wouldn't need (distance_km < rad_search_radius_km) because speed_kts was masked beyond + # the search radius. But it makes a difference. + iquad = (az <= bearing) & (bearing < az+90) & (speed_kts >= wind_thresh_kts) & (distance_km < rad_search_radius_km) + rad_nm[wind_thresh_kts][quad] = 0. + if np.sum(iquad) > 0: + x_km = distance_km[iquad] + if wind_radii_method[-10:] == "percentile": + # assume wind_radii_method is a number followed by the string "percentile". + distance_percentile = float(wind_radii_method[:-10]) + # index of array entry nearest to percentile value + idist_of_wind_threshold=abs(x_km-np.percentile(x_km,distance_percentile,interpolation='nearest')).argmin() + rad_nm[wind_thresh_kts][quad] = np.percentile(x_km, distance_percentile) * km2nm + elif wind_radii_method == "max": + idist_of_wind_threshold = np.argmax(x_km) + rad_nm[wind_thresh_kts][quad] = np.max(x_km) * km2nm + else: + print("unexpected wind_radii_method:" + wind_radii_method) + sys.exit(1) + if debug: + print('get_ext_of_wind():', "%3d "%wind_thresh_kts, quad, ' %3d-%3d'%(az,az+90), '%4d'%np.sum(iquad), + '%6.2fnm'%rad_nm[wind_thresh_kts][quad], '%4.0fdeg'%bearing[iquad][idist_of_wind_threshold], end="") + if lonCell is not None: + print('%8.2fN'%latCell[iquad][idist_of_wind_threshold], '%7.2fE'%lonCell[iquad][idist_of_wind_threshold], end="") + print() return rad_nm -def derived_winds(u10, v10, mslp, lonCell, latCell, row, vmax_search_radius=250., mslp_search_radius=100., debug=False): +def derived_winds(u10, v10, mslp, lonCell, latCell, row, vmax_search_radius=250., mslp_search_radius=100., wind_radii_method="max", debug=False): # Given a row (with row.lon and row.lat)... @@ -398,7 +450,7 @@ def derived_winds(u10, v10, mslp, lonCell, latCell, row, vmax_search_radius=250. raw_minp = mslp[mslprad].min() / 100. # Get max extent of wind at thresh_kts thresholds. - rad_nm = get_max_ext_of_wind(speed_kts, distance_km, bearing, raw_vmax_kts, debug=debug) + rad_nm = get_ext_of_wind(speed_kts, distance_km, bearing, raw_vmax_kts, latCell=latCell, lonCell=lonCell, wind_radii_method=wind_radii_method, debug=debug) return raw_vmax_kts, raw_RMW_nm, raw_minp, rad_nm @@ -415,6 +467,10 @@ def add_wind_rad_lines(row, rad_nm, fullcircle=False, debug=False): for thresh in thresh_kts[thresh_kts < raw_vmax_kts]: if any(rad_nm[thresh].values()): newrow = row.copy() + # not sure how to do this with "rad" as part of the DataFrame index or Series name + if row.fhr > 110: + pdb.set_trace() + newrow.rename({34:thresh}, inplace=True) newrow.rad = thresh if fullcircle: # Append row with full circle 34, 50, or 64 knot radius @@ -435,9 +491,39 @@ def add_wind_rad_lines(row, rad_nm, fullcircle=False, debug=False): return lines +def update_row(row, raw_vmax_kts, raw_RMW_nm, raw_minp, rad_nm, debug=False): -def update_df(df, row, raw_vmax_kts, raw_RMW_nm, raw_minp, rad_nm, debug=False): + # TODO Call with origgridWRF, origgrid, and mpas.origmesh + + if debug: + print("atcf.update_row: before update\n", row[['valid_time','lon','lat', 'vmax', 'minp', 'rmw']]) + row["vmax"] = raw_vmax_kts + row["minp"] = raw_minp + row["rmw"] = raw_RMW_nm + # Add note of original mesh = True in user data (not defined) column + if 'origmeshTrue' not in row.userdata: + moreuserdata = 'origmeshTrue wind_radii_method '+ rad_nm["wind_radii_method"] + if debug: + print("appending "+moreuserdata+" to row.userdata") + row.userdata += moreuserdata + if debug: + print('after', row[['vmax', 'minp', 'rmw']]) + # Return row if vmax < 34 kts + if rad_nm['raw_vmax_kts'] < 34: + return row + + # Make 34/50/64 knot rows + newrows = add_wind_rad_lines(row, rad_nm, debug=debug) + if debug: + print("changed", row, " to ", newrows) + return newrows + + + +def update_df(df, row, raw_vmax_kts, raw_RMW_nm, raw_minp, rad_nm, gridfile=None, debug=False): + + # TODO: Use update_row instead. add gridfile to update_row # Called by origgrid and origmesh if debug: @@ -447,12 +533,17 @@ def update_df(df, row, raw_vmax_kts, raw_RMW_nm, raw_minp, rad_nm, debug=False): row["rmw"] = raw_RMW_nm # Add note of original mesh = True in user data (not defined) column if 'origmeshTrue' not in row.userdata: + moreuserdata = 'origmeshTrue wind_radii_method '+ rad_nm["wind_radii_method"] + if gridfile is not None: + # Append origmesh file to userdata column (after a comma) + moreuserdata += ', ' + gridfile if debug: - print(row, " is already from original mesh.") - row.userdata += 'origmeshTrue' + print("appending "+moreuserdata+" to row.userdata") + row.userdata += moreuserdata if debug: print('after', row[['vmax', 'minp', 'rmw']]) + # hacky - can probably be cleaner. why the [0]? avoid (1, 44) with (44,) setting mismatch error df.loc[row.name,:] = row # Append 34/50/64 knot lines to DataFrame @@ -464,7 +555,10 @@ def update_df(df, row, raw_vmax_kts, raw_RMW_nm, raw_minp, rad_nm, debug=False): df.drop(row.name, inplace=True) if debug: print("appending ", newlines) - df = df.append(newlines, sort=False) + df = df.append(newlines, sort=False) + # Sort DataFrame by index (deal with appended wind radii lines) + # sort by rad too. I tried to avoid this, but when rad=0, it would be left behind other fhrs that had rad>0. + df = df.sort_index().sort_values(['initial_time','fhr','rad']) return df @@ -480,94 +574,145 @@ def write(ofile, df, fullcircle=False, debug=False): # TODO: deal with fullcircle. print("writing", ofile) - # Valid time is not part of ATCF file. - del(df["valid_time"]) - if debug: pdb.set_trace() - # Had to add parentheses () to .len to not get error about instancemethod not being iterable. - if max(df.cy.str.len()) > 2: - print('cy more than 2 characters. Truncating...') - # Append full CY to userdata column (after a comma) - df['userdata'] = df['userdata'] + ', ' + df['cy'] - # Keep first 2 characters - df['cy'] = df['cy'].str.slice(0,2) - formatters={ - "basin": '{},'.format, - # The problem with CY is ATCF only reserves 2 characters for it. - "cy": lambda x: x.zfill(2)+"," , # not always an integer (e.g. 10E) # 20181116 force to be integer - # Convert initial_time from datetime to string. - "initial_time":lambda x: x.strftime('%Y%m%d%H,'), - "technum":'{},'.format, - "model":'{},'.format, - "fhr":'{:3.0f},'.format, - "lat":lat2s, - "lon":lon2s, - "vmax":'{:3.0f},'.format, - "minp":'{:4.0f},'.format, - "ty":'{},'.format, - "windcode":'{:>3s},'.format, - "rad":'{:3.0f},'.format, - "rad1":'{:4.0f},'.format, - "rad2":'{:4.0f},'.format, - "rad3":'{:4.0f},'.format, - "rad4":'{:4.0f},'.format, - "pouter":'{:4.0f},'.format, - "router":'{:4.0f},'.format, - "rmw":'{:3.0f},'.format, - "gusts":'{:3.0f},'.format, - "eye":'{:3.0f},'.format, - "subregion":'{:>2s},'.format, - "maxseas":'{:3.0f},'.format, - "initials":'{:>3s},'.format, - "dir":'{:3.0f},'.format, - "speed":'{:3.0f},'.format, - "stormname":'{:>9s},'.format, - "depth":'{:>1s},'.format, - "seas":'{:2.0f},'.format, - "seascode":'{:>3s},'.format, - "seas1":'{:4.0f},'.format, - "seas2":'{:4.0f},'.format, - "seas3":'{:4.0f},'.format, - "seas4":'{:4.0f},'.format, - "userdefined":'{:>18s},'.format, - "cpsB":'{:4.0f},'.format, - "cpsll":'{:4.0f},'.format, - "cpsul":'{:4.0f},'.format, - "warmcore":'{},'.format, - "direction":'{},'.format, - } - junk = df.to_string(header=False, index=False, na_rep=' ', columns=atcfcolumns, formatters=formatters) - - # TODO: FIX IDL-STYLE COLUMNS. STORMNAME IS MISSING A LETTER - - # na_rep=' ' has no effect - # strings have extra space in front of them - junk = junk.split('\n') - # replace first 3 occurrences of ', ' with ', '. - # replace ', XX,' with ', XX,' - # replace 'nan' with ' ' - junk = [j.replace(', ', ', ', 3).replace(', XX,',', XX,').replace('nan',' ') for j in junk] - #delete space before windcode (e.g. NEQ) - junk = [j[:68]+j[69:] for j in junk] - #delete space before initials - junk = [j[:133]+j[134:] for j in junk] - #delete space before depth - junk = [j[:161]+j[162:] for j in junk] - #delete space before seascode - junk = [j[:168]+j[169:] for j in junk] - junk = '\n'.join(junk) + atcf_lines = "" + for index, row in df.iterrows(): + atcf_lines += "{:2s}, ".format(row.basin) + atcf_lines += "{:2s}, ".format(row.cy.zfill(2)) + atcf_lines += "{:8s}, ".format(row.initial_time.strftime('%Y%m%d%H')) + atcf_lines += "{}, ".format(row.technum) + atcf_lines += "{}, ".format(row.model) + atcf_lines += "{:3.0f}, ".format(row.fhr) + atcf_lines += "{:>4s}, ".format(lat2s(row.lat)) + atcf_lines += "{:>5s}, ".format(lon2s(row.lon)) + atcf_lines += "{:3.0f}, ".format(row.vmax) + atcf_lines += "{:4.0f}, ".format(row.minp) + atcf_lines += "{}, ".format(row.ty) + atcf_lines += "{:3.0f}, ".format(row.rad) + atcf_lines += "{:>3s}, ".format(row.windcode) + atcf_lines += "{:4.0f}, ".format(row.rad1) + atcf_lines += "{:4.0f}, ".format(row.rad2) + atcf_lines += "{:4.0f}, ".format(row.rad3) + atcf_lines += "{:4.0f}, ".format(row.rad4) + atcf_lines += "{:4.0f}, ".format(row.pouter) + atcf_lines += "{:4.0f}, ".format(row.router) + atcf_lines += "{:3.0f}, ".format(row.rmw) + atcf_lines += "{:3.0f}, ".format(row.gusts) + atcf_lines += "{:3.0f}, ".format(row.eye) + atcf_lines += "{:>3s}, ".format(row.subregion) # supposedly 1 character, but always 3 in official b-decks + atcf_lines += "{:3.0f}, ".format(row.maxseas) + atcf_lines += "{:>3s}, ".format(row.initials) + atcf_lines += "{:3.0f}, ".format(row.dir) + atcf_lines += "{:3.0f}, ".format(row.speed) + atcf_lines += "{:>10s}, ".format(row.stormname) + atcf_lines += "{:>1s}, ".format(row.depth) + atcf_lines += "{:2.0f}, ".format(row.seas) + atcf_lines += "{:>3s}, ".format(row.seascode) + atcf_lines += "{:4.0f}, ".format(row.seas1) + atcf_lines += "{:4.0f}, ".format(row.seas2) + atcf_lines += "{:4.0f}, ".format(row.seas3) + atcf_lines += "{:4.0f}, ".format(row.seas4) + atcf_lines += "{:>20s}, ".format(row.userdefined) # Described as 20 chars in atcf doc. + atcf_lines += "{}, ".format(row.userdata) + atcf_lines += "\n" + + atcf_lines = atcf_lines.replace("nan"," ") + + if False: + + # I couldn't get this section to work. + # The pandas to_string method inexplicibly padded some columns with an extra space that had to be removed later. + + # Valid time is not part of ATCF file. + del(df["valid_time"]) + + # Had to add parentheses () to .len to not get error about instancemethod not being iterable. + if max(df.cy.str.len()) > 2: + print('cy more than 2 characters. Truncating...') + # Append full CY to userdata column (after a comma) + df['userdata'] = df['userdata'] + ', ' + df['cy'] + # Keep first 2 characters + df['cy'] = df['cy'].str.slice(0,2) + formatters={ + "basin": '{},'.format, + # The problem with CY is ATCF only reserves 2 characters for it. + "cy": lambda x: x.zfill(2)+"," , # not always an integer (e.g. 10E) # 20181116 force to be integer + # Convert initial_time from datetime to string. + "initial_time":lambda x: x.strftime('%Y%m%d%H,'), + "technum":'{},'.format, + "model":'{},'.format, + "fhr":'{:3.0f},'.format, + "lat":lat2s, + "lon":lon2s, + "vmax":'{:3.0f},'.format, + "minp":'{:4.0f},'.format, + "ty":'{},'.format, + "windcode":'{:>3s},'.format, + "rad":'{:3.0f},'.format, + "rad1":'{:4.0f},'.format, + "rad2":'{:4.0f},'.format, + "rad3":'{:4.0f},'.format, + "rad4":'{:4.0f},'.format, + "pouter":'{:4.0f},'.format, + "router":'{:4.0f},'.format, + "rmw":'{:3.0f},'.format, + "gusts":'{:3.0f},'.format, + "eye":'{:3.0f},'.format, + "subregion":'{:>2s},'.format, + "maxseas":'{:3.0f},'.format, + "initials":'{:>3s},'.format, + "dir":'{:3.0f},'.format, + "speed":'{:3.0f},'.format, + "stormname":'{:>9s},'.format, + "depth":'{:>1s},'.format, + "seas":'{:2.0f},'.format, + "seascode":'{:>3s},'.format, + "seas1":'{:4.0f},'.format, + "seas2":'{:4.0f},'.format, + "seas3":'{:4.0f},'.format, + "seas4":'{:4.0f},'.format, + "userdefined":'{:>18s},'.format, + "userdata":'{},'.format, + "cpsB":'{:4.0f},'.format, + "cpsll":'{:4.0f},'.format, + "cpsul":'{:4.0f},'.format, + "warmcore":'{},'.format, + "direction":'{},'.format, + } + junk = df.to_string(header=False, index=False, na_rep=' ', formatters=formatters, columns=atcfcolumns) + + + + # TODO: FIX IDL-STYLE COLUMNS. STORMNAME IS MISSING A LETTER + + # na_rep=' ' has no effect + # strings have extra space in front of them + junk = junk.split('\n') + # replace first 3 occurrences of ', ' with ', '. + # replace ', XX,' with ', XX,' + # replace 'nan' with ' ' + junk = [j.replace(', ', ', ', 3).replace(', XX,',', XX,').replace('nan',' ') for j in junk] + #delete space before windcode (e.g. NEQ) + junk = [j[:68]+j[69:] for j in junk] + #delete space before initials + junk = [j[:133]+j[134:] for j in junk] + #delete space before depth + junk = [j[:161]+j[162:] for j in junk] + #delete space before seascode + junk = [j[:168]+j[169:] for j in junk] + junk = '\n'.join(junk) if debug: pdb.set_trace() f = open(ofile, "w") - f.write(junk+"\n") + f.write(atcf_lines) f.close() print("wrote", ofile) -def origgridWRF(df, griddir, grid="d03", debug=False): +def origgridWRF(df, griddir, grid="d03", wind_radii_method = "max", debug=False): # Get vmax, minp, radius of max wind, max radii of wind thresholds from WRF by Alex Kowaleski WRFmember = df.model.str.extract(r'WF(\d\d)', flags=re.IGNORECASE) @@ -601,7 +746,7 @@ def origgridWRF(df, griddir, grid="d03", debug=False): if debug: print("Extract vmax, RMW, minp, and radii of wind thresholds from row", row.name) - raw_vmax_kts, raw_RMW_nm, raw_minp, rad_nm = derived_winds(u10, v10, mslp, lonCell, latCell, row, debug=debug) + raw_vmax_kts, raw_RMW_nm, raw_minp, rad_nm = derived_winds(u10, v10, mslp, lonCell, latCell, row, wind_radii_method=wind_radii_method, debug=debug) df = update_df(df, row, raw_vmax_kts, raw_RMW_nm, raw_minp, rad_nm, debug=debug) # Sort DataFrame by index (deal with appended wind radii lines) @@ -609,15 +754,28 @@ def origgridWRF(df, griddir, grid="d03", debug=False): df = df.sort_index().sort_values(['initial_time','fhr','rad']) return df +def get_var_with_str(nc, s): + matching_vars = [v for v in nc.variables if s.lower() in v.lower()] # .lower() makes it case-insensitive + if len(matching_vars) != 1: + print("number of matching variables not 1") + print(nc.variables, s, matching_vars) + sys.exit(1) + return matching_vars[0] -def origgrid(df, griddir, debug=False): +def origgrid(df, griddir, ensemble_prefix="ens_", wind_radii_method="max", debug=False): # Get vmax, minp, radius of max wind, max radii of wind thresholds from ECMWF grid, not from tracker. # Assumes # ECMWF data came from TIGGE and were converted from GRIB to netCDF with ncl_convert2nc. # 4-character model string in ATCF file is "EExx" (where xx is the 2-digit ensemble member). # ECMWF ensemble member in directory named "ens_xx" (where xx is the 2-digit ensemble member). # File path is "ens_xx/${gs}yyyymmddhh.xx.nc", where ${gs} is the grid spacing (0p15, 0p25, or 0p5). + # ensemble_prefix may be a single string or a list of strings + + if isinstance(ensemble_prefix, str): + ensemble_prefixes = [ensemble_prefix] + elif isinstance(ensemble_prefix, (list, tuple)): + ensemble_prefixes = ensemble_prefix # TODO: Why group rows by initial_time and model? Why not process each row independently? for run_id, group in df.groupby(['initial_time', 'model']): @@ -629,22 +787,29 @@ def origgrid(df, griddir, debug=False): print('no original grid for',model,'- skipping') continue ens = int(m.group(1)) # strip leading zero - if ens < 1: - continue + + # used to skip EE00 because I didn't know how to handle control run. Now it is handled. + #if ens < 1: + # continue + # Allow some naming conventions # ens_n/yyyymmddhh.n.nc # ens_n/0p15yyyymmddhh_sfc.nc # ens_n/0p25yyyymmddhh_sfc.nc # ens_n/0p5yyyymmddhh_sfc.nc yyyymmddhh = initial_time.strftime('%Y%m%d%H') - # If first filename doesn't exist, try the next one, and so on... - # List in order of most preferred to least preferred. - potential_gridfiles = [ - "ens_"+str(ens)+"/"+ "0p15"+yyyymmddhh+"."+str(ens)+".nc", - "ens_"+str(ens)+"/"+ "0p25"+yyyymmddhh+"."+str(ens)+".nc", - "ens_"+str(ens)+"/"+ "0p5"+yyyymmddhh+"."+str(ens)+".nc", - "ens_"+str(ens)+"/"+ yyyymmddhh+"."+str(ens)+".nc" - ] + yyyymmdd_hhmm = initial_time.strftime('%Y%m%d_%H%M') + potential_gridfiles = [] + for ensemble_prefix in ensemble_prefixes: + # If first filename doesn't exist, try the next one, and so on... + # List in order of most preferred to least preferred. + potential_gridfiles.extend([ + ensemble_prefix+str(ens)+"/SFC_"+yyyymmdd_hhmm+".nc", # Linus-style + ensemble_prefix+str(ens)+"/"+ "0p15"+yyyymmddhh+"."+str(ens)+".nc", + ensemble_prefix+str(ens)+"/"+ "0p25"+yyyymmddhh+"."+str(ens)+".nc", + ensemble_prefix+str(ens)+"/"+ "0p5"+yyyymmddhh+"."+str(ens)+".nc", + ensemble_prefix+str(ens)+"/"+ yyyymmddhh+"."+str(ens)+".nc" + ]) for gridfile in potential_gridfiles: if os.path.isfile(griddir + gridfile): break @@ -653,12 +818,12 @@ def origgrid(df, griddir, debug=False): print('opening', gridfile) nc = Dataset(griddir + gridfile, "r") - lon = nc.variables['lon_0'][:] - lat = nc.variables['lat_0'][:] + lon = nc.variables[get_var_with_str(nc, 'lon_')][:] + lat = nc.variables[get_var_with_str(nc, 'lat_')][:] lonCell,latCell = np.meshgrid(lon, lat) - u10s = nc.variables['10u_P1_L103_GLL0'][:] - v10s = nc.variables['10v_P1_L103_GLL0'][:] - mslps = nc.variables['msl_P1_L101_GLL0'][:] + u10s = nc.variables[get_var_with_str(nc, '10u')][:] + v10s = nc.variables[get_var_with_str(nc, '10v')][:] + mslps = nc.variables[get_var_with_str(nc, 'msl')][:] model_forecast_times = nc.variables['forecast_time0'][:] nc.close() for index, row in group.iterrows(): @@ -671,10 +836,10 @@ def origgrid(df, griddir, debug=False): mslp = mslps[itime,:,:] # Extract vmax, RMW, minp, and radii of wind thresholds - raw_vmax_kts, raw_RMW_nm, raw_minp, rad_nm = derived_winds(u10, v10, mslp, lonCell, latCell, row, debug=debug) + raw_vmax_kts, raw_RMW_nm, raw_minp, rad_nm = derived_winds(u10, v10, mslp, lonCell, latCell, row, wind_radii_method=wind_radii_method, debug=debug) - df = update_df(df, row, raw_vmax_kts, raw_RMW_nm, raw_minp, rad_nm, debug=debug) + df = update_df(df, row, raw_vmax_kts, raw_RMW_nm, raw_minp, rad_nm, gridfile=gridfile, debug=debug) return df From dd89cd5b373aff3201e7e6bc4bb09c3c9f18e2f4 Mon Sep 17 00:00:00 2001 From: ahijevyc Date: Tue, 23 Jul 2019 10:07:34 -0600 Subject: [PATCH 31/68] added TODO and debug pause --- mpas.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mpas.py b/mpas.py index f6e96b7..68ed9b2 100644 --- a/mpas.py +++ b/mpas.py @@ -66,7 +66,12 @@ def origmesh(df, initfile, diagdir, debug=False): # Extract vmax, RMW, minp, and radii of wind thresholds raw_vmax_kts, raw_RMW_nm, raw_minp, rad_nm = atcf.derived_winds(u10, v10, mslp, lonCell, latCell, row, debug=debug) + # TODO: figure out how to replace the row with (possibly) multiple rows with different wind radii + # without passing df, the entire DataFrame df = atcf.update_df(df, row, raw_vmax_kts, raw_RMW_nm, raw_minp, rad_nm, debug=debug) + if debug: + print("mpas.origmesh() pausing before return") + pdb.set_trace() return df, initfile From bda79d050d75ea593acd3e85ebbf123825f3f88e Mon Sep 17 00:00:00 2001 From: ahijevyc Date: Tue, 23 Jul 2019 10:07:53 -0600 Subject: [PATCH 32/68] debug option --- mysavfig.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mysavfig.py b/mysavfig.py index ba95e13..5ca91de 100644 --- a/mysavfig.py +++ b/mysavfig.py @@ -1,7 +1,9 @@ import matplotlib.pyplot as plt from subprocess import call # to use "mogrify" import datetime as dt -def mysavfig(ofile, string="", timestamp=True, size=5, **kwargs): +def mysavfig(ofile, string="", timestamp=True, size=5, debug=False, **kwargs): + if debug: + print('mysavfig: timestamp=',timestamp) if timestamp: string = string + '\n' + 'created '+str(dt.datetime.now(tz=None)).split('.')[0] # + '\n' # extra newline to keep timestamp onscreen. th = plt.annotate(string, xy=(2,1), xycoords='figure pixels', horizontalalignment='left', verticalalignment='bottom', size=size) From 3f27e189bf0cb7e3d305dff4747e02d2d3a58038 Mon Sep 17 00:00:00 2001 From: ahijevyc Date: Tue, 23 Jul 2019 10:08:13 -0600 Subject: [PATCH 33/68] No PYTHONPATH stuff Don't print missing indices on side of plot. --- myskewt.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/myskewt.py b/myskewt.py index d41ea8b..b8e9a1b 100755 --- a/myskewt.py +++ b/myskewt.py @@ -1,7 +1,7 @@ import sys import pdb -sys.path.append('/glade/u/home/ahijevyc/lib/python2.7/site-packages/SHARPpy-1.3.0-py2.7.egg') -sys.path.append('/glade/u/home/ahijevyc/lib/python2.7/site-packages') +#sys.path.append('/glade/u/home/ahijevyc/lib/python2.7/site-packages/SHARPpy-1.3.0-py2.7.egg') +#sys.path.append('/glade/u/home/ahijevyc/lib/python2.7/site-packages') import sharppy import sharppy.sharptab.interp as interp import sharppy.sharptab.params as params @@ -11,7 +11,7 @@ import sharppy.sharptab.winds as winds import numpy as np -from StringIO import StringIO +from io import StringIO import matplotlib.pyplot as plt import cartopy @@ -34,8 +34,8 @@ def parseCLS(sfile): data = np.genfromtxt( sound_data ) clean_data = [] for i in data: - if i[1] != 9999 and i[2] != 999 and i[3] != 999 and i[7] != 999 and i[8] != 999 and i[14] != 99999: - clean_data.append(i) + if i[1] != 9999 and i[2] != 999 and i[3] != 999 and i[7] != 999 and i[8] != 999 and i[14] != 99999: + clean_data.append(i) p = np.array([i[1] for i in clean_data]) h = np.array([i[14] for i in clean_data]) T = np.array([i[2] for i in clean_data]) @@ -48,7 +48,7 @@ def parseCLS(sfile): max_points = 250 s = p.size/max_points if s == 0: s = 1 - print "stride=",s + print("stride=",s) return p[::s], h[::s], T[::s], Td[::s], wdir[::s], wspd[::s], latitude, longitude def thetas(tempC, presvals): @@ -189,16 +189,18 @@ def wind_barb_spaced(ax, prof, xpos=1.0, yspace=0.04): def add_globe(longitude, latitude): + # TODO: avoid matplotlib depreciation warning about creating a unique id for each axes instance. You need a new axes + # instance whenever lat/lon changes. # Globe with dot on location. mapax = plt.axes([.795, 0.09,.18,.18], projection=cartopy.crs.Orthographic(longitude, latitude)) mapax.add_feature(cartopy.feature.OCEAN, zorder=0) - mapax.add_feature(cartopy.feature.LAND, zorder=0) + mapax.add_feature(cartopy.feature.LAND, zorder=0, linewidth=0) # linewidth=0 or coastlines are fuzzy mapax.set_global() sloc = mapax.plot(longitude, latitude,'o', color='green', markeredgewidth=0, markersize=4., transform=cartopy.crs.Geodetic()) return mapax -def indices(prof): +def indices(prof, debug=False): # return a formatted-string list of stability and kinematic indices @@ -286,7 +288,14 @@ def indices(prof): # Update the indices within the indices dictionary on the side of the plot. string = '' - for index, value in sorted(indices.iteritems()): + for index, value in sorted(indices.items()): + if np.ma.is_masked(value[0]): + if debug: + print("skipping masked value for index=",index) + continue + if debug: + print("index=",index) + print("value=",value) format = '%.'+str(value[1])+'f' string += index + ": " + format % value[0] + " " + value[2] + '\n' From fb0f8bb9992296d6bee2b4197b13023e72fbc59f Mon Sep 17 00:00:00 2001 From: ahijevyc Date: Tue, 23 Jul 2019 11:40:17 -0600 Subject: [PATCH 34/68] removed scripts unrelated to webplot --- Fanglin.py | 19 -- atcf.py | 847 ---------------------------------------------- cache.py | 80 ----- get_model_time.py | 63 ---- mysavfig.py | 18 - myskewt.py | 303 ----------------- spc.py | 359 -------------------- 7 files changed, 1689 deletions(-) delete mode 100644 Fanglin.py delete mode 100644 atcf.py delete mode 100644 cache.py delete mode 100644 get_model_time.py delete mode 100644 mysavfig.py delete mode 100755 myskewt.py delete mode 100644 spc.py diff --git a/Fanglin.py b/Fanglin.py deleted file mode 100644 index ba4f688..0000000 --- a/Fanglin.py +++ /dev/null @@ -1,19 +0,0 @@ -import numpy as np - -def confidence_interval(x): - nsz = x.size - std = np.std(x) - # From 11 Aug 2016 email from Fanglin - # 95% confidence level defined by {-intvl, intvl} - if nsz>=80: - intvl=1.960*std/np.sqrt(nsz-1) - if nsz>=40 and nsz <80: - intvl=2.000*std/np.sqrt(nsz-1) - if nsz>=20 and nsz <40: - intvl=2.042*std/np.sqrt(nsz-1) - if nsz<20: - intvl=2.228*std/np.sqrt(nsz-1) - - return intvl - - diff --git a/atcf.py b/atcf.py deleted file mode 100644 index 2adc70f..0000000 --- a/atcf.py +++ /dev/null @@ -1,847 +0,0 @@ -import pandas as pd -import pdb -import re -import pint -import csv -import os, sys -from netCDF4 import Dataset -import numpy as np - - -def kts2category(kts): - category = -1 - if kts > 34: - category = 0 - if kts > 64: - category = 1 - if kts > 83: - category = 2 - if kts > 96: - category = 3 - if kts > 113: - category = 4 - if kts > 137: - category = 5 - return category - - -ifile = '/glade/work/ahijevyc/work/atcf/Irma.ECMWF.dat' -ifile = '/glade/scratch/mpasrt/uni/2018071700/latlon_0.500deg_0.25km/gfdl_tracker/tcgen/fort.64' - - - -# Standard ATCF columns (doesn't include track id, like in fort.66). -# https://www.nrlmry.navy.mil/atcf_web/docs/database/new/abrdeck.html -atcfcolumns=["basin","cy","initial_time","technum","model","fhr","lat","lon","vmax","minp","ty", - "rad", "windcode", "rad1", "rad2", "rad3", "rad4", "pouter", "router", "rmw", "gusts", "eye", - "subregion", "maxseas", "initials", "dir", "speed", "stormname", "depth", "seas", "seascode", - "seas1", "seas2", "seas3", "seas4", "userdefined", "userdata"] - -def cyclone_phase_space_columns(): - names = [] - names.append('cpsB') # Cyclone Phase Space "Parameter B" for thermal asymmetry. (Values are *10) - names.append('cpsll') # Cyclone Phase Space lower level (600-900 mb) thermal wind parameter, for diagnosing low-level warm core. (Values are *10) - names.append('cpsul') # Cyclone Phase Space upper level (300-600 mb) thermal wind parameter, for diagnosing upper-level warm core. (Values are *10) - return names - - -def getcy(cys): - return cys[0:2] - -def read(ifile = ifile, debug=False, fullcircle=False): - # Read data into Pandas Dataframe - print('Reading', ifile, 'fullcircle=', fullcircle) - names = list(atcfcolumns) # make a copy of list, not a copy of the reference to the list. - converters={ - # The problem with CY is ATCF only reserves 2 characters for it. - "cy" : lambda x: x.strip(), # cy is not always an integer (e.g. 10E) # Why strip leading zeros? - "initial_time" : lambda x: pd.to_datetime(x.strip(),format='%Y%m%d%H'), - "technum" : lambda x: x.strip(), #strip leading spaces but not leading zeros - "model" : lambda x: x.strip(), # Strip leading whitespace - for matching later. - "vmax": float, - "minp": float, - "minp": float, - "ty" : lambda x: x.strip(), - "windcode" : lambda x: x[-3:], - "rad1": float, - "rad2": float, - "rad3": float, - "rad4": float, - "pouter": float, - "router": float, - "subregion": lambda x: x[-2:], - # subregion ends up being 3 when written with .to_string - # strange subregion only needs one character, but official a-decks leave 3. - "initials" : lambda x: x[-3:], - 'stormname': lambda x: x[-9:], - 'depth' : lambda x: x[-1:], - "seascode" : lambda x: x[-3:], - "seas1": float, - "seas2": float, - "seas3": float, - "seas4": float, - "userdefined" : lambda x: x.strip(), - "userdata" : lambda x: x.strip(), - } - dtype={ - 'rmw' : np.float64, - 'gusts' : np.float64, - 'eye' : np.float64, - 'maxseas' : np.float64, - 'dir' : np.float64, - 'speed' : np.float64, - "seas" : np.float64, - } - - # Tried using converter for these columns, but couldn't convert 4-space string to float. - # If you add a key na_values, also add it to dtype dict, and remove it from converters. - na_values = { - "rmw" : 4*' ', - "gusts" : 4*' ', - "eye" : 4*' ', - "maxseas" : 4*' ', - "dir" : 4*' ', - "speed" : 4*' ', - "seas" : 3*' ', # one less than other columns - } - - - reader = csv.reader(open(ifile),delimiter=',') - testline = next(reader) - num_cols = len(testline) - if debug: - print("test line num_cols:", num_cols) - print(testline) - del reader - - # Output from HWRF vortex tracker, fort.64 and fort.66 - # are mostly ATCF format but have subset of columns - if num_cols == 43: - print('assume HWRF tracker fort.64-style output with 43 columns in', ifile) - TPstr = "THERMO PARAMS" - if testline[35].strip() != TPstr: - print("expected 36th column to be", TPstr) - print("got", testline[35].strip()) - sys.exit(4) - for ii in range(20,35): - names[ii] = "space filler" - names = names[0:35] - names.append(TPstr) - names.extend(cyclone_phase_space_columns()) - names.append('warmcore') - names.append("warmcore_strength") - names.append("string") - names.append("string") - - # fort.66 has track id in the 3rd column. - if num_cols == 31: - print('Assuming fort.66-style with 31 columns in', ifile) - # There is a cyclogenesis ID column for fort.66 - if debug: - print('inserted ID for cyclogenesis in column 2') - names.insert(2, 'id') # ID for the cyclogenesis - print('Using 1st 21 elements of names list') - names = names[0:21] - if debug: - print('redefining columns 22-31') - names.extend(cyclone_phase_space_columns()) - names.append('warmcore') - names.append('dir') - names.append('speedms') - names.append('vort850mb') - names.append('maxvort850mb') - names.append('vort700mb') - names.append('maxvort700mb') - - # TODO read IDL output - if num_cols == 44 and 'min_warmcore_fract d' in testline[35]: - if debug: - print("Looks like IDL output") - names = [n.replace('userdata', 'min_warmcore_fract') for n in names] - names.append('dT500') - names.append('dT200') - names.append('ddZ850200') - names.append('rainc') - names.append('rainnc') - names.append('id') - - if num_cols == 11: - print("Assuming simple adeck with 11 columns") - if ifile[-4:] != '.dat': - print("even though file doesn't end in .dat", ifile) - names = names[0:11] - - usecols = list(range(len(names))) - - # If you get a beyond index range (or something like that) error, see if userdata column is intermittent and has commas in it. - # If so, clean it up (i.e. truncate it) - - #atrack = ['basin','cy','initial_time','technum','model'] - #if 'id' in names: - # atrack.append('id') - - if debug: - print("before pd.read_csv") - print('column names', names) - print("usecols=",usecols) - print("converters=",converters) - print("dype=", dtype) - df = pd.read_csv(ifile,index_col=None,header=None, delimiter=",", usecols=usecols, names=names, - converters=converters, na_values=na_values, dtype=dtype) - # fort.64 has asterisks sometimes. Problem with hwrf_tracker. - badlines = df['lon'].str.contains("\*") - if any(badlines): - df = df[~badlines] - - # Extract last character of lat and lon columns - # Multiply integer by -1 if "S" or "W" - # Divide by 10 - S = df.lat.str[-1] == 'S' - lat = df.lat.str[:-1].astype(float) / 10. - lat[S] = lat[S] * -1 - df.lat = lat - W = df.lon.str[-1] == 'W' - lon = df.lon.str[:-1].astype(float) / 10. - lon[W] = lon[W] * -1 - df.lon = lon - - if debug: - pdb.set_trace() - # Derive valid time. valid_time = initial_time + fhr - # Use datetime module to add, where yyyymmddh is a datetime object and fhr is a timedelta object. - df['valid_time'] = df.initial_time + pd.to_timedelta(df.fhr, unit='h') - - for col in atcfcolumns: - if col not in df.columns: - if debug: - print(col, 'not in DataFrame. Fill with appropriate value.') - # if 'rad' column doesn't exist make it zeroes - if col in ['rad', 'rad1', 'rad2', 'rad3', 'rad4','pouter', 'router', 'seas', 'seas1','seas2','seas3','seas4']: - df[col] = 0. - - # Initialize other default values. - if col in ['windcode', 'seascode']: - df[col] = ' ' - - # Numbers are NaN - if col in ['rmw','gusts','eye','maxseas','dir','speed']: - df[col] = np.NaN - - # Strings are empty - if col in ['subregion','stormname','depth','userdefined','userdata']: - df[col] = '' - - if col in ['initials', 'depth']: - df[col] = 'X' - - if fullcircle: - if debug: - print("full circle wind radii") - # Full circle wind radii instead of quadrants - # TODO better way than this hack - df['windcode'] = 'AAA' - df['rad1'] = df[['rad1','rad2','rad3','rad4']].max(axis=1) - df['rad2'] = 0 - df['rad3'] = 0 - df['rad4'] = 0 - - - # Tried converting to MultiIndex DataFrame but it led to all sorts of problems. - # TODO: try append=True to avoid losing columns when you make them an index - - return df - -def x2s(x): - # Convert absolute value of float to integer number of tenths for ATCF lat/lon - # called by lat2s and lon2s - x *= 10 - x = np.around(x) - x = np.abs(x) - return str(int(x)) - -def lat2s(lat): - NS = 'N' if lat >= 0 else 'S' - lat = x2s(lat) + NS - return lat - -def lon2s(lon): - EW = 'E' if lon >= 0 else 'W' - lon = x2s(lon) + EW - return lon - -# function to compute great circle distance between point lat1 and lon1 and arrays of points -# given by lons, lats -# Returns 2 things: -# 1) distance in km -# 2) initial bearing from 1st pt to 2nd pt. -def dist_bearing(lon1,lons,lat1,lats): - lon1 = np.radians(lon1) - lons = np.radians(lons) - lat1 = np.radians(lat1) - lats = np.radians(lats) - # great circle distance. - arg = np.sin(lat1)*np.sin(lats)+np.cos(lat1)*np.cos(lats)*np.cos(lon1-lons) - #arg = np.where(np.fabs(arg) < 1., arg, 0.999999) - - dlon = lons-lon1 - bearing = np.arctan2(np.sin(dlon)*np.cos(lats), np.cos(lat1)*np.sin(lats) - np.sin(lat1)*np.cos(lats)*np.cos(dlon)) - - # convert from radians to degrees - bearing = np.degrees(bearing) - - # -180 - 180 -> 0 - 360 - bearing = (bearing + 360) % 360 - - # Ellipsoid [CLARKE 1866] Semi-Major Axis (Equatorial Radius) - a = 6378.2064 - return np.arccos(arg)* a, bearing - - -ms2kts = pint.UnitRegistry()["m/s"].to("knots").magnitude # 1.94384 -km2nm = pint.UnitRegistry()["km"].to("nautical_mile").magnitude # 0.539957 - -quads = {'NE':0, 'SE':90, 'SW':180, 'NW':270} -thresh_kts = np.array([34, 50, 64]) - -def get_azimuthal_mean(x, distance_km, binsize_km = 25.): - radius = np.arange(0, max(distance_km), binsize_km) - x_vs_radius = [] # maybe should be numpy array but can't remember syntax - for r in radius: - i = (r <= distance_km) & (distance_km < r+binsize_km) - npts = np.sum(i) - if npts == 0: - print("get_azimuthal_mean: no pts b/t", r, "and", r+binsize_km) - sys.exit(1) - if npts == 1: - print("get_azimuthal_mean: only " + "%d" % npts + " grid cell b/t", r, "and", r+binsize_km) - x_vs_radius.append(np.mean(x[i])) - return x_vs_radius, radius - -def get_ext_of_wind(speed_kts, distance_km, bearing, raw_vmax_kts, quads=quads, thresh_kts=thresh_kts, - rad_search_radius_nm=300., lonCell=None, latCell=None, debug=False, wind_radii_method='max'): - - # speed_kts is converted to masked array. Masked where distance >= 300 nm - rad_nm = {"wind_radii_method":wind_radii_method} - # Put in dictionary "rad_nm" where rad_nm = { - # 34: {'NE':rad1, 'SE':rad2, 'SW':rad3, 'NW':rad4}, - # 50: {'NE':rad1, 'SE':rad2, 'SW':rad3, 'NW':rad4}, - # 64: {'NE':rad1, 'SE':rad2, 'SW':rad3, 'NW':rad4} - # } - - rad_search_radius_km = rad_search_radius_nm / km2nm - - rad_nm['raw_vmax_kts'] = raw_vmax_kts - rad_nm['thresh_kts'] = thresh_kts - rad_nm['quads'] = quads - - # Originally had distance_km < 800, but Chris D. suggested 300nm in Sep 2018 email - # This was to deal with Irma and the unrelated 34 knot onshore flow in Georgia - # Looking at HURDAT2 R34 sizes (since 2004), ex-tropical storm Karen 2015 had 710nm. - # Removing EX storms, the max was 480 nm in Hurricane Sandy 2012 - speed_kts = np.ma.array(speed_kts, mask = distance_km >= rad_search_radius_km) - - for wind_thresh_kts in thresh_kts[thresh_kts < raw_vmax_kts]: - ithresh = speed_kts >= wind_thresh_kts # Boolean array same shape - imax = np.argmax(distance_km * ithresh) # arg must be same shape as subscript target - iedge = np.unravel_index(imax, distance_km.shape) - # warn if max_dist_of_wind_threshold is on edge of 2-d domain (like nested WRF grid) - if distance_km.ndim == 2: - if debug: - print("imax:", imax) - print("iedge:", iedge) - if iedge[0] == distance_km.shape[0]-1 or iedge[1] == distance_km.shape[1]-1 or any(iedge) == 0: - print("get_ext_of_wind(): R"+str(wind_thresh_kts)+" at edge of domain",iedge,"shape:",distance_km.shape) - rad_nm[wind_thresh_kts] = {} - if debug: - print('get_ext_of_wind(): method ' + wind_radii_method) - print('get_ext_of_wind(): kts quad azimuth npts dist bearing lat lon') - for quad,az in quads.items(): - # Compute azimuthal mean - if wind_radii_method == "azimuthal_mean": - # I thought I wouldn't need (distance_km < rad_search_radius_km) because speed_kts was masked beyond - # the search radius. But it makes a difference. - iquad = (az <= bearing) & (bearing < az+90) & (distance_km < rad_search_radius_km) - speed_kts_vs_radius_km, radius_km = get_azimuthal_mean(speed_kts[iquad], distance_km[iquad], binsize_km = 25.) - rad_nm[wind_thresh_kts][quad] = 0. - if any(speed_kts_vs_radius_km >= wind_thresh_kts): - max_dist_of_wind_threshold_nm = np.max(radius_km[speed_kts_vs_radius_km >= wind_thresh_kts]) * km2nm - rad_nm[wind_thresh_kts][quad] = max_dist_of_wind_threshold_nm - if debug: - print('get_ext_of_wind():', "%3d "%wind_thresh_kts, quad, ' %3d-%3d'%(az,az+90), '%4d'%np.sum(iquad), - '%6.2fnm'%max_dist_of_wind_threshold_nm, end="") - print(radius_km) - print(speed_kts_vs_radius_km) - print() - else: - # I thought I wouldn't need (distance_km < rad_search_radius_km) because speed_kts was masked beyond - # the search radius. But it makes a difference. - iquad = (az <= bearing) & (bearing < az+90) & (speed_kts >= wind_thresh_kts) & (distance_km < rad_search_radius_km) - rad_nm[wind_thresh_kts][quad] = 0. - if np.sum(iquad) > 0: - x_km = distance_km[iquad] - if wind_radii_method[-10:] == "percentile": - # assume wind_radii_method is a number followed by the string "percentile". - distance_percentile = float(wind_radii_method[:-10]) - # index of array entry nearest to percentile value - idist_of_wind_threshold=abs(x_km-np.percentile(x_km,distance_percentile,interpolation='nearest')).argmin() - rad_nm[wind_thresh_kts][quad] = np.percentile(x_km, distance_percentile) * km2nm - elif wind_radii_method == "max": - idist_of_wind_threshold = np.argmax(x_km) - rad_nm[wind_thresh_kts][quad] = np.max(x_km) * km2nm - else: - print("unexpected wind_radii_method:" + wind_radii_method) - sys.exit(1) - if debug: - print('get_ext_of_wind():', "%3d "%wind_thresh_kts, quad, ' %3d-%3d'%(az,az+90), '%4d'%np.sum(iquad), - '%6.2fnm'%rad_nm[wind_thresh_kts][quad], '%4.0fdeg'%bearing[iquad][idist_of_wind_threshold], end="") - if lonCell is not None: - print('%8.2fN'%latCell[iquad][idist_of_wind_threshold], '%7.2fE'%lonCell[iquad][idist_of_wind_threshold], end="") - print() - return rad_nm - - -def derived_winds(u10, v10, mslp, lonCell, latCell, row, vmax_search_radius=250., mslp_search_radius=100., wind_radii_method="max", debug=False): - - # Given a row (with row.lon and row.lat)... - - # Derive cell distances and bearings - distance_km, bearing = dist_bearing(row.lon, lonCell, row.lat, latCell) - - # Derive 10m wind speed and Vt from u10 and v10 - speed_kts = np.sqrt(u10**2 + v10**2) * ms2kts - - # Tangential (cyclonic) wind speed - # v dx - u dy - dx = lonCell - row.lon - # work on the dateline? - dx[dx>=180] = dx[dx>=180]-360. - dy = latCell - row.lat - Vt = v10 * dx - u10 * dy - if row.lat < 0: - Vt = -Vt - - # Restrict Vmax search to a certain radius (vmax_search_radius) - vmaxrad = distance_km < vmax_search_radius - ispeed_max = np.argmax(speed_kts[vmaxrad]) - raw_vmax_kts = speed_kts[vmaxrad].max() - - # If vmax > 17, check if tangential component of max wind is negative (anti-cyclonic) - if row.vmax > 17 and Vt[vmaxrad][ispeed_max] < 0: - print("center", row.valid_time, row.lat, row.lon) - print("max wind is anti-cyclonic! (unknown units)", Vt[vmaxrad][ispeed_max]) - print("max wind lat/lon", latCell[vmaxrad][ispeed_max], lonCell[vmaxrad][ispeed_max]) - print("max wind U/V", u10[vmaxrad][ispeed_max], v10[vmaxrad][ispeed_max]) - if debug: pdb.set_trace() - - # Check if average tangential wind within search radius is negative (anti-cyclonic) - average_tangential_wind = np.average(Vt[vmaxrad]) - if average_tangential_wind < 0: - print("center", row.valid_time, row.lat, row.lon) - print("avg wind is anti-cyclonic!", average_tangential_wind) - if debug: pdb.set_trace() - - # Get radius of max wind - raw_RMW_nm = distance_km[vmaxrad][ispeed_max] * km2nm - if debug: - print('max wind lat', latCell[vmaxrad][ispeed_max], 'lon', lonCell[vmaxrad][ispeed_max]) - - # Restrict min mslp search - mslprad = distance_km < mslp_search_radius - raw_minp = mslp[mslprad].min() / 100. - - # Get max extent of wind at thresh_kts thresholds. - rad_nm = get_ext_of_wind(speed_kts, distance_km, bearing, raw_vmax_kts, latCell=latCell, lonCell=lonCell, wind_radii_method=wind_radii_method, debug=debug) - - return raw_vmax_kts, raw_RMW_nm, raw_minp, rad_nm - - -def add_wind_rad_lines(row, rad_nm, fullcircle=False, debug=False): - raw_vmax_kts = rad_nm['raw_vmax_kts'] - thresh_kts = rad_nm['thresh_kts'] - # if not empty...must be NEQ - if row.windcode.strip() and row.windcode != 'NEQ': - print('bad windcode', row.windcode, 'in', row) - print('expected NEQ') - sys.exit(1) - lines = pd.DataFrame() - for thresh in thresh_kts[thresh_kts < raw_vmax_kts]: - if any(rad_nm[thresh].values()): - newrow = row.copy() - # not sure how to do this with "rad" as part of the DataFrame index or Series name - if row.fhr > 110: - pdb.set_trace() - newrow.rename({34:thresh}, inplace=True) - newrow.rad = thresh - if fullcircle: - # Append row with full circle 34, 50, or 64 knot radius - # MET-TC will not derive this on its own - see email from John Halley-Gotway Oct 11, 2018 - # Probably shouldn't have AAA and NEQ in same file. - newrow[['windcode','rad1']] = ['AAA',np.nanmax(list(rad_nm[thresh].values()))] - newrow[['rad2','rad3','rad4']] = np.nan - else: - newrow['windcode'] = 'NEQ' - newrow[['rad1','rad2','rad3','rad4']] = [ - rad_nm[thresh]['NE'], - rad_nm[thresh]['SE'], - rad_nm[thresh]['SW'], - rad_nm[thresh]['NW'] - ] - # Append row with 34, 50, or 64 knot radii - lines = lines.append(newrow) - - return lines - -def update_row(row, raw_vmax_kts, raw_RMW_nm, raw_minp, rad_nm, debug=False): - - # TODO Call with origgridWRF, origgrid, and mpas.origmesh - - if debug: - print("atcf.update_row: before update\n", row[['valid_time','lon','lat', 'vmax', 'minp', 'rmw']]) - row["vmax"] = raw_vmax_kts - row["minp"] = raw_minp - row["rmw"] = raw_RMW_nm - # Add note of original mesh = True in user data (not defined) column - if 'origmeshTrue' not in row.userdata: - moreuserdata = 'origmeshTrue wind_radii_method '+ rad_nm["wind_radii_method"] - if debug: - print("appending "+moreuserdata+" to row.userdata") - row.userdata += moreuserdata - if debug: - print('after', row[['vmax', 'minp', 'rmw']]) - - # Return row if vmax < 34 kts - if rad_nm['raw_vmax_kts'] < 34: - return row - - # Make 34/50/64 knot rows - newrows = add_wind_rad_lines(row, rad_nm, debug=debug) - if debug: - print("changed", row, " to ", newrows) - return newrows - - - -def update_df(df, row, raw_vmax_kts, raw_RMW_nm, raw_minp, rad_nm, gridfile=None, debug=False): - - # TODO: Use update_row instead. add gridfile to update_row - # Called by origgrid and origmesh - - if debug: - print("atcf.update_df: before update_df\n", row[['valid_time','lon','lat', 'vmax', 'minp', 'rmw']]) - row["vmax"] = raw_vmax_kts - row["minp"] = raw_minp - row["rmw"] = raw_RMW_nm - # Add note of original mesh = True in user data (not defined) column - if 'origmeshTrue' not in row.userdata: - moreuserdata = 'origmeshTrue wind_radii_method '+ rad_nm["wind_radii_method"] - if gridfile is not None: - # Append origmesh file to userdata column (after a comma) - moreuserdata += ', ' + gridfile - if debug: - print("appending "+moreuserdata+" to row.userdata") - row.userdata += moreuserdata - if debug: - print('after', row[['vmax', 'minp', 'rmw']]) - - # hacky - can probably be cleaner. why the [0]? avoid (1, 44) with (44,) setting mismatch error - df.loc[row.name,:] = row - - # Append 34/50/64 knot lines to DataFrame - newlines = add_wind_rad_lines(row, rad_nm, debug=debug) - # If there are new lines, drop the old one and append new ones. - if not newlines.empty: - if debug: - print("dropping", row.name) - df.drop(row.name, inplace=True) - if debug: - print("appending ", newlines) - df = df.append(newlines, sort=False) - # Sort DataFrame by index (deal with appended wind radii lines) - # sort by rad too. I tried to avoid this, but when rad=0, it would be left behind other fhrs that had rad>0. - df = df.sort_index().sort_values(['initial_time','fhr','rad']) - - return df - - - - - -def write(ofile, df, fullcircle=False, debug=False): - if df.empty: - print("afcf.write(): DataFrame is empty.", ofile, "not written") - return - - # TODO: deal with fullcircle. - print("writing", ofile) - - if debug: - pdb.set_trace() - atcf_lines = "" - for index, row in df.iterrows(): - atcf_lines += "{:2s}, ".format(row.basin) - atcf_lines += "{:2s}, ".format(row.cy.zfill(2)) - atcf_lines += "{:8s}, ".format(row.initial_time.strftime('%Y%m%d%H')) - atcf_lines += "{}, ".format(row.technum) - atcf_lines += "{}, ".format(row.model) - atcf_lines += "{:3.0f}, ".format(row.fhr) - atcf_lines += "{:>4s}, ".format(lat2s(row.lat)) - atcf_lines += "{:>5s}, ".format(lon2s(row.lon)) - atcf_lines += "{:3.0f}, ".format(row.vmax) - atcf_lines += "{:4.0f}, ".format(row.minp) - atcf_lines += "{}, ".format(row.ty) - atcf_lines += "{:3.0f}, ".format(row.rad) - atcf_lines += "{:>3s}, ".format(row.windcode) - atcf_lines += "{:4.0f}, ".format(row.rad1) - atcf_lines += "{:4.0f}, ".format(row.rad2) - atcf_lines += "{:4.0f}, ".format(row.rad3) - atcf_lines += "{:4.0f}, ".format(row.rad4) - atcf_lines += "{:4.0f}, ".format(row.pouter) - atcf_lines += "{:4.0f}, ".format(row.router) - atcf_lines += "{:3.0f}, ".format(row.rmw) - atcf_lines += "{:3.0f}, ".format(row.gusts) - atcf_lines += "{:3.0f}, ".format(row.eye) - atcf_lines += "{:>3s}, ".format(row.subregion) # supposedly 1 character, but always 3 in official b-decks - atcf_lines += "{:3.0f}, ".format(row.maxseas) - atcf_lines += "{:>3s}, ".format(row.initials) - atcf_lines += "{:3.0f}, ".format(row.dir) - atcf_lines += "{:3.0f}, ".format(row.speed) - atcf_lines += "{:>10s}, ".format(row.stormname) - atcf_lines += "{:>1s}, ".format(row.depth) - atcf_lines += "{:2.0f}, ".format(row.seas) - atcf_lines += "{:>3s}, ".format(row.seascode) - atcf_lines += "{:4.0f}, ".format(row.seas1) - atcf_lines += "{:4.0f}, ".format(row.seas2) - atcf_lines += "{:4.0f}, ".format(row.seas3) - atcf_lines += "{:4.0f}, ".format(row.seas4) - atcf_lines += "{:>20s}, ".format(row.userdefined) # Described as 20 chars in atcf doc. - atcf_lines += "{}, ".format(row.userdata) - atcf_lines += "\n" - - atcf_lines = atcf_lines.replace("nan"," ") - - if False: - - # I couldn't get this section to work. - # The pandas to_string method inexplicibly padded some columns with an extra space that had to be removed later. - - # Valid time is not part of ATCF file. - del(df["valid_time"]) - - # Had to add parentheses () to .len to not get error about instancemethod not being iterable. - if max(df.cy.str.len()) > 2: - print('cy more than 2 characters. Truncating...') - # Append full CY to userdata column (after a comma) - df['userdata'] = df['userdata'] + ', ' + df['cy'] - # Keep first 2 characters - df['cy'] = df['cy'].str.slice(0,2) - formatters={ - "basin": '{},'.format, - # The problem with CY is ATCF only reserves 2 characters for it. - "cy": lambda x: x.zfill(2)+"," , # not always an integer (e.g. 10E) # 20181116 force to be integer - # Convert initial_time from datetime to string. - "initial_time":lambda x: x.strftime('%Y%m%d%H,'), - "technum":'{},'.format, - "model":'{},'.format, - "fhr":'{:3.0f},'.format, - "lat":lat2s, - "lon":lon2s, - "vmax":'{:3.0f},'.format, - "minp":'{:4.0f},'.format, - "ty":'{},'.format, - "windcode":'{:>3s},'.format, - "rad":'{:3.0f},'.format, - "rad1":'{:4.0f},'.format, - "rad2":'{:4.0f},'.format, - "rad3":'{:4.0f},'.format, - "rad4":'{:4.0f},'.format, - "pouter":'{:4.0f},'.format, - "router":'{:4.0f},'.format, - "rmw":'{:3.0f},'.format, - "gusts":'{:3.0f},'.format, - "eye":'{:3.0f},'.format, - "subregion":'{:>2s},'.format, - "maxseas":'{:3.0f},'.format, - "initials":'{:>3s},'.format, - "dir":'{:3.0f},'.format, - "speed":'{:3.0f},'.format, - "stormname":'{:>9s},'.format, - "depth":'{:>1s},'.format, - "seas":'{:2.0f},'.format, - "seascode":'{:>3s},'.format, - "seas1":'{:4.0f},'.format, - "seas2":'{:4.0f},'.format, - "seas3":'{:4.0f},'.format, - "seas4":'{:4.0f},'.format, - "userdefined":'{:>18s},'.format, - "userdata":'{},'.format, - "cpsB":'{:4.0f},'.format, - "cpsll":'{:4.0f},'.format, - "cpsul":'{:4.0f},'.format, - "warmcore":'{},'.format, - "direction":'{},'.format, - } - junk = df.to_string(header=False, index=False, na_rep=' ', formatters=formatters, columns=atcfcolumns) - - - - # TODO: FIX IDL-STYLE COLUMNS. STORMNAME IS MISSING A LETTER - - # na_rep=' ' has no effect - # strings have extra space in front of them - junk = junk.split('\n') - # replace first 3 occurrences of ', ' with ', '. - # replace ', XX,' with ', XX,' - # replace 'nan' with ' ' - junk = [j.replace(', ', ', ', 3).replace(', XX,',', XX,').replace('nan',' ') for j in junk] - #delete space before windcode (e.g. NEQ) - junk = [j[:68]+j[69:] for j in junk] - #delete space before initials - junk = [j[:133]+j[134:] for j in junk] - #delete space before depth - junk = [j[:161]+j[162:] for j in junk] - #delete space before seascode - junk = [j[:168]+j[169:] for j in junk] - junk = '\n'.join(junk) - - if debug: - pdb.set_trace() - - f = open(ofile, "w") - f.write(atcf_lines) - - f.close() - print("wrote", ofile) - -def origgridWRF(df, griddir, grid="d03", wind_radii_method = "max", debug=False): - # Get vmax, minp, radius of max wind, max radii of wind thresholds from WRF by Alex Kowaleski - - WRFmember = df.model.str.extract(r'WF(\d\d)', flags=re.IGNORECASE) - # column 0 will have match or null - if pd.isnull(WRFmember[0]).any(): - if debug: - print('Assuming WRF ensemble member, but not all model strings match WF\d\d') - print(df) - sys.exit(1) - ens = int(WRFmember[0][0]) # strip leading zero - if ens < 1: - sys.exit(2) - for index, row in df.iterrows(): - gridfile = "EPS_"+str(ens)+"/"+ "E"+str(ens)+"_"+row.initial_time.strftime('%m%d%H') + \ - "_"+grid+"_"+ row.valid_time.strftime('%Y-%m-%d_%H:%M:%S') +"_ll.nc" - print('opening ' + griddir + gridfile) - nc = Dataset(griddir + gridfile, "r") - lon = nc.variables['lon'][:] - lat = nc.variables['lat'][:] - lonCell,latCell = np.meshgrid(lon, lat) - iTime = 0 - u10 = nc.variables['u10'][iTime,:,:] - v10 = nc.variables['v10'][iTime,:,:] - mslpvar = nc.variables['slp'] - if mslpvar.units != 'hPa': - print("atcf.origgridWRF: unexpected units for mslp: "+mslpvar.units) - sys.exit(1) - mslp = mslpvar[iTime,:,:] * 100. - print('closing ' + griddir + gridfile) - nc.close() - - if debug: - print("Extract vmax, RMW, minp, and radii of wind thresholds from row", row.name) - raw_vmax_kts, raw_RMW_nm, raw_minp, rad_nm = derived_winds(u10, v10, mslp, lonCell, latCell, row, wind_radii_method=wind_radii_method, debug=debug) - df = update_df(df, row, raw_vmax_kts, raw_RMW_nm, raw_minp, rad_nm, debug=debug) - - # Sort DataFrame by index (deal with appended wind radii lines) - # sort by rad too - df = df.sort_index().sort_values(['initial_time','fhr','rad']) - return df - -def get_var_with_str(nc, s): - matching_vars = [v for v in nc.variables if s.lower() in v.lower()] # .lower() makes it case-insensitive - if len(matching_vars) != 1: - print("number of matching variables not 1") - print(nc.variables, s, matching_vars) - sys.exit(1) - return matching_vars[0] - - -def origgrid(df, griddir, ensemble_prefix="ens_", wind_radii_method="max", debug=False): - # Get vmax, minp, radius of max wind, max radii of wind thresholds from ECMWF grid, not from tracker. - # Assumes - # ECMWF data came from TIGGE and were converted from GRIB to netCDF with ncl_convert2nc. - # 4-character model string in ATCF file is "EExx" (where xx is the 2-digit ensemble member). - # ECMWF ensemble member in directory named "ens_xx" (where xx is the 2-digit ensemble member). - # File path is "ens_xx/${gs}yyyymmddhh.xx.nc", where ${gs} is the grid spacing (0p15, 0p25, or 0p5). - # ensemble_prefix may be a single string or a list of strings - - if isinstance(ensemble_prefix, str): - ensemble_prefixes = [ensemble_prefix] - elif isinstance(ensemble_prefix, (list, tuple)): - ensemble_prefixes = ensemble_prefix - - # TODO: Why group rows by initial_time and model? Why not process each row independently? - for run_id, group in df.groupby(['initial_time', 'model']): - initial_time, model = run_id - m = re.search(r'EE(\d\d)', model) - if not m: - if debug: - print('Assuming ECMWF ensemble member, but did not find EE\d\d in model string') - print('no original grid for',model,'- skipping') - continue - ens = int(m.group(1)) # strip leading zero - - # used to skip EE00 because I didn't know how to handle control run. Now it is handled. - #if ens < 1: - # continue - - # Allow some naming conventions - # ens_n/yyyymmddhh.n.nc - # ens_n/0p15yyyymmddhh_sfc.nc - # ens_n/0p25yyyymmddhh_sfc.nc - # ens_n/0p5yyyymmddhh_sfc.nc - yyyymmddhh = initial_time.strftime('%Y%m%d%H') - yyyymmdd_hhmm = initial_time.strftime('%Y%m%d_%H%M') - potential_gridfiles = [] - for ensemble_prefix in ensemble_prefixes: - # If first filename doesn't exist, try the next one, and so on... - # List in order of most preferred to least preferred. - potential_gridfiles.extend([ - ensemble_prefix+str(ens)+"/SFC_"+yyyymmdd_hhmm+".nc", # Linus-style - ensemble_prefix+str(ens)+"/"+ "0p15"+yyyymmddhh+"."+str(ens)+".nc", - ensemble_prefix+str(ens)+"/"+ "0p25"+yyyymmddhh+"."+str(ens)+".nc", - ensemble_prefix+str(ens)+"/"+ "0p5"+yyyymmddhh+"."+str(ens)+".nc", - ensemble_prefix+str(ens)+"/"+ yyyymmddhh+"."+str(ens)+".nc" - ]) - for gridfile in potential_gridfiles: - if os.path.isfile(griddir + gridfile): - break - else: - print("no", griddir + gridfile) - - print('opening', gridfile) - nc = Dataset(griddir + gridfile, "r") - lon = nc.variables[get_var_with_str(nc, 'lon_')][:] - lat = nc.variables[get_var_with_str(nc, 'lat_')][:] - lonCell,latCell = np.meshgrid(lon, lat) - u10s = nc.variables[get_var_with_str(nc, '10u')][:] - v10s = nc.variables[get_var_with_str(nc, '10v')][:] - mslps = nc.variables[get_var_with_str(nc, 'msl')][:] - model_forecast_times = nc.variables['forecast_time0'][:] - nc.close() - for index, row in group.iterrows(): - if not any(model_forecast_times == row.fhr): - print(row.fhr, 'not in model file') - continue - itime = np.argmax(model_forecast_times == row.fhr) - u10 = u10s[itime,:,:] - v10 = v10s[itime,:,:] - mslp = mslps[itime,:,:] - - # Extract vmax, RMW, minp, and radii of wind thresholds - raw_vmax_kts, raw_RMW_nm, raw_minp, rad_nm = derived_winds(u10, v10, mslp, lonCell, latCell, row, wind_radii_method=wind_radii_method, debug=debug) - - - df = update_df(df, row, raw_vmax_kts, raw_RMW_nm, raw_minp, rad_nm, gridfile=gridfile, debug=debug) - - return df - -if __name__ == "__main__": - read() diff --git a/cache.py b/cache.py deleted file mode 100644 index 8fcf226..0000000 --- a/cache.py +++ /dev/null @@ -1,80 +0,0 @@ -# Copied from http://code.activestate.com/recipes/491261-caching-and-throttling-for-urllib2/ May 16, 2017 -import http.client -import unittest -import hashlib -import urllib.request, urllib.error, urllib.parse -import os -import io -__version__ = (0,1) -__author__ = "Staffan Malmgren " - - -class CacheHandler(urllib.request.BaseHandler): - """Stores responses in a persistant on-disk cache. - - If a subsequent GET request is made for the same URL, the stored - response is returned, saving time, resources and bandwith""" - def __init__(self,cacheLocation): - """The location of the cache directory""" - self.cacheLocation = cacheLocation - if not os.path.exists(self.cacheLocation): - os.mkdir(self.cacheLocation) - - def default_open(self,request): - if ((request.get_method() == "GET") and - (CachedResponse.ExistsInCache(self.cacheLocation, request.get_full_url()))): - # print "CacheHandler: Returning CACHED response for %s" % request.get_full_url() - return CachedResponse(self.cacheLocation, request.get_full_url(), setCacheHeader=True) - else: - return urllib.request.urlopen(request.get_full_url()) - - def http_response(self, request, response): - if request.get_method() == "GET": - if 'd-cache' not in response.info(): - CachedResponse.StoreInCache(self.cacheLocation, request.get_full_url(), response) - return CachedResponse(self.cacheLocation, request.get_full_url(), setCacheHeader=False) - else: - return CachedResponse(self.cacheLocation, request.get_full_url(), setCacheHeader=True) - else: - return response - -class CachedResponse(io.StringIO): - """An urllib2.response-like object for cached responses. - - To determine wheter a response is cached or coming directly from - the network, check the x-cache header rather than the object type.""" - - def ExistsInCache(cacheLocation, url): - hash = hashlib.md5(url).hexdigest() - return (os.path.exists(cacheLocation + "/" + hash + ".headers") and - os.path.exists(cacheLocation + "/" + hash + ".body")) - ExistsInCache = staticmethod(ExistsInCache) - - def StoreInCache(cacheLocation, url, response): - hash = hashlib.md5(url).hexdigest() - f = open(cacheLocation + "/" + hash + ".headers", "w") - headers = str(response.info()) - f.write(headers) - f.close() - f = open(cacheLocation + "/" + hash + ".body", "w") - f.write(response.read()) - f.close() - StoreInCache = staticmethod(StoreInCache) - - def __init__(self, cacheLocation,url,setCacheHeader=True): - self.cacheLocation = cacheLocation - hash = hashlib.md5(url).hexdigest() - io.StringIO.__init__(self, file(self.cacheLocation + "/" + hash+".body").read()) - self.url = url - self.code = 200 - self.msg = "OK" - headerbuf = file(self.cacheLocation + "/" + hash+".headers").read() - if setCacheHeader: - headerbuf += "d-cache: %s/%s\r\n" % (self.cacheLocation,hash) - self.headers = http.client.HTTPMessage(io.StringIO(headerbuf)) - - def info(self): - return self.headers - def geturl(self): - return self.url - diff --git a/get_model_time.py b/get_model_time.py deleted file mode 100644 index 9cf4e70..0000000 --- a/get_model_time.py +++ /dev/null @@ -1,63 +0,0 @@ -from netCDF4 import Dataset -import datetime -import pytz -import pdb -import re -import sys -import os - -def valid(ncfilename, diagnostic_name): - - nc = Dataset(ncfilename,"r") - try: - x = nc.variables[diagnostic_name] - except KeyError: - print(diagnostic_name, "not found. Choices:", list(nc.variables.keys())) - sys.exit(1) - global_atts = nc.ncattrs() - if 'valid_date' in global_atts: - valid_time = nc.valid_date - # convert unicode to datetime object - valid_time = datetime.datetime.strptime(valid_time, '%Y-%m-%d_%H:%M:%S') - elif 'initial_time' in x.ncattrs(): - initialization_time = datetime.datetime.strptime(x.initial_time, '%m/%d/%Y (%H:%M)') - if x.forecast_time_units == 'hours': - td = datetime.timedelta(0,0,0,0,0,1) - if x.forecast_time_units == '15 minutes': - td = datetime.timedelta(0,0,0,0,15,0) - if x.forecast_time_units == '30 minutes': - td = datetime.timedelta(0,0,0,0,30,0) - - forecast_lead_time = x.forecast_time * td - valid_time = initialization_time + forecast_lead_time - elif 'valid_time' in x.ncattrs(): - valid_time = datetime.datetime.strptime(x.valid_time, '%Y%m%d_%H%M%S') - initialization_time = datetime.datetime.strptime(x.init_time, '%Y%m%d_%H%M%S') - elif 'START_DATE' in global_atts: - # Like ds300 NCAR WRF ensemble diags files - initialization_time = datetime.datetime.strptime(nc.START_DATE, '%Y-%m-%d_%H:%M:%S') - basename = os.path.basename(ncfilename) - # start with "diag", then anything, then _d[0-9][0-9], then _yyyymmddhh, then ... - m = re.search('diag.*_d\d\d_201\d{7}.*_f(\d\d\d).nc', basename) - if m: - fhr_str = m.group(1) - fhr = int(fhr_str) - forecast_lead_time = datetime.timedelta(hours=fhr) - valid_time = initialization_time + forecast_lead_time - else: - print("Could not get valid time from "+basename+". Using init time instead") - valid_time = initialization_time - else: - print("don't know how to get time from", ncfilename) - print(x) - pdb.set_trace() - - nc.close() - - # return time_zone_aware datetimes - return pytz.utc.localize(valid_time), pytz.utc.localize(initialization_time) - -def init(ncfilename, diagnostic_name): - valid_time, init_time = valid(ncfilename, diagnostic_name) - return init_time - diff --git a/mysavfig.py b/mysavfig.py deleted file mode 100644 index 5ca91de..0000000 --- a/mysavfig.py +++ /dev/null @@ -1,18 +0,0 @@ -import matplotlib.pyplot as plt -from subprocess import call # to use "mogrify" -import datetime as dt -def mysavfig(ofile, string="", timestamp=True, size=5, debug=False, **kwargs): - if debug: - print('mysavfig: timestamp=',timestamp) - if timestamp: - string = string + '\n' + 'created '+str(dt.datetime.now(tz=None)).split('.')[0] # + '\n' # extra newline to keep timestamp onscreen. - th = plt.annotate(string, xy=(2,1), xycoords='figure pixels', horizontalalignment='left', verticalalignment='bottom', size=size) - #plt.tight_layout() # Tried this but it screwed up SHARPY_skewt. Also tried bbox_inches='tight' below but not in that version of matplotlib. - # Tried this but it made a large blank space on the left side of SHARPY_skewt. - # plt.savefig(ofile,dpi=dpi,bbox_inches='tight') - plt.savefig(ofile, **kwargs) - th.remove() # permits the figure to reused without overlaying multiple "created . . " dates. - cmd = "mogrify +matte -type Palette -colors 255 " + ofile # prevents flickering when displaying on yellowstone. - print("created", ofile) - return call(cmd.split()) - diff --git a/myskewt.py b/myskewt.py deleted file mode 100755 index b8e9a1b..0000000 --- a/myskewt.py +++ /dev/null @@ -1,303 +0,0 @@ -import sys -import pdb -#sys.path.append('/glade/u/home/ahijevyc/lib/python2.7/site-packages/SHARPpy-1.3.0-py2.7.egg') -#sys.path.append('/glade/u/home/ahijevyc/lib/python2.7/site-packages') -import sharppy -import sharppy.sharptab.interp as interp -import sharppy.sharptab.params as params -import sharppy.sharptab.profile as profile -import sharppy.sharptab.thermo as thermo -import sharppy.sharptab.utils as utils -import sharppy.sharptab.winds as winds - -import numpy as np -from io import StringIO -import matplotlib.pyplot as plt -import cartopy - - -def parseCLS(sfile): - ## read in the file - data = np.array([l.strip() for l in sfile.split('\n')]) - - latitude = float(data[3].split(',')[5]) - longitude = float(data[3].split(',')[4]) - ## necessary index points - start_idx = 15 - finish_idx = len(data) - - ## put it all together for StringIO - full_data = '\n'.join(data[start_idx : finish_idx][:]) - sound_data = StringIO( full_data ) - - ## read the data into arrays - data = np.genfromtxt( sound_data ) - clean_data = [] - for i in data: - if i[1] != 9999 and i[2] != 999 and i[3] != 999 and i[7] != 999 and i[8] != 999 and i[14] != 99999: - clean_data.append(i) - p = np.array([i[1] for i in clean_data]) - h = np.array([i[14] for i in clean_data]) - T = np.array([i[2] for i in clean_data]) - Td = np.array([i[3] for i in clean_data]) - wdir = np.array([i[8] for i in clean_data]) - wspd = np.array([i[7] for i in clean_data]) - wspd = utils.MS2KTS(wspd) - wdir[wdir == 360] = 0. # Some wind directions are 360. Like in /glade/p/work/ahijevyc/GFS/Joaquin/g132325165.frd - - max_points = 250 - s = p.size/max_points - if s == 0: s = 1 - print("stride=",s) - return p[::s], h[::s], T[::s], Td[::s], wdir[::s], wspd[::s], latitude, longitude - -def thetas(tempC, presvals): - return ((tempC + thermo.ZEROCNK) / (np.power((1000. / presvals),thermo.ROCP))) - thermo.ZEROCNK - - - -def dewpoint_approximation(T,RH): - # approximation valid for - # 0 degC < T < 60 degC - # 1% < RH < 100% - # 0 degC < Td < 50 degC - # constants - a = 17.271 - b = 237.7 # degC - - Td = (b * gamma(T,RH)) / (a - gamma(T,RH)) - - return Td - - -def gamma(T,RH): - # constants - a = 17.271 - b = 237.7 # degC - - g = (a * T / (b + T)) + np.log(RH/100.0) - - return g - -def draw_background(ax, dry=np.arange(-50,110,20), moist=np.arange(-5,40,5), presvals=np.arange(1050, 0, -10), mix=[1,2,4,8,12,16,20,24]): - - # Plot dry adiabats - for t in dry: - ax.semilogy(thetas(t, presvals), presvals,'r-', alpha=0.2, linewidth=1) - - # Plot moist adiabats to topp - topp = 200 - moist_presvals = np.arange(np.max(presvals), topp, -10) - for t in moist: - tw = [] - for p in moist_presvals: - tw.append(thermo.wetlift(1000., t, p)) - ax.semilogy(tw, moist_presvals, 'k-', alpha=0.2, linewidth=0.5, linestyle='dashed') - # add moist adiabat text labels - ax.text(thermo.wetlift(1000., t, topp+15), topp+15, t, va='center', ha='center', size=5.6, alpha=0.3, clip_on=True) - - - # Mixing ratio lines and labels to topp - - topp = 650 - for w in mix: - ww = [] - for p in presvals: - ww.append(thermo.temp_at_mixrat(w,p)) - ax.semilogy(ww, presvals, 'g', alpha=0.35, linewidth=1, linestyle="dotted") - ax.text(thermo.temp_at_mixrat(w, topp), topp, w, va='bottom', ha='center', size=6.7, color='g', alpha=0.35) - - # Disables the log-formatting (10 to the power of x) that comes with semilogy - ax.yaxis.set_major_formatter(plt.ScalarFormatter()) - ax.set_yticks(np.linspace(100,1000,10)) - ax.set_ylim(1050,100) - plt.ylabel('Pressure (hPa)') - - # The first time this axis object is returned, no xtick gridlines show up for -110 to -60C - ax.xaxis.set_major_locator(plt.MultipleLocator(10)) - ax.set_xlim(-50,45) - plt.xlabel('Temperature (C)') - - ax.grid(True, linestyle='solid', alpha=0.5) - - -def draw_hodo(): - bbox_props = dict(boxstyle="square", color="w", alpha=0.5, lw=0.5) - ax = plt.axes([.5,.63,.2,.26]) - ax.get_xaxis().set_visible(False) - ax.get_yaxis().set_visible(False) - az = np.radians(-35) - for i in range(10,100,10): - # Draw the range rings around the hodograph. - lw = .4 if i % 20 == 0 else 0.25 - circle = plt.Circle((0,0),i,color='k', alpha=.3, fill=False, lw=lw) - ax.add_artist(circle) - if i % 20 == 0 and i < 100: - # The minus 2 nudges it a little closer to origin. - plt.text((i-2)*np.cos(az),(i-2)*np.sin(az),str(i)+" kt",rotation=np.degrees(az),size=5,alpha=.4, - ha='center', zorder=1, bbox=bbox_props) - - ax.set_xlim(-40,80) - ax.set_ylim(-60,60) - ax.axhline(y=0, color='k') - ax.axvline(x=0, color='k') - - return ax - - -def add_hodo(ax, prof, lw=1, color='black', ls='solid', size=5, AGLs=[1,2,6,10]): - # Return 2D hodograph line and list of text AGL labels. - - - # Draw hodo line - u_prof = prof.u[prof.pres >= 100] - v_prof = prof.v[prof.pres >= 100] - hodo, = ax.plot(u_prof,v_prof, 'k-', lw=lw, color=color, ls=ls) - - # position AGL text labels - bbox_props = dict(boxstyle="square", fc="w", ec="0.5", linewidth=0.5, alpha=0.7) - AGL_labels = [] - AGL_labels.append(ax.text(-35,-55,'km AGL',size=size,bbox=bbox_props)) - for a in AGLs: - in_meters = a*1000 - if in_meters <= np.max(interp.to_agl(prof, prof.hght)): - junk = ax.text(0,0,str(a),ha='center',va='center',size=size,bbox=bbox_props,color=color) - ind = np.min(np.where(interp.to_agl(prof,prof.hght)>in_meters)) - junk.set_position((prof.u[ind],prof.v[ind])) - junk.set_clip_on(True) - AGL_labels.append(junk) - return hodo, AGL_labels - - -def wind_barb_spaced(ax, prof, xpos=1.0, yspace=0.04): - # yspace = 0.04 means ~26 barbs. - s = [] - bot = 2000. - # Space out wind barbs evenly on log axis. - for ind, i in enumerate(prof.pres): - if i < 100: break - if np.log(bot/i) > yspace: - s.append(ind) - bot = i - # x coordinate in (0-1); y coordinate in pressure log(p) - b = plt.barbs(xpos*np.ones(len(prof.pres[s])), prof.pres[s], prof.u[s], prof.v[s], - length=5.8, lw=0.35, clip_on=False, transform=ax.get_yaxis_transform()) - - # 'knots' label under wind barb stack - kts = ax.text(1.0, prof.pres[0]+10, 'knots', clip_on=False, transform=ax.get_yaxis_transform(),ha='center',va='top',size=7) - return b, kts - - -def add_globe(longitude, latitude): - # TODO: avoid matplotlib depreciation warning about creating a unique id for each axes instance. You need a new axes - # instance whenever lat/lon changes. - # Globe with dot on location. - mapax = plt.axes([.795, 0.09,.18,.18], projection=cartopy.crs.Orthographic(longitude, latitude)) - mapax.add_feature(cartopy.feature.OCEAN, zorder=0) - mapax.add_feature(cartopy.feature.LAND, zorder=0, linewidth=0) # linewidth=0 or coastlines are fuzzy - mapax.set_global() - sloc = mapax.plot(longitude, latitude,'o', color='green', markeredgewidth=0, markersize=4., transform=cartopy.crs.Geodetic()) - return mapax - - -def indices(prof, debug=False): - - # return a formatted-string list of stability and kinematic indices - - sfcpcl = params.parcelx(prof, flag=1) - mupcl = params.parcelx(prof, flag=3) # most unstable - mlpcl = params.parcelx(prof, flag=4) # 100 mb mean layer parcel - - - pcl = mupcl - sfc = prof.pres[prof.sfc] - p3km = interp.pres(prof, interp.to_msl(prof, 3000.)) - p6km = interp.pres(prof, interp.to_msl(prof, 6000.)) - p1km = interp.pres(prof, interp.to_msl(prof, 1000.)) - mean_3km = winds.mean_wind(prof, pbot=sfc, ptop=p3km) - sfc_6km_shear = winds.wind_shear(prof, pbot=sfc, ptop=p6km) - sfc_3km_shear = winds.wind_shear(prof, pbot=sfc, ptop=p3km) - sfc_1km_shear = winds.wind_shear(prof, pbot=sfc, ptop=p1km) - #print "0-3 km Pressure-Weighted Mean Wind (kt):", utils.comp2vec(mean_3km[0], mean_3km[1])[1] - #print "0-6 km Shear (kt):", utils.comp2vec(sfc_6km_shear[0], sfc_6km_shear[1])[1] - srwind = params.bunkers_storm_motion(prof) - srh3km = winds.helicity(prof, 0, 3000., stu = srwind[0], stv = srwind[1]) - srh1km = winds.helicity(prof, 0, 1000., stu = srwind[0], stv = srwind[1]) - #print "0-3 km Storm Relative Helicity [m2/s2]:",srh3km[0] - - #### Calculating variables based off of the effective inflow layer: - - # The effective inflow layer concept is used to obtain the layer of buoyant parcels that feed a storm's inflow. - # Here are a few examples of how to compute variables that require the effective inflow layer in order to calculate them: - - stp_fixed = params.stp_fixed(sfcpcl.bplus, sfcpcl.lclhght, srh1km[0], utils.comp2vec(sfc_6km_shear[0], sfc_6km_shear[1])[1]) - ship = params.ship(prof) - - # If you get an error about not converting masked constant to python int - # use the round() function instead of int() - Ahijevych May 11 2016 - # 2nd element of list is the # of decimal places - indices = {'SBCAPE': [sfcpcl.bplus, 0, 'J $\mathregular{kg^{-1}}$'], - 'SBCIN': [sfcpcl.bminus, 0, 'J $\mathregular{kg^{-1}}$'], - 'SBLCL': [sfcpcl.lclhght, 0, 'm AGL'], - 'SBLFC': [sfcpcl.lfchght, 0, 'm AGL'], - 'SBEL': [sfcpcl.elhght, 0, 'm AGL'], - 'SBLI': [sfcpcl.li5, 0, 'C'], - 'MLCAPE': [mlpcl.bplus, 0, 'J $\mathregular{kg^{-1}}$'], - 'MLCIN': [mlpcl.bminus, 0, 'J $\mathregular{kg^{-1}}$'], - 'MLLCL': [mlpcl.lclhght, 0, 'm AGL'], - 'MLLFC': [mlpcl.lfchght, 0, 'm AGL'], - 'MLEL': [mlpcl.elhght, 0, 'm AGL'], - 'MLLI': [mlpcl.li5, 0, 'C'], - 'MUCAPE': [mupcl.bplus, 0, 'J $\mathregular{kg^{-1}}$'], - 'MUCIN': [mupcl.bminus, 0, 'J $\mathregular{kg^{-1}}$'], - 'MULCL': [mupcl.lclhght, 0, 'm AGL'], - 'MULFC': [mupcl.lfchght, 0, 'm AGL'], - 'MUEL': [mupcl.elhght, 0, 'm AGL'], - 'MULI': [mupcl.li5, 0, 'C'], - '0-1 km SRH': [srh1km[0], 0, '$\mathregular{m^{2}s^{-2}}$'], - '0-1 km Shear': [utils.comp2vec(sfc_1km_shear[0], sfc_1km_shear[1])[1], 0, 'kt'], - '0-3 km SRH': [srh3km[0], 0, '$\mathregular{m^{2}s^{-2}}$'], - '0-6 km Shear': [utils.comp2vec(sfc_6km_shear[0], sfc_6km_shear[1])[1], 0, 'kt'], - 'PWV': [params.precip_water(prof), 2, 'inch'], - 'K-index': [params.k_index(prof), 0, ''], - 'STP(fix)': [stp_fixed, 1, ''], - 'SHIP': [ship, 1, '']} - - - - eff_inflow = params.effective_inflow_layer(prof) - if any(eff_inflow): - ebot_hght = interp.to_agl(prof, interp.hght(prof, eff_inflow[0])) - etop_hght = interp.to_agl(prof, interp.hght(prof, eff_inflow[1])) - #print "Effective Inflow Layer Bottom Height (m AGL):", ebot_hght - #print "Effective Inflow Layer Top Height (m AGL):", etop_hght - effective_srh = winds.helicity(prof, ebot_hght, etop_hght, stu = srwind[0], stv = srwind[1]) - indices['Eff. SRH'] = [effective_srh[0], 0, '$\mathregular{m^{2}s^{-2}}$'] - #print "Effective Inflow Layer SRH (m2/s2):", effective_srh[0] - ebwd = winds.wind_shear(prof, pbot=eff_inflow[0], ptop=eff_inflow[1]) - ebwspd = utils.mag( *ebwd ) - indices['EBWD'] = [ebwspd,0, 'kt'] - #print "Effective Bulk Wind Difference:", ebwspd - scp = params.scp(mupcl.bplus, effective_srh[0], ebwspd) - indices['SCP'] = [scp, 1, ''] - stp_cin = params.stp_cin(mlpcl.bplus, effective_srh[0], ebwspd, mlpcl.lclhght, mlpcl.bminus) - indices['STP(cin)'] = [stp_cin, 1, ''] - #print "Supercell Composite Parameter:", scp - #print "Significant Tornado Parameter (w/CIN):", stp_cin - #print "Significant Tornado Parameter (fixed):", stp_fixed - - # Update the indices within the indices dictionary on the side of the plot. - string = '' - for index, value in sorted(indices.items()): - if np.ma.is_masked(value[0]): - if debug: - print("skipping masked value for index=",index) - continue - if debug: - print("index=",index) - print("value=",value) - format = '%.'+str(value[1])+'f' - string += index + ": " + format % value[0] + " " + value[2] + '\n' - - return string - diff --git a/spc.py b/spc.py deleted file mode 100644 index 97e044b..0000000 --- a/spc.py +++ /dev/null @@ -1,359 +0,0 @@ -import pandas as pd -import pytz -import glob -import sys # for stderr output -import pdb -import numpy as np -import datetime -import os # for basename -import matplotlib.path as mpath -import cartopy -import sqlite3 - -def spc_event_filename(event_type): - name = '/glade/work/ahijevyc/share/SPC/'+event_type+'/' - if event_type=='torn': - return "/glade/work/ahijevyc/share/SPC/1950-2017_actual_tornadoes.csv" - name = name + "1955-2017_"+event_type+".csv" - return name - - - -def get_storm_reports( - start = datetime.datetime(2016,6,10,0,0,0,0,pytz.UTC), - end = datetime.datetime(2016,7,1,0,0,0,0,pytz.UTC), - event_types = ["torn", "wind", "hail"], - debug = False - ): - - if debug: - print("storm_reports: start:",start) - print("storm_reports: end:",end) - print("storm_reports: event types:",event_types) - - # Create one DataFrame to hold all event types. - all_rpts = pd.DataFrame() - for event_type in event_types: - rpts_file = spc_event_filename(event_type) - - # csv format described in http://www.spc.noaa.gov/wcm/data/SPC_severe_database_description.pdf - # SPC storm report files downloaded from http://www.spc.noaa.gov/wcm/#data to - # cheyenne:/glade/work/ahijevyc/share/ Mar 2019. - # Multi-year zip files have headers; pre-2016 single-year csv files have no header. - - dtype = { - "om": np.int64, - "yr": np.int32, - "mo": np.int32, - "dy": np.int32, - "date": str, - "tz": np.int32, - "st": str, - "stf": np.int64, # State FIPS number. some Puerto Rico codes are incorrect - "stn": np.int64, # State number - number of this tornado in this state in this year - "mag": np.float64, # you might think there is "sz" for hail and "f" for torn, but all "mag" - "inj": np.int64, - "fat": np.int64, - "loss": np.float64, - "closs": np.float64, - "slat": np.float64, - "slon": np.float64, - "elat": np.float64, - "elon": np.float64, - "len": np.float64, - "wid": np.float64, - "ns": np.int64, - "sn": np.int64, - "sg": np.int64, - "f1": np.int64, - "f2": np.int64, - "f3": np.int64, - "f4": np.int64, - "mt": str, - "fc": np.int64, - } - rpts = pd.read_csv(rpts_file, parse_dates=[['date','time']], dtype=dtype, infer_datetime_format=True) - if debug: - print("input file:",rpts_file) - print("read",len(rpts),"lines") - pdb.set_trace() - rpts["event_type"] = event_type - rpts["source"] = os.path.basename(rpts_file) - - # -9 = unknown tornado F-scale - # Change -9 to NaN - rpts["mag"].replace(to_replace=-9, value = np.nan, inplace=True) - - # Augment event type for large hail and high wind. - if event_type == "hail": - largehail = rpts.mag >= 2. - if any(largehail): - rpts.loc[largehail,"event_type"] = "large hail" - if event_type == "wind": - highwind = rpts.mag >= 65. - if any(highwind): - rpts.loc[highwind, "event_type"] = "high wind" - - # Fix tz=6. This is MDT in the NECI Storm Events database. - # This is confusing. A time in MDT is the same as the time in CST. - # So simply change tz to 3 (CST). - MDT = rpts['tz'] == 6 - if any(MDT): - MDT_timezones = rpts[['om','date_time','tz','event_type', 'source']][MDT] - print("get_storm_reports: found",len(MDT_timezones),"MDT time zones") - if debug: - print(MDT_timezones, file=sys.stderr) - print("changing tz from 6 to 3 because CST=MDT") - print("WARNING - proceeding with program. Wrote to SPC Apr 1 2019 about fixing these lines", file=sys.stderr) - Email20190401PatrickMarsh = "...took over database in 2017 and have no record or documentation as to what those timezones are. Each year I append new information to the end of the old information, so timezones will continue to exist as is until I can learn what those time zones are. take a look at the NCEI version of storm data. They may have information I do not." - rpts.loc[MDT == 6, "tz"] = 3 - # When timezone = 0, it is 'UNK' (unknown?) in the NCEI Storm Events database. - # When timezone = 6, it is 'MDT' (Mountain Daylight Time?) in the NCEI Storm Events database. - """ - om yr_mo_dy_time tz county/zone time zone according to NCEI Storm Events -2507 245 1956-06-01 11:33:00 0 UNK -2855 89 1957-04-02 23:45:00 0 UNK -8145 216 1965-05-05 14:45:00 6 MDT -9569 158 1967-04-21 12:33:00 0 UNK -13409 264 1972-05-13 18:08:00 0 UNK -15792 804 1974-08-13 15:03:00 0 UNK -20640 458 1980-06-04 16:30:00 0 UNK -22101 271 1982-05-11 14:25:00 0 UNK -24007 200 1984-04-26 19:32:00 0 CST -25899 501 1986-07-01 22:15:00 6 MDT -26054 656 1986-09-04 18:55:00 6 MDT -28793 419 1990-05-24 15:00:00 6 MDT -28797 422 1990-05-24 16:00:00 6 MDT -28810 433 1990-05-24 18:33:00 6 MDT -29192 815 1990-06-27 20:00:00 6 MDT -29232 855 1990-07-05 21:10:00 6 MDT -29347 971 1990-08-15 18:30:00 6 MDT -33262 151 1994-04-22 18:06:00 6 MDT -33557 446 1994-05-31 15:00:00 6 MDT -33572 461 1994-06-06 14:40:00 6 MDT -33574 463 1994-06-06 15:00:00 6 MDT -33585 474 1994-06-07 14:47:00 6 MDT -33586 476 1994-06-07 15:57:00 6 MDT -33587 477 1994-06-07 16:10:00 6 MDT -33589 478 1994-06-07 16:35:00 6 MDT -33592 482 1994-06-07 18:50:00 6 not in Storm Events database -33783 672 1994-06-29 15:45:00 6 MDT -33834 722 1994-07-06 18:17:00 6 MDT -33855 744 1994-07-12 14:30:00 6 MDT -33886 775 1994-07-18 15:30:00 6 MDT -33887 776 1994-07-18 16:00:00 6 MDT -33888 777 1994-07-18 16:25:00 6 MDT -33889 778 1994-07-18 16:40:00 6 MDT -33890 779 1994-07-18 16:55:00 6 not in Storm Events database -33891 780 1994-07-18 16:55:00 6 not in Storm Events database -33892 781 1994-07-18 17:00:00 6 MDT -""" - - - # All times, except for ?=unknown and 9=GMT, were converted to 3=CST. - # tz=3 is CST - # tz=9 is GMT - # Convert to UTC by adding 9 and subtracting tz hours. - rpts["time"] = rpts['date_time'] + pd.to_timedelta(9 - rpts.tz, unit='h') - # make time-zone aware datetime object - rpts["time"] = rpts["time"].dt.tz_localize(pytz.UTC) - - if any(rpts['tz'] == 0): - print("reports file: "+rpts_file, file=sys.stderr) - unknown_timezones = rpts[['om','date_time','tz','event_type', 'source']][rpts['tz'] == 0] - if debug: - print("get_storm_reports: found",len(unknown_timezones),"unknown time zones") - print(unknown_timezones, file=sys.stderr) - - time_window = (rpts.time >= start) & (rpts.time < end) - rpts = rpts[time_window] - if debug: - print("found",len(rpts),event_type,"reports") - - # Sanity Check: - # Verify I get the same thing as Ryan Sobash's sqlite3 database - conn = sqlite3.connect("/glade/u/home/sobash/2013RT/REPORTS/reports_all.db") - sqltable = "reports_" + event_type - # Could apply a datetime range here (WHERE datetime BETWEEN yyyy/mm/dd hh:mm:ss and blah), but converting from UTC to CST is tricky. - sqlcommand = "SELECT * FROM "+sqltable - sql_df = pd.read_sql_query(sqlcommand, conn, parse_dates=['datetime']) - conn.close() - # Add 6 hours to datetime. This converts to UTC. - sql_df["datetime"] = sql_df["datetime"] + pd.to_timedelta(6, unit='h') - # make it aware of its UTC timezone. - sql_df["datetime"] = sql_df["datetime"].dt.tz_localize(pytz.UTC) - sql_df = sql_df[(sql_df.datetime >= start) & (sql_df.datetime < end)] - - # See if they have the same number of rows - if len(sql_df) != len(rpts): - print("My data don't match Ryan's SQL database") - pdb.set_trace() - sys.exit(1) - # See if they have the same times - if (sql_df["datetime"].values != rpts["time"].values).any(): - print("My data times don't match Ryan's SQL database") - # See if they have the same locations - same_columns = ["slat", "slon", "elat", "elon"] - if (sql_df[same_columns].values != rpts[same_columns].values).any(): - print("My data locations don't match Ryan's SQL database") - max_abs_difference = np.max(np.abs(sql_df[same_columns]-rpts[same_columns])) - print("max abs difference") - print(max_abs_difference) - if all(max_abs_difference < 0.000001): - print("who cares about such a small difference?") - else: - pdb.set_trace() - sys.exit(1) - if debug: - print("From Ryan's SQL database") - print(sqlcommand) - print(sql_df.to_string()) - - - all_rpts = all_rpts.append(rpts, ignore_index=True, sort=False) - - return all_rpts - -def plot(storm_reports, ax, scale=1, drawradius=0, alpha=0.4, debug=False): - - if storm_reports.empty: - # is this the right thing to return? what about empty list []? or rpts? - if debug: - print("spc.plot(): storm reports DataFrame is empty. Returning") - return - - # Color, size, marker, and label of wind, hail, and tornado storm reports - kwdict = { - "wind": {"c" : 'blue', "s": 8*scale, "marker":"s", "label":"Wind Report"}, - "high wind": {"c" : 'black', "s":12*scale, "marker":"s", "label":"Wind Report/HI"}, - "hail": {"c" : 'green', "s":12*scale, "marker":"^", "label":"Hail Report"}, - "large hail": {"c" : 'black', "s":16*scale, "marker":"^", "label":"Hail Report/LG"}, - "torn": {"c" : 'red', "s":12*scale, "marker":"v", "label":"Torn Report"} - } - - storm_rpts_plots = [] - - for event_type in ["wind", "high wind", "hail", "large hail", "torn"]: - if debug: - print("looking for "+event_type) - xrpts = storm_reports[storm_reports.event_type == event_type] - print("plot",len(xrpts),event_type,"reports") - if len(xrpts) == 0: - continue - lons, lats = xrpts.slon.values, xrpts.slat.values - storm_rpts_plot = ax.scatter(lons, lats, alpha = alpha, edgecolors="None", **kwdict[event_type], - transform=cartopy.crs.Geodetic()) - storm_rpts_plots.append(storm_rpts_plot) - if debug: - pdb.set_trace() - if drawradius > 0: - # With lons and lats, specifying more than one dimension allows individual points to be drawn. - # Otherwise a grid of circles will be drawn. - # It warns about using PlateCarree to approximate Geodetic. It still warps the circles - # appropriately, so I think this is okay. - within_radius = ax.tissot(rad_km = drawradius, lons=lons[np.newaxis], lats=lats[np.newaxis], - facecolor=kwdict[event_type]["c"], alpha=0.4, label=str(drawradius)+" km radius") - # TODO: Legend does not support tissot cartopy.mpl.feature_artist. - # A proxy artist may be used instead. - # matplotlib.org/users/legend_guide.html# - # creating-artists-specifically-for-adding-to-the-legend-aka-proxy-artists - # storm_rpts_plots.append(within_radius) - - return storm_rpts_plots - - - -def to_MET(df, gribcode=187): - # gribcode 187 :lightning - # INPUT: storm_reports DataFrame from spc.get_storm_reports() - # Output: MET point observation format, one observation per line. - # Use gribcode if specified. Otherwise default. - # 10 = WIND - # - # Each observation line will consist of the following 11 columns of data: - met_columns = ["Message_Type", "Station_ID", "Valid_Time", "Lat", "Lon", "Elevation", "Grib_Code", "Level", "Height", "QC_String", "Observation_Value"] - - df["Message_Type"] = "ADPSFC" - df["Station_ID"] = df.st - df["Valid_Time"] = df.time.dt.strftime('%Y%m%d_%H%M%S') #df.time should be aware of UTC time zone - df["Lat"] = (df.slat+df.elat)/2 - df["Lon"] = (df.slon+df.elon)/2 - df["Elevation"] = "NA" - df["Grib_Code"] = gribcode - df["Height"] = "NA" - # Change grib_code to 10 (WIND) where event type is wind - df.loc[df['event_type'].str.contains('wind'), "Grib_Code"] = 10 - df.loc[df['event_type'].str.contains('wind'), "Height"] = 10 - df["Level"] = "NA" - df["QC_String"] = "NA" - df["Observation_Value"] = df.mag - # index=False don't write index number - # Change NaN to "NA" MET considers "NA" missing - # This may not matter, but by adding a string 'NA', it changes the format of the entire column. Floats change to integers (or maybe strings). - df.replace(to_replace=np.nan, value = 'NA', inplace=True) - return df.to_string(columns=met_columns,index=False,header=False) + "\n" - - - -# NCDC storm event details don't have a lat and lon. Just a city, a range, and an azimuth. -# Perhaps the lat and lon are in the storm event location files (as opposed to "details"). -# Maybe just go with SPC storm reports. - -def stormEvents( - idir="/glade/work/ahijevyc/share/stormevents", - start = datetime.datetime(2016,6,1), - end = datetime.datetime(2016,7,1), - event_type = "Tornado", - debug = False - ): - # INPUT: idir - # Directory with input files. For example: StormEvents_details-ftp_v1.0_d2015_c20180525.csv - # Downloaded from NCDC storm events database at https://www.ncdc.noaa.gov/stormevents/ftp.jsp - - ifiles = glob.glob(idir+"/StormEvents_details-*csv*") - year = start.year - ifile = [s for s in ifiles if "_d"+str(year)+"_c" in s] - if len(ifile) != 1: - print("Did not find one storm events file", ifile) - pdb.set_trace() - - ifile = ifile[0] - if debug: - pdb.set_trace() - - events = pd.read_csv(ifile) - # This is ugly - events["BEGIN"] = pd.to_datetime(events.BEGIN_DATE_TIME, format="%d-%b-%y %H:%M:%S") - events["END"] = pd.to_datetime(events.END_DATE_TIME, format="%d-%b-%y %H:%M:%S") - # Just return a particular event type. 'Thunderstorm Wind' or 'Tornado' or 'Hail', for example. - if event_type is not None: - events = events[events.EVENT_TYPE == event_type] - - if start is not None: - events = events[events.BEGIN >= start] - - if end is not None: - events = events[events.END < end] - - return events - -def events2met(df): - # Point Observation Format - # input ASCII MET point observation format contains one observation per line. - # Each input observation line should consist of the following 11 columns of data: - met_columns = ["Message_Type", "Station_ID", "Valid_Time", "Lat", "Lon", "Elevation", "Grib_Code", "Level", "Height", "QC_String", "Observation_Value"] - - df["Message_Type"] = "ADPSFC" - df["Station_ID"] = df.WFO - df["Valid_Time"] = df.BEGIN.dt.strftime('%Y%m%d_%H%M%S') - df["Lat"] = df.lat # I don't know how to get this. - df["Lon"] = df.lon - df["Elevation"] = "NA" - df["Grib_Code"] = "NA" - df["Level"] = "NA" - df["Height"] = "NA" - df["QC_String"] = "NA" - df["Observation_Value"] = df.TOR_F_SCALE - print(df.to_string(columns=met_columns)) - - From dac842431fd5af6abdf686be94c9c02097c68331 Mon Sep 17 00:00:00 2001 From: ahijevyc Date: Tue, 20 Oct 2020 13:40:22 -0600 Subject: [PATCH 35/68] Copied version in ~ahijevyc/lib/python*/webplot.py Will try to make this repo (webplot) and this branch (mpas_ensemble_hwt2017) my best version. Link to it from ~ahijevyc/lib/python*/. Stuff about mesh. Other improvements. --- webplot.py | 166 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 103 insertions(+), 63 deletions(-) diff --git a/webplot.py b/webplot.py index 95984f5..7332672 100755 --- a/webplot.py +++ b/webplot.py @@ -8,15 +8,20 @@ from scipy import interpolate from scipy.spatial import qhull import subprocess -import mpas, mpas_vort_cell +import mpas_vort_cell import re +import pdb from fieldinfo import * +print("importing mpas module. remove 'import mpas' to do some other model") +from mpas import fieldinfo, makeEnsembleListMPAS # To use MFDataset, first convert netcdf4 to netcdf4-classic with nccopy # for example, nccopy -d nc7 /glade/p/nsc/nmmm0046/schwartz/MPAS_ens_15-3km_mesh/POST/2017050100/ens_1/diag_latlon_g193.2017-05-02_00.00.00.nc /glade/scratch/ahijevyc/hwt2017/2017050100/ens_1/diag_latlon_g193.2017-05-02_00.00.00.nc from netCDF4 import Dataset, MFDataset # xarray can handle muliple files with NETCDF4 non-classic. netCDF4 MFDataset can't do that. import xarray +def log(msg): print(time.ctime(time.time()),':', msg) + class webPlot: '''A class to plot data from NCAR ensemble''' def __init__(self): @@ -26,6 +31,7 @@ def __init__(self): self.debug = self.opts['debug'] self.autolevels = self.opts['autolevels'] self.domain = self.opts['domain'] + self.mesh= self.opts['mesh'] if ',' in self.opts['timerange']: self.shr, self.ehr = list(map(int, self.opts['timerange'].split(','))) else: self.shr, self.ehr = int(self.opts['timerange']), int(self.opts['timerange']) self.createFilename() @@ -65,19 +71,21 @@ def toServer(self, debug=False, url="nova.mmm.ucar.edu:/web/htdocs/projects/mpas if result.returncode != 0: print(result) - def loadMap(self, overlay=False): + def loadMap(self, nlon_max=1500, nlat_max=1500, overlay=False): if hasattr(self, 'domain'): # once called with empty junk webplot class, just to get lats/lons/x/y/ibox attributes if overlay: PYTHON_SCRIPTS_DIR = os.getenv('PYTHON_SCRIPTS_DIR', '/glade/u/home/sobash/RT2015_gpx') self.fig, self.ax, self.m = pickle.load(open('%s/overlays/rt2015_overlay_%s.pk'%(PYTHON_SCRIPTS_DIR,self.domain), 'r')) else: PYTHON_SCRIPTS_DIR = os.getenv('PYTHON_SCRIPTS_DIR', '/glade/work/ahijevyc/share/rt_ensemble/python_scripts') - pklfile = '%s/rt2015_%s.pk'%(PYTHON_SCRIPTS_DIR,self.domain) + pklfile = '%s/%s_%s_%dx%d.pk'%(PYTHON_SCRIPTS_DIR,self.mesh,self.domain,nlon_max,nlat_max) print("loading "+pklfile) self.fig, self.ax, self.m, self.lons,self.lats,self.min_grid_spacing_km,self.delta_deg,self.lon2d,self.lat2d,self.x2d,self.y2d,self.ibox,self.x,self.y,self.vtx,self.wts = pickle.load(open(pklfile, 'rb')) def readEnsemble(self): - self.data, self.missing_members = readEnsemble(self.initdate, self.domain, timerange=[self.shr,self.ehr], fields=self.opts, debug=self.debug, ENS_SIZE=self.ENS_SIZE) + if hasattr(self, 'data') and hasattr(self, 'missing_members') and 'must_reread_ensemble' not in self.data: + return + self.data, self.missing_members = readEnsemble(self.initdate, self.domain, timerange=[self.shr,self.ehr], fields=self.opts, debug=self.debug, ENS_SIZE=self.ENS_SIZE, mesh=self.mesh) def plotDepartures(self): from collections import OrderedDict @@ -87,7 +95,7 @@ def plotDepartures(self): if fieldname == 't2depart': normals = open('/glade/u/home/sobash/RT2015_gpx/hly-temp-normal.txt').readlines() elif fieldname == 'td2depart': normals = open('/glade/u/home/sobash/RT2015_gpx/hly-dewp-normal.txt').readlines() - # figure out local times for this UTC time because NCDC is stupid + # figure out local times for this UTC time because NCDC utcoffset = (time.mktime(time.gmtime()) - time.mktime(time.localtime()))/3600.0 #utc to local time offset for mountain time monthday = (self.initdate + timedelta(hours=self.shr)).strftime(' %m %d ') #search for this string in normals file (see below) hr_pt = (self.initdate + timedelta(hours=self.shr) - timedelta(hours=utcoffset+1)).hour # hour in pacific time @@ -292,7 +300,7 @@ def plotTitleTimes(self): # Plot missing members (use wrfout count here, if upp missing this wont show that) if len(self.missing_members['wrfout']) > 0: - missing_members = sorted(set([ (x%10)+1 for x in self.missing_members['wrfout'] ])) #get member number from missing indices + missing_members = sorted(set([ (x%self.ENS_SIZE)+1 for x in self.missing_members['wrfout'] ])) #get member number from missing indices missing_members_string = ', '.join(str(x) for x in missing_members) self.ax.text(x1-5, y0+5, 'Missing member #s: %s'%missing_members_string, horizontalalignment='right') @@ -322,7 +330,6 @@ def plotFill(self, debug=False): min, max = self.data['fill'][0].min(), self.data['fill'][0].max() levels = np.linspace(min, max, num=10) cmap = colors.ListedColormap(self.opts['fill']['colors']) - norm = colors.BoundaryNorm(levels, cmap.N) tick_labels = levels[:-1] else: levels = self.opts['fill']['levels'] @@ -334,7 +341,7 @@ def plotFill(self, debug=False): cmap.set_over(self.opts['fill']['colors'][-1]) extend, extendfrac = 'max', 0.02 tick_labels = levels - norm = colors.BoundaryNorm(levels, cmap.N) + norm = colors.BoundaryNorm(levels, cmap.N) data = self.data['fill'][0] # regrid 1D mesh that needs to be smoothed @@ -349,7 +356,7 @@ def plotFill(self, debug=False): elif self.opts['fill']['ensprod'] in ['neprob', 'neprobgt', 'neproblt']: # assume the data array has been interpolated to lat/lon cs1 = self.m.contourf(self.x2d, self.y2d, data, levels=levels, cmap=cmap, norm=norm, extend='max', ax=self.ax) - else: + elif self.vtx is not None: # This is probably not an irregular mesh like MPAS # use ibox and tri # Sometimes you get a warning kwarg tri is ignored. # Tried removing tri=True but got IndexError: too many indices for array @@ -359,6 +366,9 @@ def plotFill(self, debug=False): if debug: print("plotFill: starting contourf with 2d array") cs1 = self.m.contourf(self.x2d, self.y2d, data, levels=levels, cmap=cmap, norm=norm, extend='max', ax=self.ax) + else: + # Added for HRRR + cs1 = self.m.contourf(self.x2d, self.y2d, data, levels=levels, cmap=cmap, norm=norm, extend='max', ax=self.ax) self.plotColorbar(cs1, levels, tick_labels, extend, extendfrac) @@ -475,7 +485,11 @@ def plotContour(self): if self.opts['contour']['name'] in ['sbcinh','mlcinh']: linewidth, alpha = 0.5, 0.75 else: linewidth, alpha = 1.5, 1.0 - data = self.latlonGrid(self.data['contour'][0]) + data = self.data['contour'][0] + if self.debug: + pdb.set_trace() + if self.vtx is not None: # Interpolate to latlon Grid + data = self.latlonGrid(data) if self.opts['contour']['name'] in ['t2-0c']: data = ndimage.gaussian_filter(data, sigma=2) else: data = ndimage.gaussian_filter(data, sigma=25) @@ -491,7 +505,7 @@ def plotBarbs(self, debug=False): else: alpha=1.0 - if debug: print("plotBarbs: starting barbs") + if self.debug: print("plotBarbs: starting barbs") # skip interval was intended for 2-D fields if len(self.x.shape) == 2: cs2 = self.m.barbs(self.x[::skip,::skip], self.y[::skip,::skip], self.data['barb'][0][::skip,::skip], self.data['barb'][1][::skip,::skip], color='black', alpha=alpha, length=5.5, linewidth=0.25, sizes={'emptybarb':0.05}, ax=self.ax) @@ -534,7 +548,7 @@ def plotSpaghetti(self): #plt.legend(proxy, ["member %d"%i for i in range(1,11)], ncol=5, loc='right', bbox_to_anchor=(1.0,-0.05), fontsize=11, \ # frameon=False, borderpad=0.25, borderaxespad=0.25, handletextpad=0.2) - def plotStamp(self, debug=False): + def plotStamp(self): fig_width_px, dpi = 1280, 90 fig = plt.figure(dpi=dpi) @@ -562,7 +576,7 @@ def plotStamp(self, debug=False): w, h = (width_per_panel/float(fig_width))-spacing_w, (height_per_panel/float(fig_height))-spacing_h if member == 9: y = 0 - if debug: + if self.debug: print('member', member, 'creating axes at', x, y) thisax = fig.add_axes([x,y,w,h]) @@ -576,7 +590,7 @@ def plotStamp(self, debug=False): # plot, unless file that has fill field is missing, then skip if member not in self.missing_members[filename] and member < self.ENS_SIZE: data = self.latlonGrid(self.data['fill'][0][memberidx,:]) - if debug: + if self.debug: print("plotStamp: starting contourf with regridded array", memberidx) cs1 = self.m.contourf(self.x2d, self.y2d, data, levels=levels, cmap=cmap, norm=norm, extend='max', ax=thisax) memberidx += 1 @@ -658,6 +672,7 @@ def parseargs(): parser.add_argument('-bs', '--barbskip', help='barb skip interval') parser.add_argument('-t', '--title', help='title for plot') parser.add_argument('-dom', '--domain', default='CONUS', help='domain to plot') + parser.add_argument('-m', '--mesh', default='rt2015', choices=['mpas','rt2015','hrrr'], help='mesh') parser.add_argument('-al', '--autolevels', action='store_true', help='use min/max to determine levels for plot') parser.add_argument('-con', '--convert', default=True, action='store_false', help='run final image through imagemagick') parser.add_argument('-i', '--interp', default=False, action='store_true', help='plot interpolated station values') @@ -812,25 +827,27 @@ def makeEnsembleListHREF(wrfinit, timerange, ENS_SIZE): return (file_list, missing_list) -def makeEnsembleListMPAS(wrfinit, timerange, ENS_SIZE, g193=False, debug=False): +def makeEnsembleListHRRR(wrfinit, timerange, ENS_SIZE): # create lists of files (and missing file indices) for various file types shr, ehr = timerange - file_list = { 'wrfout':[], 'diag':[] } - missing_list = { 'wrfout':[], 'diag':[] } + file_list = { 'diag':[], 'wrfout':[]} + missing_list = { 'diag':[], 'wrfout':[]} missing_index = 0 + for hr in range(shr,ehr+1): - wrfvalidstr = (wrfinit + timedelta(hours=hr)).strftime('%Y-%m-%d_%H.%M.%S') yyyymmddhh = wrfinit.strftime('%Y%m%d%H') + yyyymmdd = wrfinit.strftime('%Y%m%d') + init = wrfinit.strftime('%H') + for mem in range(1,ENS_SIZE+1): - diag = '/glade/p/nsc/nmmm0046/schwartz/MPAS_ens_15-3km_mesh/POST/%s/ens_%d/diag.%s.nc'%(yyyymmddhh,mem,wrfvalidstr) - if g193: - diag = '/glade/p/nsc/nmmm0046/schwartz/MPAS_ens_15-3km_mesh/POST/%s/ens_%d/diag_latlon_g193.%s.nc'%(yyyymmddhh,mem,wrfvalidstr) - if debug: print(diag) + if mem == 1: diag = '/glade/scratch/ahijevyc/hrrr/%s/%s_i%s_f%03d_HRRR-NCEP_wrfprs.nc'%(yyyymmddhh,yyyymmdd,init,hr) + print(diag) + if os.path.exists(diag): file_list['diag'].append(diag) else: missing_list['diag'].append(missing_index) missing_index += 1 - if not file_list['diag']: - print('Empty file_list') + + return (file_list, missing_list) def makeEnsembleListArchive(wrfinit, timerange): @@ -899,20 +916,27 @@ def makeEnsembleListDA(wrfinit, timerange): missing_index += 1 return (file_list, missing_list) -def readEnsemble(wrfinit, domain, timerange=None, fields=None, debug=False, ENS_SIZE=10): +def readEnsemble(wrfinit, domain, timerange=None, fields=None, debug=False, ENS_SIZE=10, mesh=None): ''' Reads in desired fields and returns 2-D arrays of data for each field (barb/contour/field) ''' if debug: print(fields) datadict = {} + file_list, missing_list = None, None #file_list, missing_list = makeEnsembleList(wrfinit, timerange, ENS_SIZE) #construct list of files #file_list, missing_list = makeEnsembleListNSC(wrfinit, timerange) #construct list of files #file_list, missing_list = makeEnsembleListStan(wrfinit, timerange, ENS_SIZE) #construct list of files - file_list, missing_list = makeEnsembleListMPAS(wrfinit, timerange, ENS_SIZE, g193=False, debug=debug) #construct list of files + if mesh == 'mpas': + file_list, missing_list = makeEnsembleListMPAS(wrfinit, timerange, ENS_SIZE, g193=False, debug=debug) #construct list of files #file_list, missing_list = makeEnsembleListArchive(wrfinit, timerange) #construct list of files #file_list, missing_list = makeEnsembleListHybrid(wrfinit, timerange) #construct list of files #file_list, missing_list = makeEnsembleListHREF(wrfinit, timerange, ENS_SIZE) #construct list of files - + #file_list, missing_list = makeEnsembleListHRRR(wrfinit, timerange, 1) #construct list of files + if not file_list: + print("no Ensemble file list. Exiting.") + print("Perhaps add --mesh mpas to make_webplot.py command line") + sys.exit(1) + # loop through fill field, contour field, barb field and retrieve required data for f in ['fill', 'contour', 'barb']: if not list(fields[f].keys()): continue @@ -928,7 +952,8 @@ def readEnsemble(wrfinit, domain, timerange=None, fields=None, debug=False, ENS_ # open Multi-file netcdf dataset if debug: - print("opening xarray mfdataset ", file_list[filename]) + print("opening xarray mfdataset "+ ' '.join(file_list[filename])) + log("opening xarray mfdataset "+ ' '.join(file_list[filename])) fh = xarray.open_mfdataset(file_list[filename],concat_dim='Time') # This concatenation dimension includes different times AND members. @@ -937,7 +962,7 @@ def readEnsemble(wrfinit, domain, timerange=None, fields=None, debug=False, ENS_ # loop through each field, wind fields will have two fields that need to be read datalist = [] for n,array in enumerate(arrays): - if debug: print('Reading', array) + if debug: log('Reading data '+ array) #read in 3D array (times*members,ny,nx) from file object if 'arraylevel' in fields[f]: @@ -945,11 +970,10 @@ def readEnsemble(wrfinit, domain, timerange=None, fields=None, debug=False, ENS_ else: level = fields[f]['arraylevel'] else: level = None - #if level == 'max': data = np.amax(fh.variables[array][:,:,:,:], axis=1) - #elif level is None: data = fh.variables[array][:,:,:] - #else: data = fh.variables[array][:,level,:,:] + if level == 'max': data = np.amax(fh.variables[array][:,:,:,:], axis=1) + elif level is None: data = fh.variables[array][:] + else: data = fh.variables[array][:,level,:,:] - data = fh.variables[array][:,:] data = data.values # use the numpy array, not the full xarray object. # Many things that come afterward assume a numpy array, like flatten method. if fh.variables[array].dims[1] == 'nVertices': @@ -1067,7 +1091,8 @@ def readEnsemble(wrfinit, domain, timerange=None, fields=None, debug=False, ENS_ for i in missing_list[filename]: data = np.insert(data, i, np.nan, axis=0) #insert nan for missing files data = np.reshape(data, (ntimes,ENS_SIZE,*spatial_dimensions)) data = np.nanmax(data, axis=0) - if fieldtype in ['neprob','neprobgt','neproblt']: + if fieldtype in ['neprob','neprobgt','neproblt']: + datadict['must_reread_ensemble'] = True junk = webPlot() junk.domain = domain# define correct domain so .ibox can be correct junk.loadMap() @@ -1123,8 +1148,8 @@ def readMPASVertices(ifile="/glade/p/mmm/parc/schwartz/MPAS/15-3km_mesh/init.nc" fh.close() return (nEdgesOnCell, verticesOnCell) -def readGridMPAS(): - fh = Dataset("/glade/p/mmm/parc/schwartz/MPAS/15-3km_mesh/init.nc", "r") +def readGridMPAS(init_file="/glade/p/mmm/parc/schwartz/MPAS/15-3km_mesh/init.nc"): + fh = Dataset(init_file, "r") latCell = fh.variables['latCell'][:] lonCell = fh.variables['lonCell'][:] areaCell = fh.variables['areaCell'][:] # units m^2 @@ -1135,7 +1160,7 @@ def readGridMPAS(): lonCell[lonCell >= 180] = lonCell[lonCell >= 180] - 360 return (latCell, lonCell, min_grid_spacing_km) -def saveNewMap(domstr='CONUS', wrfout=None): +def saveNewMap(domstr='CONUS', mesh='rt2015', wrfout=None, nlon_max=1500, nlat_max=1500): # if domstr is not in the dictionary, then use provided wrfout to create new domain if domstr not in domains: fh = Dataset(wrfout, 'r') @@ -1188,31 +1213,47 @@ def saveNewMap(domstr='CONUS', wrfout=None): #m.drawcounties(linewidth=0.1, color='gray', ax=ax) - # load lat/lons - lats, lons, min_grid_spacing_km = readGridMPAS() - delta_deg = min_grid_spacing_km / 111 - if m.lonmin > 180 or m.lonmax > 180: - lons[lons<0] = lons[lons<0] + 360. # change -180-0 to 180-360 to match m.lonmin and m.lonmax - # Replace m.lonmax with ur_lon? Otherwise, risk getting +179.999 when NW corner crosses the datetime - nlon = int((m.lonmax - m.lonmin)/delta_deg) - nlat = int((m.latmax - m.latmin)/delta_deg) - if nlon > 1500: - nlon = 1500 - if nlat > 1500: - nlat = 1500 - lon2d, lat2d = np.meshgrid(np.linspace(m.lonmin, m.lonmax, nlon), np.linspace(m.latmin,m.latmax,nlat)) - # Convert to map coordinates instead of latlon to avoid the need to specify latlon=True in contour and barb methods. - x2d, y2d = m(lon2d,lat2d) - # ibox: subscripts within lat/lon box # only used to speed up 1-D array triangulation and plotting - ibox = (m.lonmin-1 <= lons ) & (lons < m.lonmax+1) & (m.latmin-1 <= lats) & (lats < m.latmax+1) - lons = lons[ibox] - lats = lats[ibox] - x, y = m(lons,lats) - - # use .filled() to avoid error about masked arrays - vtx, wts = interp_weights(np.vstack((lons.filled(),lats.filled())).T,np.vstack((lon2d.flatten(), lat2d.flatten())).T) - - pickle.dump((fig,ax,m,lons,lats,min_grid_spacing_km,delta_deg,lon2d,lat2d,x2d,y2d,ibox,x,y,vtx,wts), open('/glade/work/ahijevyc/share/rt_ensemble/python_scripts/rt2015_%s.pk'%domstr, 'wb')) + if mesh == 'hrrr': + lons=None + lats=None + min_grid_spacing_km = None + delta_deg = None + # Can we + fh = Dataset("/glade/scratch/ahijevyc/hrrr/2018120106/20181201_i06_f024_HRRR-NCEP_wrfprs.nc", 'r') + lat2d = fh.variables['gridlat_0'][:] + lon2d = fh.variables['gridlon_0'][:] + x2d, y2d = m(lon2d,lat2d) + ibox = None + x = None + y = None + vtx = None + wts = None + else: + # load lat/lons + lats, lons, min_grid_spacing_km = readGridMPAS(init_file="/glade/p/mmm/parc/schwartz/MPAS/15-3km_mesh/init.nc") + delta_deg = min_grid_spacing_km / 111 + if m.lonmin > 180 or m.lonmax > 180: + lons[lons<0] = lons[lons<0] + 360. # change -180-0 to 180-360 to match m.lonmin and m.lonmax + # Replace m.lonmax with ur_lon? Otherwise, risk getting +179.999 when NW corner crosses the datetime + nlon = int((m.lonmax - m.lonmin)/delta_deg) + nlat = int((m.latmax - m.latmin)/delta_deg) + if nlon > nlon_max: + nlon = nlon_max + if nlat > nlat_max: + nlat = nlat_max + lon2d, lat2d = np.meshgrid(np.linspace(m.lonmin, m.lonmax, nlon), np.linspace(m.latmin,m.latmax,nlat)) + # Convert to map coordinates instead of latlon to avoid the need to specify latlon=True in contour and barb methods. + x2d, y2d = m(lon2d,lat2d) + # ibox: subscripts within lat/lon box # only used to speed up 1-D array triangulation and plotting + ibox = (m.lonmin-1 <= lons ) & (lons < m.lonmax+1) & (m.latmin-1 <= lats) & (lats < m.latmax+1) + lons = lons[ibox] + lats = lats[ibox] + x, y = m(lons,lats) + + # use .filled() to avoid error about masked arrays + vtx, wts = interp_weights(np.vstack((lons.filled(),lats.filled())).T,np.vstack((lon2d.flatten(), lat2d.flatten())).T) + + pickle.dump((fig,ax,m,lons,lats,min_grid_spacing_km,delta_deg,lon2d,lat2d,x2d,y2d,ibox,x,y,vtx,wts), open('/glade/work/ahijevyc/share/rt_ensemble/python_scripts/%s_%s_%dx%d.pk'%(mesh,domstr,nlon_max,nlat_max), 'wb')) def drawOverlay(domstr='CONUS'): ll_lat, ll_lon, ur_lat, ur_lon = domains[domstr]['corners'] @@ -1379,4 +1420,3 @@ def computefrzdepth(t): frz_at_surface = np.where(t[0,:] < 33, True, False) #pts where surface T is below 33F max_column_t = np.amax(t, axis=0) above_frz_aloft = np.where(max_column_t > 32, True, False) #pts where max column T is above 32F - From e0f48218fe75d263757fdc0ff5adcd83a2151bd6 Mon Sep 17 00:00:00 2001 From: ahijevyc Date: Fri, 10 Feb 2023 10:20:40 -0700 Subject: [PATCH 36/68] copied mpas.py from ~ahijevyc/lib/python3.6 --- mpas.py | 209 ++++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 166 insertions(+), 43 deletions(-) diff --git a/mpas.py b/mpas.py index 68ed9b2..f2df0b2 100644 --- a/mpas.py +++ b/mpas.py @@ -1,19 +1,46 @@ -import pandas as pd -import sys -from netCDF4 import Dataset +import atcf import datetime +from fieldinfo import fieldinfo,readNCLcm +import logging +from netCDF4 import Dataset import numpy as np +import os +import pandas as pd import pdb import re -import atcf +import sys +import xarray def get_diag_name(valid_time, prefix='diag.', suffix='.nc'): diag_name = prefix + valid_time.strftime("%Y-%m-%d_%H.%M.%S") + suffix return diag_name -def origmesh(df, initfile, diagdir, debug=False): +def raw_vitals(row, diagdir, lonCell, latCell, wind_radii_method=None): + row = row.head(1).squeeze() # make multiple rad lines one series + assert 'originalmeshfile' not in row, f"{row} already has original mesh vitals" + diagfile = get_diag_name(row.valid_time, prefix='diag.', suffix='.nc') + + diagfile = os.path.join(diagdir,diagfile) + logging.debug(f"reading diagfile {diagfile}") + ds = xarray.open_dataset(diagfile) + ds = ds[['u10','v10','mslp']].metpy.quantify() # Don't choke on 'dBZ' not defined in unit registry + ds = ds.isel(Time=0) + + # Extract vmax, RMW, minp, and radii of wind thresholds + derived_winds_dict = atcf.derived_winds(ds.u10, ds.v10, ds.mslp, lonCell, latCell, row, wind_radii_method=wind_radii_method) + row = atcf.unitless_row(derived_winds_dict, row) + + row["originalmeshfile"] = diagfile + # replace the row with (possibly) multiple rows with different wind radii (34/50/64 knot) + row = atcf.add_wind_rad_lines(row, derived_winds_dict["wind_radii"]) + return row + - # Get raw values from MPAS mesh +def origmesh(df, initfile, diagdir, wind_radii_method="max"): + # assert this is a single track + assert df.groupby(['basin','cy','initial_time','model']).ngroups == 1, 'mpas.origmesh got more than 1 track' + + # Get raw values from MPAS mesh for one track # input # df = pandas Dataframe version of atcf data @@ -25,53 +52,149 @@ def origmesh(df, initfile, diagdir, debug=False): # The first time this is called initfile is a simple string. # Next time, it is a dictionary with all the needed variables. if isinstance(initfile, str): - if debug: - print("reading lat/lon from", initfile) - init = Dataset(initfile,"r") - lonCellrad = init.variables['lonCell'][:] - latCellrad = init.variables['latCell'][:] - lonCell = np.degrees(lonCellrad) - latCell = np.degrees(latCellrad) + logging.debug(f"reading lat/lon from {initfile}") + init = xarray.open_dataset(initfile) + lonCell = init['lonCell'].metpy.convert_units("degrees") + latCell = init['latCell'].metpy.convert_units("degrees") lonCell[lonCell >= 180] = lonCell[lonCell >=180] - 360. - nEdgesOnCell = init.variables['nEdgesOnCell'][:] - cellsOnCell = init.variables['cellsOnCell'][:] init.close() initfile = { "initfile":initfile, "lonCell":lonCell, "latCell":latCell, - "nEdgesOnCell":nEdgesOnCell, - "cellsOnCell":cellsOnCell } else: - if debug: - print("reading lat/lon from dictionary") + logging.debug("reading lat/lon from dictionary") lonCell = initfile["lonCell"] latCell = initfile["latCell"] - nEdgesOnCell = initfile["nEdgesOnCell"] - cellsOnCell = initfile["cellsOnCell"] - - itime = 0 - for index, row in df.iterrows(): - diagfile = get_diag_name(row.valid_time, prefix='diag.', suffix='.nc') - - if debug: print("reading diagfile", diagdir+diagfile) - nc = Dataset(diagdir+diagfile, "r") - - u10 = nc.variables['u10'][itime,:] - v10 = nc.variables['v10'][itime,:] - mslp = nc.variables['mslp'][itime,:] - nc.close() - - # Extract vmax, RMW, minp, and radii of wind thresholds - raw_vmax_kts, raw_RMW_nm, raw_minp, rad_nm = atcf.derived_winds(u10, v10, mslp, lonCell, latCell, row, debug=debug) - - # TODO: figure out how to replace the row with (possibly) multiple rows with different wind radii - # without passing df, the entire DataFrame - df = atcf.update_df(df, row, raw_vmax_kts, raw_RMW_nm, raw_minp, rad_nm, debug=debug) - if debug: - print("mpas.origmesh() pausing before return") - pdb.set_trace() + + # Only groupby 'fhr'. This should already be a single unique track. + df = df.groupby('fhr').apply(raw_vitals,diagdir,lonCell,latCell,wind_radii_method=wind_radii_method) + if "fhr" in df.index.names: # Don't know why it sometimes is not an index + # don't return with fhr as index + df = df.droplevel('fhr') return df, initfile +# fieldinfo should have been imported from fieldinfo module. +# Copy fieldinfo dictionary for MPAS. Change some fnames and filenames. +fieldinfo['precip']['fname'] = ['rainnc'] +fieldinfo['precip-24hr']['fname'] = ['rainnc'] +fieldinfo['precip-48hr']['fname'] = ['rainnc'] +fieldinfo['precipacc']['fname'] = ['rainnc'] +fieldinfo['precipacc']['filename'] = 'diag' +fieldinfo['sbcape']['fname'] = ['sbcape'] +fieldinfo['sbcape']['filename'] = 'diag' +fieldinfo['mlcape']['fname'] = ['mlcape'] +fieldinfo['mlcape']['filename'] = 'diag' +fieldinfo['mucape']['fname'] = ['cape'] +fieldinfo['mucape']['filename'] = 'diag' +fieldinfo['sbcinh']['fname'] = ['sbcin'] +fieldinfo['sbcinh']['filename'] = 'diag' +fieldinfo['mlcinh']['fname'] = ['mlcin'] +fieldinfo['mlcinh']['filename'] = 'diag' +fieldinfo['pwat']['fname'] = ['precipw'] +fieldinfo['pwat']['filename'] = 'diag' +fieldinfo['mslp']['fname'] = ['mslp'] +fieldinfo['mslp']['filename'] = 'diag' +fieldinfo['td2']['fname'] = ['surface_dewpoint'] +fieldinfo['td2depart']['fname'] = ['surface_dewpoint'] +fieldinfo['thetae']['fname'] = ['t2m', 'q2', 'surface_pressure'] +fieldinfo['rh2m']['fname'] = ['t2m', 'surface_pressure', 'q2'] +fieldinfo['pblh']['fname'] = ['hpbl'] +fieldinfo['hmuh']['fname'] = ['updraft_helicity_max'] +fieldinfo['hmuh03']['fname'] = ['updraft_helicity_max03'] +fieldinfo['hmuh01']['fname'] = ['updraft_helicity_max01'] +fieldinfo['rvort1']['fname'] = ['rvort1_max'] +fieldinfo['hmup']['fname'] = ['w_velocity_max'] +fieldinfo['hmdn']['fname'] = ['w_velocity_min'] +fieldinfo['hmwind']['fname'] = ['wind_speed_level1_max'] +fieldinfo['hmgrp']['fname'] = ['grpl_max'] +fieldinfo['cref']['fname'] = ['refl10cm_max'] +fieldinfo['cref']['filename'] = 'diag' +fieldinfo['ref1km']['fname'] = ['refl10cm_1km'] +fieldinfo['ref1km']['filename'] = 'diag' +for ztop in ['3','1']: + fieldinfo['srh'+ztop]['fname'] = ['srh_0_'+ztop+'km'] + fieldinfo['srh'+ztop]['filename'] = 'diag' +for ztop in ['6','1']: + fieldinfo['shr0'+ztop+'mag']['fname'] = ['uzonal_'+ztop+'km', 'umeridional_'+ztop+'km', 'uzonal_surface', 'umeridional_surface'] + fieldinfo['shr0'+ztop+'mag']['filename'] = 'diag' +fieldinfo['zlfc']['fname'] = ['lfc'] +fieldinfo['zlcl']['fname'] = ['lcl'] +fieldinfo['zlcl']['filename'] = 'diag' # only zlcl filename needed to be changed from upp, not zlfc +for plev in ['200', '250','300','500','700','850','925']: + fieldinfo['hgt'+plev]['fname'] = ['height_'+plev+'hPa'] + fieldinfo['speed'+plev]['fname'] = ['uzonal_'+plev+'hPa','umeridional_'+plev+'hPa'] + del fieldinfo['speed'+plev]['arraylevel'] + fieldinfo['temp'+plev]['fname'] = ['temperature_'+plev+'hPa'] + del fieldinfo['temp'+plev]['arraylevel'] +for plev in ['500', '700', '850']: + fieldinfo['td'+plev]['fname'] = ['dewpoint_'+plev+'hPa'] + del fieldinfo['td'+plev]['arraylevel'] + fieldinfo['vort'+plev] = {'levels' : [0,9,12,15,18,21,24,27,30,33], 'cmap': readNCLcm('prcp_1'), 'fname': ['vorticity_'+plev+'hPa'], 'filename':'diag'} +fieldinfo['vortpv'] = {'levels' : [0,9,12,15,18,21,24,27,30,33], 'cmap': readNCLcm('prcp_1'), 'fname': ['vort_pv'], 'filename':'diag'} +for plev in ['300', '500', '700', '850', '925']: + fieldinfo['rh'+plev]['fname'] = ['relhum_'+plev+'hPa'] +fieldinfo['speed10m']['fname'] = ['u10', 'v10'] +fieldinfo['speed10m-tc']['fname'] = ['u10','v10'] +fieldinfo['stp']['fname'] = ['sbcape','lcl','srh_0_1km','uzonal_6km','umeridional_6km','uzonal_surface','umeridional_surface'] +fieldinfo['stp']['filename'] = 'diag' +fieldinfo['crefuh']['fname'] = ['refl10cm_max', 'updraft_helicity_max'] +fieldinfo['crefuh']['filename'] = 'diag' +fieldinfo['wind10m']['fname'] = ['u10','v10'] +fieldinfo['shr06'] = { 'fname' : ['uzonal_6km','umeridional_6km','uzonal_surface','umeridional_surface'], 'filename': 'diag', 'skip':50 } +fieldinfo['shr01'] = { 'fname' : ['uzonal_1km','umeridional_1km','uzonal_surface','umeridional_surface'], 'filename': 'diag', 'skip':50 } + +# Enter wind barb info for list of pressure levels +for plev in ['200', '250', '300', '500', '700', '850', '925']: + fieldinfo['wind'+plev] = { 'fname' : ['uzonal_'+plev+'hPa', 'umeridional_'+plev+'hPa'], 'filename':'diag', 'skip':50} + + +def makeEnsembleListMPAS(wrfinit, timerange, ENS_SIZE, g193=False): + # create lists of files (and missing file indices) for various file types + shr, ehr = timerange + file_list = { 'wrfout':[], 'diag':[] } + missing_list = { 'wrfout':[], 'diag':[] } + missing_index = 0 + for hr in range(shr,ehr+1): + wrfvalidstr = (wrfinit + datetime.timedelta(hours=hr)).strftime('%Y-%m-%d_%H.%M.%S') + yyyymmddhh = wrfinit.strftime('%Y%m%d%H') + for mem in range(1,ENS_SIZE+1): + diag = '/glade/p/nsc/nmmm0046/schwartz/MPAS_ens_15-3km_mesh/POST/%s/ens_%d/diag.%s.nc'%(yyyymmddhh,mem,wrfvalidstr) + if g193: + diag = '/glade/p/nsc/nmmm0046/schwartz/MPAS_ens_15-3km_mesh/POST/%s/ens_%d/diag_latlon_g193.%s.nc'%(yyyymmddhh,mem,wrfvalidstr) + logging.debug(diag) + if os.path.exists(diag): file_list['diag'].append(diag) + else: + missing_list['diag'].append(missing_index) + logging.warning(f"{diag} does not exist") + missing_index += 1 + logging.debug(f"file_list {file_list}") + if not file_list['diag']: + logging.info('Empty file_list') + return (file_list, missing_list) + +# From NCAR geocat example + +# This funtion splits a global mesh along longitude +# +# Examine the X coordinates of each triangle in 'tris'. Return an array of 'tris' where only those triangles +# with legs whose length is less than 't' are returned. +# +def unzipMesh(x,tris,t): + return tris[(np.abs((x[tris[:,0]])-(x[tris[:,1]])) < t) & (np.abs((x[tris[:,0]])-(x[tris[:,2]])) < t)] + +# Compute the signed area of a triangle +# +def triArea(x,y,tris): + return ((x[tris[:,1]]-x[tris[:,0]]) * (y[tris[:,2]]-y[tris[:,0]])) - ((x[tris[:,2]]-x[tris[:,0]]) * (y[tris[:,1]]-y[tris[:,0]])) + +# Reorder triangles as necessary so they all have counter clockwise winding order. CCW is what Datashader and MPL +# require. +# +def orderCCW(x,y,tris): + tris[triArea(x,y,tris)<0.0,:] = tris[triArea(x,y,tris)<0.0,::-1] + return(tris) + + From d98f65dcd97b8bc91fa3b5501117af6379d89973 Mon Sep 17 00:00:00 2001 From: ahijevyc Date: Fri, 10 Feb 2023 12:59:44 -0700 Subject: [PATCH 37/68] Clean version of ~ahijevyc/lib/python3.6/mpas.py no reference to atcf --- mpas.py | 94 --------------------------------------------------------- 1 file changed, 94 deletions(-) diff --git a/mpas.py b/mpas.py index f2df0b2..0df0328 100644 --- a/mpas.py +++ b/mpas.py @@ -1,80 +1,10 @@ -import atcf import datetime from fieldinfo import fieldinfo,readNCLcm import logging -from netCDF4 import Dataset import numpy as np import os -import pandas as pd -import pdb import re import sys -import xarray - -def get_diag_name(valid_time, prefix='diag.', suffix='.nc'): - diag_name = prefix + valid_time.strftime("%Y-%m-%d_%H.%M.%S") + suffix - return diag_name - -def raw_vitals(row, diagdir, lonCell, latCell, wind_radii_method=None): - row = row.head(1).squeeze() # make multiple rad lines one series - assert 'originalmeshfile' not in row, f"{row} already has original mesh vitals" - diagfile = get_diag_name(row.valid_time, prefix='diag.', suffix='.nc') - - diagfile = os.path.join(diagdir,diagfile) - logging.debug(f"reading diagfile {diagfile}") - ds = xarray.open_dataset(diagfile) - ds = ds[['u10','v10','mslp']].metpy.quantify() # Don't choke on 'dBZ' not defined in unit registry - ds = ds.isel(Time=0) - - # Extract vmax, RMW, minp, and radii of wind thresholds - derived_winds_dict = atcf.derived_winds(ds.u10, ds.v10, ds.mslp, lonCell, latCell, row, wind_radii_method=wind_radii_method) - row = atcf.unitless_row(derived_winds_dict, row) - - row["originalmeshfile"] = diagfile - # replace the row with (possibly) multiple rows with different wind radii (34/50/64 knot) - row = atcf.add_wind_rad_lines(row, derived_winds_dict["wind_radii"]) - return row - - -def origmesh(df, initfile, diagdir, wind_radii_method="max"): - # assert this is a single track - assert df.groupby(['basin','cy','initial_time','model']).ngroups == 1, 'mpas.origmesh got more than 1 track' - - # Get raw values from MPAS mesh for one track - - # input - # df = pandas Dataframe version of atcf data - # init.nc = path to file with mesh cells lat/lon (first time this is run) - # or a dictionary containing mesh cells lat/lon (faster) - # diagdir = path to directory with diagnostics files. - - - # The first time this is called initfile is a simple string. - # Next time, it is a dictionary with all the needed variables. - if isinstance(initfile, str): - logging.debug(f"reading lat/lon from {initfile}") - init = xarray.open_dataset(initfile) - lonCell = init['lonCell'].metpy.convert_units("degrees") - latCell = init['latCell'].metpy.convert_units("degrees") - lonCell[lonCell >= 180] = lonCell[lonCell >=180] - 360. - init.close() - initfile = { - "initfile":initfile, - "lonCell":lonCell, - "latCell":latCell, - } - else: - logging.debug("reading lat/lon from dictionary") - lonCell = initfile["lonCell"] - latCell = initfile["latCell"] - - # Only groupby 'fhr'. This should already be a single unique track. - df = df.groupby('fhr').apply(raw_vitals,diagdir,lonCell,latCell,wind_radii_method=wind_radii_method) - if "fhr" in df.index.names: # Don't know why it sometimes is not an index - # don't return with fhr as index - df = df.droplevel('fhr') - return df, initfile - # fieldinfo should have been imported from fieldinfo module. # Copy fieldinfo dictionary for MPAS. Change some fnames and filenames. @@ -174,27 +104,3 @@ def makeEnsembleListMPAS(wrfinit, timerange, ENS_SIZE, g193=False): if not file_list['diag']: logging.info('Empty file_list') return (file_list, missing_list) - -# From NCAR geocat example - -# This funtion splits a global mesh along longitude -# -# Examine the X coordinates of each triangle in 'tris'. Return an array of 'tris' where only those triangles -# with legs whose length is less than 't' are returned. -# -def unzipMesh(x,tris,t): - return tris[(np.abs((x[tris[:,0]])-(x[tris[:,1]])) < t) & (np.abs((x[tris[:,0]])-(x[tris[:,2]])) < t)] - -# Compute the signed area of a triangle -# -def triArea(x,y,tris): - return ((x[tris[:,1]]-x[tris[:,0]]) * (y[tris[:,2]]-y[tris[:,0]])) - ((x[tris[:,2]]-x[tris[:,0]]) * (y[tris[:,1]]-y[tris[:,0]])) - -# Reorder triangles as necessary so they all have counter clockwise winding order. CCW is what Datashader and MPL -# require. -# -def orderCCW(x,y,tris): - tris[triArea(x,y,tris)<0.0,:] = tris[triArea(x,y,tris)<0.0,::-1] - return(tris) - - From 0a50dd02dec674c80339001fec0a1d2c4c8375a1 Mon Sep 17 00:00:00 2001 From: ahijevyc Date: Fri, 10 Feb 2023 13:00:50 -0700 Subject: [PATCH 38/68] f2py3 -c mpas_vort_cell.f90 -m mpas_vort_cell First had to fix .c file in numpy.f2py line 707 for (int i = 0 ------- int i; for (i = 0 --- ..._vort_cell.cpython-310-x86_64-linux-gnu.so | Bin 0 -> 47664 bytes mpas_vort_cell.f90 | 27 ++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100755 mpas_vort_cell.cpython-310-x86_64-linux-gnu.so create mode 100644 mpas_vort_cell.f90 diff --git a/mpas_vort_cell.cpython-310-x86_64-linux-gnu.so b/mpas_vort_cell.cpython-310-x86_64-linux-gnu.so new file mode 100755 index 0000000000000000000000000000000000000000..b5c4e0daef5dc08e31ac2392554d65dc5861c788 GIT binary patch literal 47664 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 sD{)Ky(tlI`OvsM7yt?n_vDah)ks Date: Sat, 11 Feb 2023 13:24:05 -0700 Subject: [PATCH 39/68] use xarray, not netCDF4 use logging list of forecast hours, not start and end hour TODO: get different domains to work --- fieldinfo.py | 2 + make_webplot.py | 30 ++-- mpas.py | 12 +- webplot.py | 405 ++++++++++++++++++++++-------------------------- 4 files changed, 209 insertions(+), 240 deletions(-) diff --git a/fieldinfo.py b/fieldinfo.py index 6518fb9..17b2b32 100755 --- a/fieldinfo.py +++ b/fieldinfo.py @@ -101,6 +101,7 @@ def readNCLcm(name): # RAS: adjusted these ranges - need to capture higher wind speeds 'speed200' :{ 'levels' : [25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100,105,110], 'cmap': readNCLcm('wind_17lev'), 'fname': ['uzonal_200hPa','umeridional_200hPa'], 'filename':'diag'}, 'speed250' :{ 'levels' : [25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100,105,110], 'cmap': readNCLcm('wind_17lev'), 'fname': ['uzonal_250hPa','umeridional_250hPa'], 'filename':'diag'}, + 'speed300' :{ 'levels' : [25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100,105,110], 'cmap': readNCLcm('wind_17lev'), 'fname': ['uzonal_300hPa','umeridional_300hPa'], 'filename':'diag'}, 'speed500' :{ 'levels' : [15,20,25,30,35,40,45,50,55,60,65,70,75,80,85,90,95,100], 'cmap': readNCLcm('wind_17lev'), 'fname': ['uzonal_500hPa','umeridional_500hPa'], 'filename':'diag'}, 'speed700' :{ 'levels' : [5,10,15,20,25,30,35,40,45,50,55,60,65,70,75,80,85], 'cmap': readNCLcm('wind_17lev'), 'fname': ['uzonal_700hPa','umeridional_700hPa'], 'filename':'diag'}, 'speed850' :{ 'levels' : [6,10,14,18,22,26,30,34,38,42,46,50,54,58,62,66,70,74], 'cmap': readNCLcm('wind_17lev'), 'fname': ['uzonal_850hPa','umeridional_850hPa'], 'filename':'diag'}, @@ -112,6 +113,7 @@ def readNCLcm(name): 'temp700' :{ 'levels' : [-36,-33,-30,-27,-24,-21,-18,-15,-12,-9,-6,-3,0,3,6,9,12,15,18,21], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['temperature_700hPa'], 'filename':'diag'}, 'temp850' :{ 'levels' : [-30,-27,-24,-21,-18,-15,-12,-9,-6,-3,0,3,6,9,12,15,18,21,24,27,30], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['temperature_850hPa'], 'filename':'diag'}, 'temp925' :{ 'levels' : [-24,-21,-18,-15,-12,-9,-6,-3,0,3,6,9,12,15,18,21,24,27,30,33], 'cmap': readNCLcm('nice_gfdl')[3:193], 'fname': ['temperature_925hPa'], 'filename':'diag'}, + 'td500' :{ 'levels' : [-30,-25,-20,-15,-10,-5,0,5,10], 'cmap' : readNCLcm('nice_gfdl')[3:193], 'fname': ['dewpoint_500hPa'], 'filename':'diag'}, 'td700' :{ 'levels' : [-30,-25,-20,-15,-10,-5,0,5,10], 'cmap' : readNCLcm('nice_gfdl')[3:193], 'fname': ['dewpoint_700hPa'], 'filename':'diag'}, 'td850' :{ 'levels' : [-40,-35,-30,-25,-20,-15,-10,-5,0,5,10,15,20,25,30], 'cmap' : readNCLcm('nice_gfdl')[3:193], 'fname': ['dewpoint_850hPa'], 'filename':'diag'}, 'td925' :{ 'levels' : [-30,-25,-20,-15,-10,-5,0,5,10,15,20,25,30], 'cmap' : readNCLcm('nice_gfdl')[3:193], 'fname': ['dewpoint_925hPa'], 'filename':'diag'}, diff --git a/make_webplot.py b/make_webplot.py index 6fbdede..2441879 100755 --- a/make_webplot.py +++ b/make_webplot.py @@ -1,21 +1,24 @@ -#!/usr/bin/env python - -import sys, time, os +import logging +import os +import pdb +import sys +import time from webplot import webPlot, readGrid, saveNewMap -def log(msg): print(time.ctime(time.time()),':', msg) - -log('Begin Script'); stime = time.time() +logging.info('Begin Script') +stime = time.time() regions = ['CONUS', 'NGP', 'SGP', 'CGP', 'MATL', 'NE', 'NW', 'SE', 'SW'] -if not os.path.exists('picklefilename.pk'): - saveNewMap(wrfout='wrfout_file_containing_lat_lons', domstr='name_for_domain') newPlot = webPlot() -log('Reading Data'); newPlot.readEnsemble() +logging.info('Reading Data') +newPlot.readEnsemble() for dom in regions: + pk_file = newPlot.pk_file + if not os.path.exists(pk_file): + saveNewMap(newPlot, wrfout='latlonfile.nc', init_file='/glade/campaign/mmm/parc/ahijevyc/MPAS/uni/2018103000/init.nc') file_not_created, num_attempts = True, 0 while file_not_created and num_attempts <= 3: newPlot.domain = dom @@ -23,25 +26,24 @@ def log(msg): print(time.ctime(time.time()),':', msg) newPlot.createFilename() fname = newPlot.outfile - log('Loading Map for %s'%newPlot.domain) newPlot.loadMap() - log('Plotting Data') + logging.info('Plotting Data') if newPlot.opts['interp']: newPlot.plotInterp() else: newPlot.plotFields() newPlot.plotTitleTimes() - log('Writing Image') + logging.info('Writing Image') newPlot.saveFigure(trans=newPlot.opts['over']) if os.path.exists(fname): file_not_created = False - log('Created %s, %.1f KB'%(fname,os.stat(fname).st_size/1000.0)) + logging.info(f'Created {fname} {os.stat(fname).st_size/1000:.1f} KB') num_attempts += 1 etime = time.time() -log('End Plotting (took %.2f sec)'%(etime-stime)) +logging.info(f'End Plotting (took {etime-stime:.2f} sec)') diff --git a/mpas.py b/mpas.py index 0df0328..cff81e1 100644 --- a/mpas.py +++ b/mpas.py @@ -56,12 +56,9 @@ for plev in ['200', '250','300','500','700','850','925']: fieldinfo['hgt'+plev]['fname'] = ['height_'+plev+'hPa'] fieldinfo['speed'+plev]['fname'] = ['uzonal_'+plev+'hPa','umeridional_'+plev+'hPa'] - del fieldinfo['speed'+plev]['arraylevel'] fieldinfo['temp'+plev]['fname'] = ['temperature_'+plev+'hPa'] - del fieldinfo['temp'+plev]['arraylevel'] for plev in ['500', '700', '850']: fieldinfo['td'+plev]['fname'] = ['dewpoint_'+plev+'hPa'] - del fieldinfo['td'+plev]['arraylevel'] fieldinfo['vort'+plev] = {'levels' : [0,9,12,15,18,21,24,27,30,33], 'cmap': readNCLcm('prcp_1'), 'fname': ['vorticity_'+plev+'hPa'], 'filename':'diag'} fieldinfo['vortpv'] = {'levels' : [0,9,12,15,18,21,24,27,30,33], 'cmap': readNCLcm('prcp_1'), 'fname': ['vort_pv'], 'filename':'diag'} for plev in ['300', '500', '700', '850', '925']: @@ -81,19 +78,18 @@ fieldinfo['wind'+plev] = { 'fname' : ['uzonal_'+plev+'hPa', 'umeridional_'+plev+'hPa'], 'filename':'diag', 'skip':50} -def makeEnsembleListMPAS(wrfinit, timerange, ENS_SIZE, g193=False): +def makeEnsembleListMPAS(wrfinit, fhr, ENS_SIZE, g193=False): # create lists of files (and missing file indices) for various file types - shr, ehr = timerange file_list = { 'wrfout':[], 'diag':[] } missing_list = { 'wrfout':[], 'diag':[] } missing_index = 0 - for hr in range(shr,ehr+1): + for hr in fhr: wrfvalidstr = (wrfinit + datetime.timedelta(hours=hr)).strftime('%Y-%m-%d_%H.%M.%S') yyyymmddhh = wrfinit.strftime('%Y%m%d%H') for mem in range(1,ENS_SIZE+1): - diag = '/glade/p/nsc/nmmm0046/schwartz/MPAS_ens_15-3km_mesh/POST/%s/ens_%d/diag.%s.nc'%(yyyymmddhh,mem,wrfvalidstr) + diag = '/glade/p/mmm/parc/schwartz/MPAS_ens_15-3km_mesh/POST/%s/ens_%d/diag.%s.nc'%(yyyymmddhh,mem,wrfvalidstr) if g193: - diag = '/glade/p/nsc/nmmm0046/schwartz/MPAS_ens_15-3km_mesh/POST/%s/ens_%d/diag_latlon_g193.%s.nc'%(yyyymmddhh,mem,wrfvalidstr) + diag = '/glade/p/mmm/parc/schwartz/MPAS_ens_15-3km_mesh/POST/%s/ens_%d/diag_latlon_g193.%s.nc'%(yyyymmddhh,mem,wrfvalidstr) logging.debug(diag) if os.path.exists(diag): file_list['diag'].append(diag) else: diff --git a/webplot.py b/webplot.py index 7332672..db9349e 100755 --- a/webplot.py +++ b/webplot.py @@ -1,43 +1,46 @@ +import argparse +from collections import defaultdict +import datetime +from fieldinfo import * +import logging import matplotlib.colors as colors import matplotlib.pyplot as plt +from mpas import fieldinfo, makeEnsembleListMPAS +import mpas_vort_cell from mpl_toolkits.basemap import * -from datetime import * +import pandas as pd +import pdb import pickle -import os, sys, time, argparse +import os import scipy.ndimage as ndimage from scipy import interpolate from scipy.spatial import qhull import subprocess -import mpas_vort_cell import re -import pdb -from fieldinfo import * -print("importing mpas module. remove 'import mpas' to do some other model") -from mpas import fieldinfo, makeEnsembleListMPAS -# To use MFDataset, first convert netcdf4 to netcdf4-classic with nccopy -# for example, nccopy -d nc7 /glade/p/nsc/nmmm0046/schwartz/MPAS_ens_15-3km_mesh/POST/2017050100/ens_1/diag_latlon_g193.2017-05-02_00.00.00.nc /glade/scratch/ahijevyc/hwt2017/2017050100/ens_1/diag_latlon_g193.2017-05-02_00.00.00.nc -from netCDF4 import Dataset, MFDataset -# xarray can handle muliple files with NETCDF4 non-classic. netCDF4 MFDataset can't do that. +import sys +import time import xarray -def log(msg): print(time.ctime(time.time()),':', msg) +logging.basicConfig(level=logging.INFO) class webPlot: '''A class to plot data from NCAR ensemble''' def __init__(self): self.opts = parseargs() - self.initdate = datetime.strptime(self.opts['date'], '%Y%m%d%H') + self.initdate = pd.to_datetime(self.opts['date']) self.title = self.opts['title'] self.debug = self.opts['debug'] self.autolevels = self.opts['autolevels'] self.domain = self.opts['domain'] - self.mesh= self.opts['mesh'] - if ',' in self.opts['timerange']: self.shr, self.ehr = list(map(int, self.opts['timerange'].split(','))) - else: self.shr, self.ehr = int(self.opts['timerange']), int(self.opts['timerange']) + self.fhr = self.opts['fhr'] + self.mesh = self.opts['mesh'] + self.nlat_max = self.opts['nlat_max'] + self.nlon_max = self.opts['nlon_max'] + self.pk_file = os.path.join(os.getenv('TMPDIR'), f"{self.mesh}_{self.domain}_{self.nlon_max}x{self.nlat_max}.pk") self.createFilename() - self.ENS_SIZE = int(os.getenv('ENS_SIZE', 10)) + self.ENS_SIZE = self.opts['ENS_SIZE'] - def createFilename(self): + def createFilename(self, prefx=''): for f in ['fill', 'contour','barb']: # CSS added this for loop and everything in it if 'name' in self.opts[f]: if 'thresh' in self.opts[f]: @@ -46,24 +49,24 @@ def createFilename(self): prefx = self.opts[f]['name']+'_'+self.opts[f]['ensprod'] # CSS break - if self.shr == self.ehr: # CSS - self.outfile = prefx+'_f'+'%03d'%self.shr+'_'+self.domain+'.png' # 'test.png' # CSS + shr = min(self.fhr) + ehr = max(self.fhr) + if len(self.fhr) == 1: + self.outfile = f"{prefx}_f{shr:03.0f}_{self.domain}.png" else: # CSS - #self.outfile = prefx+'_f'+'%03d'%self.shr+'-f'+'%03d'%self.ehr+'_'+self.domain+'_'+self.opts['date']+'.png' # 'test.png' # CSS - self.outfile = prefx+'_f'+'%03d'%self.shr+'-f'+'%03d'%self.ehr+'_'+self.domain+'.png' # 'test.png' # CSS + self.outfile = f"{prefx}_f{shr:03.0f}-f{ehr:03.0f}_{self.domain}.png" # create yyyymmddhh/domain/ directory if needed - subdir_path = os.path.join(self.opts['date'], self.domain) + subdir_path = os.path.join(os.getenv('TMPDIR'), self.opts['date'], self.domain) if not os.path.isdir(subdir_path): - print("webPlot.createFilename(): making new output directory "+subdir_path) - os.mkdir(subdir_path) + logging.warning(f"webPlot.createFilename(): making new output directory {subdir_path}") + os.makedirs(subdir_path) # prepend subdir_path to outfile. self.outfile = os.path.join(subdir_path, self.outfile) - def toServer(self, debug=False, url="nova.mmm.ucar.edu:/web/htdocs/projects/mpas/plots/."): - rsync_opts = '-RL' - if debug: - rsync_opts += 'v' # append a 'v' for verbose + + def toServer(self, url="nova.mmm.ucar.edu:/web/htdocs/projects/mpas/plots/."): + rsync_opts = '-RLv' #result = subprocess.run(['rsync', rsync_opts, '--timeout=10', '--bwlimit=3', self.outfile, url], check=True, stdout=subprocess.PIPE) # tried capture_output=True but this version of subprocess doesn't recognize it. # send directly to koa instead of nova. Carter set up ssh keys. Not working. Get return status=14 result = subprocess.run(['rsync', '-e', "'ssh -vi /home/ahijevyc/.ssh/id_rsa'", '-avR', self.outfile, url], check=True, stdout=subprocess.PIPE) @@ -71,21 +74,16 @@ def toServer(self, debug=False, url="nova.mmm.ucar.edu:/web/htdocs/projects/mpas if result.returncode != 0: print(result) - def loadMap(self, nlon_max=1500, nlat_max=1500, overlay=False): - if hasattr(self, 'domain'): # once called with empty junk webplot class, just to get lats/lons/x/y/ibox attributes - if overlay: - PYTHON_SCRIPTS_DIR = os.getenv('PYTHON_SCRIPTS_DIR', '/glade/u/home/sobash/RT2015_gpx') - self.fig, self.ax, self.m = pickle.load(open('%s/overlays/rt2015_overlay_%s.pk'%(PYTHON_SCRIPTS_DIR,self.domain), 'r')) - else: - PYTHON_SCRIPTS_DIR = os.getenv('PYTHON_SCRIPTS_DIR', '/glade/work/ahijevyc/share/rt_ensemble/python_scripts') - pklfile = '%s/%s_%s_%dx%d.pk'%(PYTHON_SCRIPTS_DIR,self.mesh,self.domain,nlon_max,nlat_max) - print("loading "+pklfile) - self.fig, self.ax, self.m, self.lons,self.lats,self.min_grid_spacing_km,self.delta_deg,self.lon2d,self.lat2d,self.x2d,self.y2d,self.ibox,self.x,self.y,self.vtx,self.wts = pickle.load(open(pklfile, 'rb')) + def loadMap(self, overlay=False): + logging.info(f"loadMap {self.pk_file}") + (self.fig, self.ax, self.m, self.lons, self.lats, self.min_grid_spacing_km, self.delta_deg, + self.lon2d, self.lat2d, self.x2d, self.y2d, self.ibox, self.x, self.y, self.vtx, + self.wts) = pickle.load(open(self.pk_file, 'rb')) def readEnsemble(self): if hasattr(self, 'data') and hasattr(self, 'missing_members') and 'must_reread_ensemble' not in self.data: return - self.data, self.missing_members = readEnsemble(self.initdate, self.domain, timerange=[self.shr,self.ehr], fields=self.opts, debug=self.debug, ENS_SIZE=self.ENS_SIZE, mesh=self.mesh) + self.data, self.missing_members = readEnsemble(self.initdate, self.domain, fhr=self.fhr, fields=self.opts, ENS_SIZE=self.ENS_SIZE, mesh=self.mesh) def plotDepartures(self): from collections import OrderedDict @@ -235,15 +233,13 @@ def plotMRMS(self, thresh=40.0): validstr = (self.initdate+timedelta(hours=self.shr)).strftime('%Y%m%d%H') # READ IN MRMS GRID - fh = Dataset('/glade/p/nmmm0001/sobash/MRMS/mrms_grid.nc', 'r') - lats = fh.variables['lat_0'][:] - lons = fh.variables['lon_0'][:] - fh.close() + fh = xarray.open_dataset('/glade/p/nmmm0001/sobash/MRMS/mrms_grid.nc') + lats = fh['lat_0'] + lons = fh['lon_0'] # READ IN DATA - fh = Dataset('/glade/scratch/sobash/mrms_tmp/cref_mrms_%s.nc'%validstr, 'r') - mrms = fh.variables['CREF'][:] - fh.close() + fh = xarray.open_dataset('/glade/scratch/sobash/mrms_tmp/cref_mrms_%s.nc'%validstr) + mrms = fh['CREF'] lats, lons = np.meshgrid(lats, lons) x, y = self.m(lons, lats) @@ -259,10 +255,9 @@ def plotMRMSmax(self, field='qpe'): #validstr = (self.initdate+timedelta(hours=self.shr-1)).strftime('%Y%m%d00') # READ IN MRMS GRID - fh = Dataset('/glade/p/nmmm0001/sobash/MRMS/mrms_grid.nc', 'r') - lats = fh.variables['lat_0'][:] - lons = fh.variables['lon_0'][:] - fh.close() + fh = xarray.open_dataset('/glade/p/nmmm0001/sobash/MRMS/mrms_grid.nc') + lats = fh['lat_0'] + lons = fh['lon_0'] # READ IN DATA if field == 'qpe': @@ -274,9 +269,8 @@ def plotMRMSmax(self, field='qpe'): field = 'CREF' levels = [40,1000] - fh = Dataset(fname, 'r') - mrms = fh.variables[field][0,:] - fh.close() + fh = xarray.open_dataset(fname) + mrms = fh[field] # plot lats, lons = np.meshgrid(lats, lons) @@ -321,7 +315,7 @@ def plotFields(self): if self.opts['fill']['name'] in ['t2depart', 'td2depart']: self.plotDepartures() - def plotFill(self, debug=False): + def plotFill(self): if self.opts['fill']['name'] == 'ptype': self.plotFill_ptype(); return if self.opts['fill']['name'][0:6] == 'ptypes': self.plotFill_ptypes(); return elif self.opts['fill']['name'] == 'crefuh': self.plotReflectivityUH(); return @@ -363,8 +357,7 @@ def plotFill(self, debug=False): #print("plotFill: starting contourf with ",self.ibox.shape," array "+self.opts['fill']['name']) #cs1 = self.m.contourf(self.x, self.y, self.data['fill'][0][self.ibox], tri=True, levels=levels, cmap=cmap, norm=norm, extend='max', ax=self.ax) #MPAS data = self.latlonGrid(self.data['fill'][0]) - if debug: - print("plotFill: starting contourf with 2d array") + logging.debug("plotFill: starting contourf with 2d array") cs1 = self.m.contourf(self.x2d, self.y2d, data, levels=levels, cmap=cmap, norm=norm, extend='max', ax=self.ax) else: # Added for HRRR @@ -468,6 +461,7 @@ def plotColorbar(self, cs, levels, tick_labels, extend='neither', extendfrac=0.0 cb.outline.set_linewidth(0.5) def interpolatetri(self, values, vtx, wts): + logging.info("interpolatetri") return np.einsum('nj,nj->n', np.take(values, vtx), wts) def latlonGrid(self, data): @@ -475,9 +469,10 @@ def latlonGrid(self, data): data = data[self.ibox] if hasattr(self, "vtx") and hasattr(self, "wts"): data_gridded = self.interpolatetri(data, self.vtx, self.wts) + logging.info("reshape") data_gridded = np.reshape(data_gridded, self.lat2d.shape) else: - print("latlonGrid: interpolating to latlon grid with griddata()") + logging.info("latlonGrid: interpolating to latlon grid with griddata()") data_gridded = interpolate.griddata((self.lons, self.lats), data, (self.lon2d, self.lat2d), method='nearest') return data_gridded @@ -486,8 +481,6 @@ def plotContour(self): if self.opts['contour']['name'] in ['sbcinh','mlcinh']: linewidth, alpha = 0.5, 0.75 else: linewidth, alpha = 1.5, 1.0 data = self.data['contour'][0] - if self.debug: - pdb.set_trace() if self.vtx is not None: # Interpolate to latlon Grid data = self.latlonGrid(data) if self.opts['contour']['name'] in ['t2-0c']: data = ndimage.gaussian_filter(data, sigma=2) @@ -496,7 +489,7 @@ def plotContour(self): cs2 = self.m.contour(self.x2d, self.y2d, data, levels=self.opts['contour']['levels'], colors='k', linewidths=linewidth, ax=self.ax, alpha=alpha) plt.clabel(cs2, fontsize='small', fmt='%i') - def plotBarbs(self, debug=False): + def plotBarbs(self): skip = self.opts['barb']['skip'] if self.domain != 'CONUS': skip = int(skip*0.45) if self.domain == 'NA': skip = int(skip*2) @@ -505,7 +498,7 @@ def plotBarbs(self, debug=False): else: alpha=1.0 - if self.debug: print("plotBarbs: starting barbs") + logging.debug("plotBarbs: starting barbs") # skip interval was intended for 2-D fields if len(self.x.shape) == 2: cs2 = self.m.barbs(self.x[::skip,::skip], self.y[::skip,::skip], self.data['barb'][0][::skip,::skip], self.data['barb'][1][::skip,::skip], color='black', alpha=alpha, length=5.5, linewidth=0.25, sizes={'emptybarb':0.05}, ax=self.ax) @@ -576,8 +569,7 @@ def plotStamp(self): w, h = (width_per_panel/float(fig_width))-spacing_w, (height_per_panel/float(fig_height))-spacing_h if member == 9: y = 0 - if self.debug: - print('member', member, 'creating axes at', x, y) + logging.debug(f'member {member} creating axes at {x},{y}') thisax = fig.add_axes([x,y,w,h]) thisax.axis('on') @@ -590,8 +582,7 @@ def plotStamp(self): # plot, unless file that has fill field is missing, then skip if member not in self.missing_members[filename] and member < self.ENS_SIZE: data = self.latlonGrid(self.data['fill'][0][memberidx,:]) - if self.debug: - print("plotStamp: starting contourf with regridded array", memberidx) + logging.debug(f"plotStamp: starting contourf with regridded array {memberidx}") cs1 = self.m.contourf(self.x2d, self.y2d, data, levels=levels, cmap=cmap, norm=norm, extend='max', ax=thisax) memberidx += 1 @@ -621,17 +612,19 @@ def plotStamp(self): def getInitValidStr(self): initstr = self.initdate.strftime(' Init: %a %Y-%m-%d %H UTC') - if ((self.ehr - self.shr) == 0): - validstr = (self.initdate+timedelta(hours=self.shr)).strftime('Valid: %a %Y-%m-%d %H UTC') + if len(self.fhr) == 1: + validstr = (self.initdate+datetime.timedelta(hours=self.fhr)).strftime('Valid: %a %Y-%m-%d %H UTC') else: + shr = min(self.fhr) + ehr = max(self.fhr) # match precip or precip-24hr, but not precipacc # accept precip-24hr, precip-48hr, precip-120hr, etc. if self.opts['fill']['name'] == 'precip' or is_precip_diff(self.opts['fill']['name']): # do not subtract 1 from start hour if array is difference of accumulated precipitation - validstr1 = (self.initdate+timedelta(hours=(self.shr))).strftime('%a %Y-%m-%d %H UTC') + validstr1 = (self.initdate + datetime.timedelta(hours=shr)).strftime('%a %Y-%m-%d %H UTC') else: - validstr1 = (self.initdate+timedelta(hours=(self.shr-1))).strftime('%a %Y-%m-%d %H UTC') - validstr2 = (self.initdate+timedelta(hours=self.ehr)).strftime('%a %Y-%m-%d %H UTC') + validstr1 = (self.initdate + datetime.timedelta(hours=shr-1)).strftime('%a %Y-%m-%d %H UTC') + validstr2 = (self.initdate+datetime.timedelta(hours=ehr)).strftime('%a %Y-%m-%d %H UTC') validstr = "Valid: %s - %s"%(validstr1, validstr2) return initstr, validstr @@ -640,7 +633,7 @@ def saveFigure(self, trans=False): if 'ensprod' in self.opts['fill']: # CSS needed incase not a fill plot if not trans and self.opts['fill']['ensprod'] not in ['stamp', 'maxstamp']: x, y = self.ax.transAxes.transform((0,0)) - self.fig.figimage(plt.imread('ncar.png'), xo=x, yo=(y-44), zorder=1000) + #self.fig.figimage(plt.imread('ncar.png'), xo=x, yo=(y-44), zorder=1000) plt.text(x+10, y-54, 'ensemble.ucar.edu', fontdict={'size':9, 'color':'#505050'}, transform=None) plt.savefig(self.outfile, dpi=90, transparent=trans) @@ -664,15 +657,19 @@ def parseargs(): '''Parse arguments and return dictionary of fill, contour and barb field parameters''' parser = argparse.ArgumentParser(description='Web plotting script for NCAR ensemble') - parser.add_argument('-d', '--date', default=datetime.utcnow().strftime('%Y%m%d00'), help='initialization datetime (YYYYMMDDHH)') - parser.add_argument('-tr', '--timerange', required=True, help='time range of forecasts (START,END)') + parser.add_argument('-d', '--date', default=datetime.datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0), + help='initialization datetime') + parser.add_argument('--fhr', nargs='+', type=float, default=[12], help='list of forecast hours') parser.add_argument('-f', '--fill', help='fill field (FIELD_PRODUCT_THRESH), field keys:'+','.join(list(fieldinfo.keys()))) parser.add_argument('-c', '--contour', help='contour field (FIELD_PRODUCT_THRESH)') parser.add_argument('-b', '--barb', help='barb field (FIELD_PRODUCT_THRESH)') parser.add_argument('-bs', '--barbskip', help='barb skip interval') - parser.add_argument('-t', '--title', help='title for plot') parser.add_argument('-dom', '--domain', default='CONUS', help='domain to plot') - parser.add_argument('-m', '--mesh', default='rt2015', choices=['mpas','rt2015','hrrr'], help='mesh') + parser.add_argument('--ENS_SIZE', type=int, default=10, help='ensemble size') + parser.add_argument('-t', '--title', help='title for plot') + parser.add_argument('--nlon_max', default=1500, help='max pts in longitude dimension') + parser.add_argument('--nlat_max', default=1500, help='max pts in latitude dimension') + parser.add_argument('-m', '--mesh', default='rt2015', choices=['mpas','rt2015','hrrr','uni'], help='mesh') parser.add_argument('-al', '--autolevels', action='store_true', help='use min/max to determine levels for plot') parser.add_argument('-con', '--convert', default=True, action='store_false', help='run final image through imagemagick') parser.add_argument('-i', '--interp', default=False, action='store_true', help='plot interpolated station values') @@ -691,6 +688,8 @@ def parseargs(): if opts[f] is not None: input = opts[f].lower().split('_') + assert len(input) > 1, f"{f} has 2-3 components separated by _. Add '_mean'?" + thisdict['name'] = input[0] thisdict['ensprod'] = input[1] thisdict['arrayname'] = fieldinfo[input[0]]['fname'] @@ -739,17 +738,16 @@ def parseargs(): opts[f] = thisdict return opts -def makeEnsembleList(wrfinit, timerange, ENS_SIZE): +def makeEnsembleList(wrfinit, fhr, ENS_SIZE): # create lists of files (and missing file indices) for various file types - shr, ehr = timerange file_list = { 'wrfout':[], 'upp': [], 'diag':[] } missing_list = { 'wrfout':[], 'upp': [], 'diag':[] } EXP_DIR = os.getenv('EXP_DIR', '/glade/scratch/wrfrt/realtime_ensemble/ensf') missing_index = 0 - for hr in range(shr,ehr+1): - wrfvalidstr = (wrfinit + timedelta(hours=hr)).strftime('%Y-%m-%d_%H:%M:%S') + for hr in fhr: + wrfvalidstr = (wrfinit + datetime.timedelta(hours=hr)).strftime('%Y-%m-%d_%H:%M:%S') yyyymmddhh = wrfinit.strftime('%Y%m%d%H') for mem in range(1,ENS_SIZE+1): wrfout = '%s/%s/wrf_rundir/ens_%d/wrfout_d02_%s'%(EXP_DIR,yyyymmddhh,mem,wrfvalidstr) @@ -767,41 +765,36 @@ def makeEnsembleList(wrfinit, timerange, ENS_SIZE): missing_index += 1 return (file_list, missing_list) -def makeEnsembleListStan(wrfinit, timerange, ENS_SIZE): +def makeEnsembleListStan(wrfinit, fhr, ENS_SIZE): # create lists of files (and missing file indices) for various file types - shr, ehr = timerange file_list = { 'wrfout':[], 'upp': [], 'diag':[] } missing_list = { 'wrfout':[], 'upp': [], 'diag':[] } EXP_DIR = os.getenv('EXP_DIR', '/glade/scratch/trier/jun4-5/') missing_index = 0 - #for hr in range(shr,ehr+1): - print(shr, ehr) - for m in range(shr*60,(ehr*60)+1,15): - #wrfvalidstr = (wrfinit + timedelta(hours=m)).strftime('%Y-%m-%d_%H:%M:%S') - wrfvalidstr = (wrfinit + timedelta(minutes=m)).strftime('%Y-%m-%d_%H:%M:%S') - yyyymmddhh = wrfinit.strftime('%Y%m%d%H') - for mem in range(1,ENS_SIZE+1): - wrfout = '%s/ens_%d/wrfout_d02_%s'%(EXP_DIR,mem,wrfvalidstr) - diag = '%s/ens_%d/diags_d02.%s.nc'%(EXP_DIR,mem,wrfvalidstr) - print(diag) - if os.path.exists(wrfout): file_list['wrfout'].append(wrfout) - else: missing_list['wrfout'].append(missing_index) - if os.path.exists(diag): file_list['diag'].append(diag) - else: missing_list['diag'].append(missing_index) - missing_index += 1 + for h in fhr: + wrfvalidstr = (wrfinit + datetime.timedelta(hours=h)).strftime('%Y-%m-%d_%H:%M:%S') + yyyymmddhh = wrfinit.strftime('%Y%m%d%H') + for mem in range(1,ENS_SIZE+1): + wrfout = '%s/ens_%d/wrfout_d02_%s'%(EXP_DIR,mem,wrfvalidstr) + diag = '%s/ens_%d/diags_d02.%s.nc'%(EXP_DIR,mem,wrfvalidstr) + print(diag) + if os.path.exists(wrfout): file_list['wrfout'].append(wrfout) + else: missing_list['wrfout'].append(missing_index) + if os.path.exists(diag): file_list['diag'].append(diag) + else: missing_list['diag'].append(missing_index) + missing_index += 1 return (file_list, missing_list) -def makeEnsembleListHREF(wrfinit, timerange, ENS_SIZE): +def makeEnsembleListHREF(wrfinit, fhr, ENS_SIZE): # create lists of files (and missing file indices) for various file types - shr, ehr = timerange file_list = { 'wrfout':[], 'diag':[] } missing_list = { 'wrfout':[], 'diag':[] } missing_index = 0 - wrfinit_prev = (wrfinit - timedelta(hours=12)) + wrfinit_prev = (wrfinit - datetime.timedelta(hours=12)) - for hr in range(shr,ehr+1): + for hr in fhr: yyyymmddhh = wrfinit.strftime('%Y%m%d%H') yyyymmddhh_p = wrfinit_prev.strftime('%Y%m%d%H') hr_p = hr - 12 @@ -827,14 +820,13 @@ def makeEnsembleListHREF(wrfinit, timerange, ENS_SIZE): return (file_list, missing_list) -def makeEnsembleListHRRR(wrfinit, timerange, ENS_SIZE): +def makeEnsembleListHRRR(wrfinit, fhr, ENS_SIZE): # create lists of files (and missing file indices) for various file types - shr, ehr = timerange file_list = { 'diag':[], 'wrfout':[]} missing_list = { 'diag':[], 'wrfout':[]} missing_index = 0 - for hr in range(shr,ehr+1): + for hr in fhr: yyyymmddhh = wrfinit.strftime('%Y%m%d%H') yyyymmdd = wrfinit.strftime('%Y%m%d') init = wrfinit.strftime('%H') @@ -850,9 +842,8 @@ def makeEnsembleListHRRR(wrfinit, timerange, ENS_SIZE): return (file_list, missing_list) -def makeEnsembleListArchive(wrfinit, timerange): +def makeEnsembleListArchive(wrfinit): # create lists of files (and missing file indices) for various file types - shr, ehr = timerange file_list = { 'wrfout':[], 'upp': [], 'diag':[] } missing_list = { 'wrfout':[], 'upp': [], 'diag':[] } @@ -871,17 +862,16 @@ def makeEnsembleListArchive(wrfinit, timerange): missing_index += 1 return (file_list, missing_list) -def makeEnsembleListNSC(wrfinit, timerange): +def makeEnsembleListNSC(wrfinit, fhr): # create lists of files (and missing file indices) for various file types - shr, ehr = timerange file_list = { 'wrfout':[], 'diag':[] } missing_list = { 'wrfout':[], 'diag':[] } EXP_DIR = os.getenv('EXP_DIR', '/glade/scratch/wrfrt/realtime_ensemble/ensf') missing_index = 0 - for hr in range(shr,ehr+1): - wrfvalidstr = (wrfinit + timedelta(hours=hr)).strftime('%Y-%m-%d_%H_%M_%S') + for hr in fhr: + wrfvalidstr = (wrfinit + datetime.timedelta(hours=hr)).strftime('%Y-%m-%d_%H_%M_%S') yyyymmddhh = wrfinit.strftime('%Y%m%d%H') for mem in range(1,2): diag = '%s/%s/diags_d01_%s.nc'%(EXP_DIR,yyyymmddhh,wrfvalidstr) @@ -891,9 +881,8 @@ def makeEnsembleListNSC(wrfinit, timerange): missing_index += 1 return (file_list, missing_list) -def makeEnsembleListDA(wrfinit, timerange): +def makeEnsembleListDA(wrfinit, fhr): # create lists of files (and missing file indices) for various file types - shr, ehr = timerange file_list = { 'wrfout':[], 'diag':[] } missing_list = { 'wrfout':[], 'diag':[] } @@ -901,46 +890,50 @@ def makeEnsembleListDA(wrfinit, timerange): #EXP_DIR = os.getenv('EXP_DIR', '/glade/scratch/sobash/VSE/1km_pbl7') EXP_DIR = os.getenv('EXP_DIR', '/glade/scratch/schwartz/VSE/3km_pbl7') missing_index = 0 - for hr in range(shr,ehr+1): - wrfvalidstr = (wrfinit + timedelta(hours=hr)).strftime('%Y-%m-%d_%H:%M:%S') - yyyymmddhh = wrfinit.strftime('%Y%m%d%H') - for mem in range(1,2): - wrfout = '%s/%s/wrfout_d01_%s'%(EXP_DIR,yyyymmddhh,wrfvalidstr) - #diag = '%s/%s/wrf/join/vse_d01.%s.nc'%(EXP_DIR,yyyymmddhh,wrfvalidstr) - diag = '%s/%s/wrf/diags_d01.%s.nc'%(EXP_DIR,yyyymmddhh,wrfvalidstr) - print(diag) - if os.path.exists(wrfout): file_list['wrfout'].append(wrfout) - else: missing_list['wrfout'].append(missing_index) - if os.path.exists(diag): file_list['diag'].append(diag) - else: missing_list['diag'].append(missing_index) - missing_index += 1 + for hr in fhr: + wrfvalidstr = (wrfinit + datetime.timedelta(hours=hr)).strftime('%Y-%m-%d_%H:%M:%S') + yyyymmddhh = wrfinit.strftime('%Y%m%d%H') + for mem in range(1,2): + wrfout = '%s/%s/wrfout_d01_%s'%(EXP_DIR,yyyymmddhh,wrfvalidstr) + #diag = '%s/%s/wrf/join/vse_d01.%s.nc'%(EXP_DIR,yyyymmddhh,wrfvalidstr) + diag = '%s/%s/wrf/diags_d01.%s.nc'%(EXP_DIR,yyyymmddhh,wrfvalidstr) + print(diag) + if os.path.exists(wrfout): file_list['wrfout'].append(wrfout) + else: missing_list['wrfout'].append(missing_index) + if os.path.exists(diag): file_list['diag'].append(diag) + else: missing_list['diag'].append(missing_index) + missing_index += 1 return (file_list, missing_list) -def readEnsemble(wrfinit, domain, timerange=None, fields=None, debug=False, ENS_SIZE=10, mesh=None): +def readEnsemble(wrfinit, domain, fhr=None, fields=None, ENS_SIZE=10, mesh=None): ''' Reads in desired fields and returns 2-D arrays of data for each field (barb/contour/field) ''' - if debug: - print(fields) + logging.debug(fields) datadict = {} - file_list, missing_list = None, None - #file_list, missing_list = makeEnsembleList(wrfinit, timerange, ENS_SIZE) #construct list of files - #file_list, missing_list = makeEnsembleListNSC(wrfinit, timerange) #construct list of files - #file_list, missing_list = makeEnsembleListStan(wrfinit, timerange, ENS_SIZE) #construct list of files + file_list = defaultdict(lambda: []) + missing_list = defaultdict(lambda: []) + #file_list, missing_list = makeEnsembleList(wrfinit, fhr, ENS_SIZE) #construct list of files + #file_list, missing_list = makeEnsembleListNSC(wrfinit, fhr) #construct list of files + #file_list, missing_list = makeEnsembleListStan(wrfinit, fhr, ENS_SIZE) #construct list of files if mesh == 'mpas': - file_list, missing_list = makeEnsembleListMPAS(wrfinit, timerange, ENS_SIZE, g193=False, debug=debug) #construct list of files - #file_list, missing_list = makeEnsembleListArchive(wrfinit, timerange) #construct list of files - #file_list, missing_list = makeEnsembleListHybrid(wrfinit, timerange) #construct list of files - #file_list, missing_list = makeEnsembleListHREF(wrfinit, timerange, ENS_SIZE) #construct list of files - #file_list, missing_list = makeEnsembleListHRRR(wrfinit, timerange, 1) #construct list of files + file_list, missing_list = makeEnsembleListMPAS(wrfinit, fhr, ENS_SIZE, g193=False) #construct list of files + if mesh == 'uni' and ENS_SIZE == 1: + idir = "/glade/campaign/mmm/parc/ahijevyc/MPAS" + file_list['diag'] = [os.path.join(idir, mesh, wrfinit.strftime('%Y%m%d%H'), + (wrfinit+datetime.timedelta(hours=f)).strftime('diag.%Y-%m-%d_%H.%M.%S.nc')) for f in fhr] + #file_list, missing_list = makeEnsembleListArchive(wrfinit, fhr) #construct list of files + #file_list, missing_list = makeEnsembleListHybrid(wrfinit, fhr) #construct list of files + #file_list, missing_list = makeEnsembleListHREF(wrfinit, fhr, ENS_SIZE) #construct list of files + #file_list, missing_list = makeEnsembleListHRRR(wrfinit, fhr, 1) #construct list of files if not file_list: - print("no Ensemble file list. Exiting.") - print("Perhaps add --mesh mpas to make_webplot.py command line") + logging.error("no Ensemble file list. Exiting.") + logging.error("Perhaps add --mesh mpas to make_webplot.py command line") sys.exit(1) # loop through fill field, contour field, barb field and retrieve required data for f in ['fill', 'contour', 'barb']: if not list(fields[f].keys()): continue - if debug: print('Reading field:', fields[f]['name'], 'from', fields[f]['filename']) + logging.debug(f"Reading field: {fields[f]['name']} from {fields[f]['filename']}") # save some variables for use in this function filename = fields[f]['filename'] @@ -948,21 +941,19 @@ def readEnsemble(wrfinit, domain, timerange=None, fields=None, debug=False, ENS_ fieldtype = fields[f]['ensprod'] fieldname = fields[f]['name'] if fieldtype in ['prob', 'neprob', 'problt', 'probgt', 'neprobgt', 'neproblt', 'prob3d']: thresh = fields[f]['thresh'] - if fieldtype[0:3]=='mem': member = int(fieldtype[3:]) + if fieldtype.startswith('mem'): member = int(fieldtype[3:]) # open Multi-file netcdf dataset - if debug: - print("opening xarray mfdataset "+ ' '.join(file_list[filename])) - log("opening xarray mfdataset "+ ' '.join(file_list[filename])) + logging.debug(f"opening xarray mfdataset {' '.join(file_list[filename])}") - fh = xarray.open_mfdataset(file_list[filename],concat_dim='Time') + fh = xarray.open_mfdataset(file_list[filename],engine='netcdf4',combine="nested", concat_dim='Time') # This concatenation dimension includes different times AND members. fh = fh.rename({'Time':'TimeMember'}) # loop through each field, wind fields will have two fields that need to be read datalist = [] for n,array in enumerate(arrays): - if debug: log('Reading data '+ array) + logging.debug(f'Reading data {array}') #read in 3D array (times*members,ny,nx) from file object if 'arraylevel' in fields[f]: @@ -970,15 +961,14 @@ def readEnsemble(wrfinit, domain, timerange=None, fields=None, debug=False, ENS_ else: level = fields[f]['arraylevel'] else: level = None - if level == 'max': data = np.amax(fh.variables[array][:,:,:,:], axis=1) - elif level is None: data = fh.variables[array][:] - else: data = fh.variables[array][:,level,:,:] + if level == 'max': data = fh[array].max(dim=level) + elif level is None: data = fh[array] + else: data = fh[array].sel(level=level) data = data.values # use the numpy array, not the full xarray object. # Many things that come afterward assume a numpy array, like flatten method. - if fh.variables[array].dims[1] == 'nVertices': - if debug: - print("field on vertices, like vorticity_500hPa, put on cells") + if fh[array].dims[1] == 'nVertices': + logging.debug("field on vertices, like vorticity_500hPa, put on cells") fieldv = data nEdgesOnCell, verticesOnCell = readMPASVertices() # verticesOnCell is the transpose of what mpas_vort_cell1 expects @@ -1020,7 +1010,7 @@ def readEnsemble(wrfinit, domain, timerange=None, fields=None, debug=False, ENS_ datalist.append(data) # these are derived fields, we don't have in any of the input files but we can compute - print(datalist[0].shape) + logging.info(f"datalist[0].shape={datalist[0].shape}") if 'name' in fields[f]: if fieldname in ['shr06mag', 'shr01mag']: # derive wind shear from top and bottom level @@ -1045,8 +1035,7 @@ def readEnsemble(wrfinit, domain, timerange=None, fields=None, debug=False, ENS_ if is_precip_diff(fieldname): - if debug: - print("Deriving accumulated precipitation. Subract ensemble at first time from ensemble at last time") + logging.debug("Deriving accumulated precipitation. Subract ensemble at first time from ensemble at last time") for i in missing_list[filename]: data = np.insert(data, i, np.nan, axis=0) #insert nan for missing files # last and first time in the requested time range ensemble_at_last_time = data[-ENS_SIZE:] @@ -1103,7 +1092,7 @@ def readEnsemble(wrfinit, domain, timerange=None, fields=None, debug=False, ENS_ data3d[i] = data2d miles_to_km = 1.60934 roi = 25 * miles_to_km / junk.min_grid_spacing_km - if debug: print("roi",roi) + logging.debug(f"roi {roi}") data = compute_neprob(data3d, roi=int(roi), sigma=float(fields['sigma']), type='gaussian') else: data = np.nanmean(data, axis=0) data = data+0.001 #hack to ensure that plot displays discrete prob values @@ -1112,9 +1101,9 @@ def readEnsemble(wrfinit, domain, timerange=None, fields=None, debug=False, ENS_ for i in missing_list[filename]: data = np.insert(data, i, np.nan, axis=0) data = np.reshape(data, (ntimes,ENS_SIZE,*spatial_dimensions)) data = compute_prob3d(data, roi=14, sigma=float(fields['sigma']), type='gaussian') - if debug: print('field '+ fieldname+ ' has shape', data.shape, 'max', data.max(), 'min', data.min()) + logging.debug(f'field {fieldname} has shape {data.shape} range {data.min()}-{data.min()}') - print(data.max()) + logging.info(f"max={data.max()}") #kernel = np.ones((7,7)) #data = ndimage.filters.convolve(data, kernel/float(kernel.sum())) # attach data arrays for each type of field (e.g. { 'fill':[data], 'barb':[data,data] }) @@ -1134,25 +1123,24 @@ def is_precip_diff(s): def readGrid(file_dir): - f = Dataset(file_dir, 'r') - lats = f.variables['XLAT'][0,:] - lons = f.variables['XLONG'][0,:] - f.close() + f = xarray.open_dataset(file_dir) + lats = f['XLAT'] + lons = f['XLONG'] return (lats,lons) def readMPASVertices(ifile="/glade/p/mmm/parc/schwartz/MPAS/15-3km_mesh/init.nc"): # Used for regridding field from vertex to cell. latVertex and lonVertex not needed - fh = Dataset(ifile, "r") - nEdgesOnCell = fh.variables['nEdgesOnCell'][:] - verticesOnCell = fh.variables['verticesOnCell'][:] - fh.close() + fh = xarray.open_dataset(ifile) + nEdgesOnCell = fh['nEdgesOnCell'] + verticesOnCell = fh['verticesOnCell'] return (nEdgesOnCell, verticesOnCell) def readGridMPAS(init_file="/glade/p/mmm/parc/schwartz/MPAS/15-3km_mesh/init.nc"): - fh = Dataset(init_file, "r") - latCell = fh.variables['latCell'][:] - lonCell = fh.variables['lonCell'][:] - areaCell = fh.variables['areaCell'][:] # units m^2 + logging.debug(f"open {init_file}") + fh = xarray.open_dataset(init_file) + latCell = fh['latCell'] + lonCell = fh['lonCell'] + areaCell = fh['areaCell'] # units m^2 min_grid_spacing_km = 2. * np.sqrt(areaCell.min()/np.pi/1000/1000) # min_grid_spacing_km used for grid spacing of interpolated lat-lon grid. fh.close() @@ -1160,33 +1148,13 @@ def readGridMPAS(init_file="/glade/p/mmm/parc/schwartz/MPAS/15-3km_mesh/init.nc" lonCell[lonCell >= 180] = lonCell[lonCell >= 180] - 360 return (latCell, lonCell, min_grid_spacing_km) -def saveNewMap(domstr='CONUS', mesh='rt2015', wrfout=None, nlon_max=1500, nlat_max=1500): - # if domstr is not in the dictionary, then use provided wrfout to create new domain - if domstr not in domains: - fh = Dataset(wrfout, 'r') - lats = fh.variables['XLAT'][0,:] - lons = fh.variables['XLONG'][0,:] - ll_lat, ll_lon, ur_lat, ur_lon = lats[0,0], lons[0,0], lats[-1,-1], lons[-1,-1] - lat_1, lat_2, lon_0 = fh.TRUELAT1, fh.TRUELAT2, fh.STAND_LON - fig_width = 1080 - fh.close() - # else assume domstr is in dictionary - elif 'file' in domains[domstr]: - fh = Dataset(domains[domstr]['file'], 'r') - lats = fh.variables['XLAT'][0,:] - lons = fh.variables['XLONG'][0,:] - ll_lat, ll_lon, ur_lat, ur_lon = lats[0,0], lons[0,0], lats[-1,-1], lons[-1,-1] - lat_1, lat_2, lon_0 = fh.TRUELAT1, fh.TRUELAT2, fh.STAND_LON - if 'fig_width' in domains[domstr]: fig_width = domains[domstr]['fig_width'] - else: fig_width = 1080 - fh.close() - else: - ll_lat, ll_lon, ur_lat, ur_lon = domains[domstr]['corners'] - fig_width = domains[domstr]['fig_width'] - lat_1, lat_2, lon_0 = 32.0, 46.0, -101.0 - if domstr=='NA': - lon_0 = -115.0 - +def saveNewMap(plot, wrfout=None, init_file="/glade/p/mmm/parc/schwartz/MPAS/15-3km_mesh/init.nc"): + fh = xarray.open_dataset(wrfout) + lats = fh['XLAT'].isel(Time=0).data + lons = fh['XLONG'].isel(Time=0).data + ll_lat, ll_lon, ur_lat, ur_lon = lats[0,0], lons[0,0], lats[-1,-1], lons[-1,-1] + lat_1, lat_2, lon_0 = fh.TRUELAT1, fh.TRUELAT2, fh.STAND_LON + fig_width = 1080 dpi = 90 fig = plt.figure(dpi=dpi) m = Basemap(projection='lcc', resolution='i', llcrnrlon=ll_lon, llcrnrlat=ll_lat, urcrnrlon=ur_lon, urcrnrlat=ur_lat, \ @@ -1213,15 +1181,15 @@ def saveNewMap(domstr='CONUS', mesh='rt2015', wrfout=None, nlon_max=1500, nlat_m #m.drawcounties(linewidth=0.1, color='gray', ax=ax) - if mesh == 'hrrr': + if plot.mesh == 'hrrr': lons=None lats=None min_grid_spacing_km = None delta_deg = None # Can we - fh = Dataset("/glade/scratch/ahijevyc/hrrr/2018120106/20181201_i06_f024_HRRR-NCEP_wrfprs.nc", 'r') - lat2d = fh.variables['gridlat_0'][:] - lon2d = fh.variables['gridlon_0'][:] + fh = xarray.open_dataset("/glade/scratch/ahijevyc/hrrr/2018120106/20181201_i06_f024_HRRR-NCEP_wrfprs.nc") + lat2d = fh['gridlat_0'] + lon2d = fh['gridlon_0'] x2d, y2d = m(lon2d,lat2d) ibox = None x = None @@ -1230,17 +1198,17 @@ def saveNewMap(domstr='CONUS', mesh='rt2015', wrfout=None, nlon_max=1500, nlat_m wts = None else: # load lat/lons - lats, lons, min_grid_spacing_km = readGridMPAS(init_file="/glade/p/mmm/parc/schwartz/MPAS/15-3km_mesh/init.nc") + lats, lons, min_grid_spacing_km = readGridMPAS(init_file=init_file) delta_deg = min_grid_spacing_km / 111 if m.lonmin > 180 or m.lonmax > 180: lons[lons<0] = lons[lons<0] + 360. # change -180-0 to 180-360 to match m.lonmin and m.lonmax # Replace m.lonmax with ur_lon? Otherwise, risk getting +179.999 when NW corner crosses the datetime nlon = int((m.lonmax - m.lonmin)/delta_deg) nlat = int((m.latmax - m.latmin)/delta_deg) - if nlon > nlon_max: - nlon = nlon_max - if nlat > nlat_max: - nlat = nlat_max + if nlon > plot.nlon_max: + nlon = plot.nlon_max + if nlat > plot.nlat_max: + nlat = plot.nlat_max lon2d, lat2d = np.meshgrid(np.linspace(m.lonmin, m.lonmax, nlon), np.linspace(m.latmin,m.latmax,nlat)) # Convert to map coordinates instead of latlon to avoid the need to specify latlon=True in contour and barb methods. x2d, y2d = m(lon2d,lat2d) @@ -1250,16 +1218,18 @@ def saveNewMap(domstr='CONUS', mesh='rt2015', wrfout=None, nlon_max=1500, nlat_m lats = lats[ibox] x, y = m(lons,lats) - # use .filled() to avoid error about masked arrays - vtx, wts = interp_weights(np.vstack((lons.filled(),lats.filled())).T,np.vstack((lon2d.flatten(), lat2d.flatten())).T) + logging.info("interp_weights") + vtx, wts = interp_weights(np.vstack((lons,lats)).T,np.vstack((lon2d.flatten(), lat2d.flatten())).T) + + pickle.dump((fig,ax,m,lons,lats,min_grid_spacing_km,delta_deg,lon2d,lat2d,x2d,y2d,ibox,x,y,vtx,wts), open(plot.pk_file, 'wb')) + - pickle.dump((fig,ax,m,lons,lats,min_grid_spacing_km,delta_deg,lon2d,lat2d,x2d,y2d,ibox,x,y,vtx,wts), open('/glade/work/ahijevyc/share/rt_ensemble/python_scripts/%s_%s_%dx%d.pk'%(mesh,domstr,nlon_max,nlat_max), 'wb')) -def drawOverlay(domstr='CONUS'): - ll_lat, ll_lon, ur_lat, ur_lon = domains[domstr]['corners'] - fig_width = domains[domstr]['fig_width'] +def drawOverlay(domain='CONUS'): + ll_lat, ll_lon, ur_lat, ur_lon = domains[domain]['corners'] + fig_width = domains[domain]['fig_width'] lat_1, lat_2, lon_0 = 32.0, 46.0, -101.0 - if domstr=='NA': + if domain=='NA': lon_0 = -115.0 dpi = 90 @@ -1283,7 +1253,7 @@ def drawOverlay(domstr='CONUS'): m.drawcoastlines(linewidth=0, ax=ax) m.drawcounties(ax=ax) ax.axis('off') - plt.savefig('overlay_counties_%s.png'%domstr, dpi=90, transparent=True) + plt.savefig('overlay_counties_%s.png'%domain, dpi=90, transparent=True) def compute_pmm(ensemble): members = ensemble.shape[0] @@ -1354,10 +1324,9 @@ def computestp(data): return stp def compute_sspf(data, cref_files): - fh = MFDataset(cref_files) - #cref = fh.variables['REFD_MAX'][:] #in diag files - cref = fh.variables['REFL_MAX_COL'][:] #in upp files - fh.close() + fh = xarray.open_mfdataset(cref_files) + #cref = fh['REFD_MAX'] #in diag files + cref = fh['REFL_MAX_COL'] #in upp files # if all times are in one file, then need to reshape and extract desired times #cref = cref.reshape((10,49,cref.shape[1],cref.shape[2])) # reshape From 6c24b0f7a7985bb6cac81cb696d92d950db9baf2 Mon Sep 17 00:00:00 2001 From: ahijevyc Date: Sun, 12 Feb 2023 13:17:39 -0700 Subject: [PATCH 40/68] webPlot object handles only 1 domain pk_file an object method, not a stand-alone method Use fieldinfo.domains Don't griddata the neighborhood data in readEnsemble. just return 3-d mesh --- make_webplot.py | 46 +++++++++++-------------------- webplot.py | 72 +++++++++++++++++++------------------------------ 2 files changed, 43 insertions(+), 75 deletions(-) diff --git a/make_webplot.py b/make_webplot.py index 2441879..f97234e 100755 --- a/make_webplot.py +++ b/make_webplot.py @@ -3,7 +3,7 @@ import pdb import sys import time -from webplot import webPlot, readGrid, saveNewMap +from webplot import webPlot logging.info('Begin Script') stime = time.time() @@ -11,38 +11,22 @@ regions = ['CONUS', 'NGP', 'SGP', 'CGP', 'MATL', 'NE', 'NW', 'SE', 'SW'] -newPlot = webPlot() -logging.info('Reading Data') -newPlot.readEnsemble() for dom in regions: - pk_file = newPlot.pk_file - if not os.path.exists(pk_file): - saveNewMap(newPlot, wrfout='latlonfile.nc', init_file='/glade/campaign/mmm/parc/ahijevyc/MPAS/uni/2018103000/init.nc') - file_not_created, num_attempts = True, 0 - while file_not_created and num_attempts <= 3: - newPlot.domain = dom - - newPlot.createFilename() - fname = newPlot.outfile - - newPlot.loadMap() - - logging.info('Plotting Data') - if newPlot.opts['interp']: - newPlot.plotInterp() - else: - newPlot.plotFields() - newPlot.plotTitleTimes() - - logging.info('Writing Image') - newPlot.saveFigure(trans=newPlot.opts['over']) - - if os.path.exists(fname): - file_not_created = False - logging.info(f'Created {fname} {os.stat(fname).st_size/1000:.1f} KB') - - num_attempts += 1 + newPlot = webPlot(domain=dom) + logging.info('Reading Data') + newPlot.readEnsemble() + newPlot.loadMap() + + logging.info('Plotting Data') + if newPlot.opts['interp']: + newPlot.plotInterp() + else: + newPlot.plotFields() + newPlot.plotTitleTimes() + + logging.info('Writing Image') + newPlot.saveFigure(trans=newPlot.opts['over']) etime = time.time() logging.info(f'End Plotting (took {etime-stime:.2f} sec)') diff --git a/webplot.py b/webplot.py index db9349e..a55429d 100755 --- a/webplot.py +++ b/webplot.py @@ -1,7 +1,7 @@ import argparse from collections import defaultdict import datetime -from fieldinfo import * +from fieldinfo import domains import logging import matplotlib.colors as colors import matplotlib.pyplot as plt @@ -25,20 +25,19 @@ class webPlot: '''A class to plot data from NCAR ensemble''' - def __init__(self): + def __init__(self, domain=None): self.opts = parseargs() self.initdate = pd.to_datetime(self.opts['date']) - self.title = self.opts['title'] - self.debug = self.opts['debug'] + self.ENS_SIZE = self.opts['ENS_SIZE'] self.autolevels = self.opts['autolevels'] - self.domain = self.opts['domain'] + self.debug = self.opts['debug'] + self.domain = domain self.fhr = self.opts['fhr'] self.mesh = self.opts['mesh'] self.nlat_max = self.opts['nlat_max'] self.nlon_max = self.opts['nlon_max'] - self.pk_file = os.path.join(os.getenv('TMPDIR'), f"{self.mesh}_{self.domain}_{self.nlon_max}x{self.nlat_max}.pk") + self.title = self.opts['title'] self.createFilename() - self.ENS_SIZE = self.opts['ENS_SIZE'] def createFilename(self, prefx=''): for f in ['fill', 'contour','barb']: # CSS added this for loop and everything in it @@ -75,15 +74,16 @@ def toServer(self, url="nova.mmm.ucar.edu:/web/htdocs/projects/mpas/plots/."): print(result) def loadMap(self, overlay=False): - logging.info(f"loadMap {self.pk_file}") + pk_file = os.path.join(os.getenv('TMPDIR'), f"{self.mesh}_{self.domain}_{self.nlon_max}x{self.nlat_max}.pk") + if not os.path.exists(pk_file): + saveNewMap(self, pk_file, init_file='/glade/campaign/mmm/parc/ahijevyc/MPAS/uni/2018103000/init.nc') + logging.info(f"loadMap {pk_file}") (self.fig, self.ax, self.m, self.lons, self.lats, self.min_grid_spacing_km, self.delta_deg, self.lon2d, self.lat2d, self.x2d, self.y2d, self.ibox, self.x, self.y, self.vtx, - self.wts) = pickle.load(open(self.pk_file, 'rb')) + self.wts) = pickle.load(open(pk_file, 'rb')) def readEnsemble(self): - if hasattr(self, 'data') and hasattr(self, 'missing_members') and 'must_reread_ensemble' not in self.data: - return - self.data, self.missing_members = readEnsemble(self.initdate, self.domain, fhr=self.fhr, fields=self.opts, ENS_SIZE=self.ENS_SIZE, mesh=self.mesh) + self.data, self.missing_members = readEnsemble(self.initdate, fhr=self.fhr, fields=self.opts, ENS_SIZE=self.ENS_SIZE, mesh=self.mesh) def plotDepartures(self): from collections import OrderedDict @@ -348,7 +348,13 @@ def plotFill(self): if self.opts['fill']['name'] == 'pbmin' : data = ndimage.gaussian_filter(data, sigma=2) cs1 = self.m.contourf(self.x2d, self.y2d, data, levels=levels, cmap=cmap, norm=norm, extend='max', ax=self.ax) elif self.opts['fill']['ensprod'] in ['neprob', 'neprobgt', 'neproblt']: - # assume the data array has been interpolated to lat/lon + data = self.latlonGrid(data) + miles_to_km = 1.60934 + roi = 25 * miles_to_km / self.min_grid_spacing_km + logging.debug(f"roi {roi}") + data = compute_neprob(data, roi=int(roi), sigma=float(fields['sigma']), type='gaussian') + data = data+0.001 #hack to ensure that plot displays discrete prob values +# assume the data array has been interpolated to lat/lon cs1 = self.m.contourf(self.x2d, self.y2d, data, levels=levels, cmap=cmap, norm=norm, extend='max', ax=self.ax) elif self.vtx is not None: # This is probably not an irregular mesh like MPAS # use ibox and tri @@ -465,6 +471,7 @@ def interpolatetri(self, values, vtx, wts): return np.einsum('nj,nj->n', np.take(values, vtx), wts) def latlonGrid(self, data): + # TODO: Handle ensemble - 3d data # apply ibox to data data = data[self.ibox] if hasattr(self, "vtx") and hasattr(self, "wts"): @@ -664,7 +671,6 @@ def parseargs(): parser.add_argument('-c', '--contour', help='contour field (FIELD_PRODUCT_THRESH)') parser.add_argument('-b', '--barb', help='barb field (FIELD_PRODUCT_THRESH)') parser.add_argument('-bs', '--barbskip', help='barb skip interval') - parser.add_argument('-dom', '--domain', default='CONUS', help='domain to plot') parser.add_argument('--ENS_SIZE', type=int, default=10, help='ensemble size') parser.add_argument('-t', '--title', help='title for plot') parser.add_argument('--nlon_max', default=1500, help='max pts in longitude dimension') @@ -905,7 +911,7 @@ def makeEnsembleListDA(wrfinit, fhr): missing_index += 1 return (file_list, missing_list) -def readEnsemble(wrfinit, domain, fhr=None, fields=None, ENS_SIZE=10, mesh=None): +def readEnsemble(wrfinit, fhr=None, fields=None, ENS_SIZE=10, mesh=None): ''' Reads in desired fields and returns 2-D arrays of data for each field (barb/contour/field) ''' logging.debug(fields) @@ -1075,27 +1081,8 @@ def readEnsemble(wrfinit, domain, fhr=None, fields=None, ENS_SIZE=10, mesh=None) data = np.nanmax(data, axis=0) data = data[member-1,:] elif (fieldtype in ['prob', 'neprob', 'problt', 'probgt', 'neprobgt', 'neproblt']): - if fieldtype in ['prob', 'neprob', 'probgt', 'neprobgt']: data = (data>=thresh).astype('float') - elif fieldtype in ['problt', 'neproblt']: data = (data=thresh).astype('float') + elif fieldtype.endswith('lt'): data = (data=thresh).astype('float') for i in missing_list[filename]: data = np.insert(data, i, np.nan, axis=0) @@ -1148,12 +1135,9 @@ def readGridMPAS(init_file="/glade/p/mmm/parc/schwartz/MPAS/15-3km_mesh/init.nc" lonCell[lonCell >= 180] = lonCell[lonCell >= 180] - 360 return (latCell, lonCell, min_grid_spacing_km) -def saveNewMap(plot, wrfout=None, init_file="/glade/p/mmm/parc/schwartz/MPAS/15-3km_mesh/init.nc"): - fh = xarray.open_dataset(wrfout) - lats = fh['XLAT'].isel(Time=0).data - lons = fh['XLONG'].isel(Time=0).data - ll_lat, ll_lon, ur_lat, ur_lon = lats[0,0], lons[0,0], lats[-1,-1], lons[-1,-1] - lat_1, lat_2, lon_0 = fh.TRUELAT1, fh.TRUELAT2, fh.STAND_LON +def saveNewMap(plot, pk_file, init_file="/glade/p/mmm/parc/schwartz/MPAS/15-3km_mesh/init.nc"): + ll_lat, ll_lon, ur_lat, ur_lon = domains[plot.domain]['corners'] + lat_1, lat_2, lon_0 = 32., 46., -101. fig_width = 1080 dpi = 90 fig = plt.figure(dpi=dpi) @@ -1221,7 +1205,7 @@ def saveNewMap(plot, wrfout=None, init_file="/glade/p/mmm/parc/schwartz/MPAS/15- logging.info("interp_weights") vtx, wts = interp_weights(np.vstack((lons,lats)).T,np.vstack((lon2d.flatten(), lat2d.flatten())).T) - pickle.dump((fig,ax,m,lons,lats,min_grid_spacing_km,delta_deg,lon2d,lat2d,x2d,y2d,ibox,x,y,vtx,wts), open(plot.pk_file, 'wb')) + pickle.dump((fig,ax,m,lons,lats,min_grid_spacing_km,delta_deg,lon2d,lat2d,x2d,y2d,ibox,x,y,vtx,wts), open(pk_file, 'wb')) @@ -1271,7 +1255,7 @@ def compute_pmm(ensemble): def compute_neprob(ensemble, roi=0, sigma=0.0, type='gaussian'): if len(ensemble.shape) < 3: - print('compute_neprob: needs ensemble of 2D arrays, not 1D arrays') + logggin.error('compute_neprob: needs ensemble of 2D arrays, not 1D arrays') sys.exit(1) y,x = np.ogrid[-roi:roi+1, -roi:roi+1] kernel = x**2 + y**2 <= roi**2 From d1ef1801c07816e8204cc8563c09e7295641f943 Mon Sep 17 00:00:00 2001 From: ahijevyc Date: Wed, 15 Feb 2023 13:58:29 -0700 Subject: [PATCH 41/68] use xarray for ensemble functions Preserve Dataset metadata when gridding. long_name and units with colorbar. Use --nbarbs to space wind barbs instead of --barbskip Delimit fill/contour/barb arguents with slash, not underscore. Remove makeEnsembleList variations. Open fhr x member Dataset with "Time" and "mem" dimensions. f2py directives for vert2cell.f90 Remove "filename" from fieldinfo. It was always "diag" Add loadMap, readEnsemble, plotFields, and plotTitleTimes to webPlot class initialization. Remove webPlot methods plotDepartures, plotReports, plotInterp, plotWarnings, plotOutlook, plotMRMS, plotFill_ptype, plotStreamlines. Added webPlot.get_mpas_mesh method to return xarray Dataset of mesh lat/lon/area. Use .startswith() and .endswith() a lot. Don't bother inserting np.nan layers for missing ens members. Simplify pickle file. --- fieldinfo.py | 8 +- make_webplot.py | 22 +- mpas.py | 65 +- ..._vort_cell.cpython-310-x86_64-linux-gnu.so | Bin 47664 -> 47632 bytes mpas_vort_cell.f90 | 10 +- webplot.py | 1110 +++++------------ 6 files changed, 325 insertions(+), 890 deletions(-) diff --git a/fieldinfo.py b/fieldinfo.py index 17b2b32..b24ce5e 100755 --- a/fieldinfo.py +++ b/fieldinfo.py @@ -117,18 +117,14 @@ def readNCLcm(name): 'td700' :{ 'levels' : [-30,-25,-20,-15,-10,-5,0,5,10], 'cmap' : readNCLcm('nice_gfdl')[3:193], 'fname': ['dewpoint_700hPa'], 'filename':'diag'}, 'td850' :{ 'levels' : [-40,-35,-30,-25,-20,-15,-10,-5,0,5,10,15,20,25,30], 'cmap' : readNCLcm('nice_gfdl')[3:193], 'fname': ['dewpoint_850hPa'], 'filename':'diag'}, 'td925' :{ 'levels' : [-30,-25,-20,-15,-10,-5,0,5,10,15,20,25,30], 'cmap' : readNCLcm('nice_gfdl')[3:193], 'fname': ['dewpoint_925hPa'], 'filename':'diag'}, - 'vort500' :{ 'levels' : [0,9,12,15,18,21,24,27,30,33], 'cmap': readNCLcm('prcp_1'), 'fname': ['vorticity_500hPa'], 'filename':'diag'}, - 'vort850' :{ 'levels' : [0,9,12,15,18,21,24,27,30,33], 'cmap': readNCLcm('prcp_1'), 'fname': ['vorticity_850hPa'], 'filename':'diag'}, - 'vort700' :{ 'levels' : [0,9,12,15,18,21,24,27,30,33], 'cmap': readNCLcm('prcp_1'), 'fname': ['vorticity_700hPa'], 'filename':'diag'}, - 'vortpv' :{ 'levels' : [0,9,12,15,18,21,24,27,30,33], 'cmap': readNCLcm('prcp_1'), 'fname': ['vort_pv'], 'filename':'diag'}, 'rh300' :{ 'levels' : [0,10,20,30,40,50,60,70,80,90,100], 'cmap' : [readNCLcm('MPL_PuOr')[i] for i in (2,18,34,50)]+[readNCLcm('MPL_Greens')[j] for j in (2,17,50,75,106,125)], 'fname': ['relhum_300hPa'], 'filename':'diag'}, 'rh500' :{ 'levels' : [0,10,20,30,40,50,60,70,80,90,100], 'cmap' : [readNCLcm('MPL_PuOr')[i] for i in (2,18,34,50)]+[readNCLcm('MPL_Greens')[j] for j in (2,17,50,75,106,125)], 'fname': ['relhum_500hPa'], 'filename':'diag'}, 'rh700' :{ 'levels' : [0,10,20,30,40,50,60,70,80,90,100], 'cmap' : readNCLcm('CBR_drywet'), 'fname': ['relhum_700hPa'], 'filename':'diag'}, 'rh850' :{ 'levels' : [0,10,20,30,40,50,60,70,80,90,100], 'cmap' : readNCLcm('CBR_drywet'), 'fname': ['relhum_850hPa'], 'filename':'diag'}, 'rh925' :{ 'levels' : [0,10,20,30,40,50,60,70,80,90,100], 'cmap' : readNCLcm('CBR_drywet'), 'fname': ['relhum_925hPa'], 'filename':'diag'}, -'pvort320k' :{ 'levels' : [0,0.1,0.2,0.3,0.4,0.5,0.75,1,1.5,2,3,4,5,7,10], + 'pvort320k' :{ 'levels' : [0,0.1,0.2,0.3,0.4,0.5,0.75,1,1.5,2,3,4,5,7,10], 'cmap' : ['#ffffff','#eeeeee','#dddddd','#cccccc','#bbbbbb','#d1c5b1','#e1d5b9','#f1ead3','#003399','#0033FF','#0099FF','#00CCFF','#8866FF','#9933FF','#660099'], - 'fname': ['PVORT_320K'], 'filename':'upp' }, + 'fname': ['PVORT_320K'], 'filename':'upp' }, 'speed10m' :{ 'levels' : [3,6,9,12,15,18,21,24,27,30,33,36,39,42,45,48,51], 'cmap': readNCLcm('wind_17lev')[1:],'fname' : ['u10', 'v10'], 'filename':'diag'}, 'speed10m-tc' :{ 'levels' : [6,12,18,24,30,36,42,48,54,60,66,72,78,84,90,96,102], 'cmap': readNCLcm('wind_17lev')[1:],'fname' : ['u10', 'v10'], 'filename':'diag'}, 'stp' :{ 'levels' : [0.5,0.75,1.0,1.5,2.0,3.0,4.0,5.0,6.0,7.0,8.0], 'cmap':readNCLcm('perc2_9lev'), 'fname':['sbcape','lcl','srh_0_1km','uzonal_6km','umeridional_6km','uzonal_surface','umeridional_surface'], 'filename':'diag'}, diff --git a/make_webplot.py b/make_webplot.py index f97234e..37446b2 100755 --- a/make_webplot.py +++ b/make_webplot.py @@ -5,28 +5,18 @@ import time from webplot import webPlot -logging.info('Begin Script') + + stime = time.time() regions = ['CONUS', 'NGP', 'SGP', 'CGP', 'MATL', 'NE', 'NW', 'SE', 'SW'] - +regions = ['CONUS', 'NGP'] for dom in regions: - newPlot = webPlot(domain=dom) - logging.info('Reading Data') - newPlot.readEnsemble() - newPlot.loadMap() - - logging.info('Plotting Data') - if newPlot.opts['interp']: - newPlot.plotInterp() - else: - newPlot.plotFields() - newPlot.plotTitleTimes() - - logging.info('Writing Image') - newPlot.saveFigure(trans=newPlot.opts['over']) + Plot = webPlot(domain=dom) + logging.debug('Writing Image') + Plot.saveFigure(trans=Plot.opts['over']) etime = time.time() logging.info(f'End Plotting (took {etime-stime:.2f} sec)') diff --git a/mpas.py b/mpas.py index cff81e1..514a2f7 100644 --- a/mpas.py +++ b/mpas.py @@ -3,34 +3,20 @@ import logging import numpy as np import os -import re -import sys # fieldinfo should have been imported from fieldinfo module. -# Copy fieldinfo dictionary for MPAS. Change some fnames and filenames. +# Copy fieldinfo dictionary for MPAS. Change some fnames. fieldinfo['precip']['fname'] = ['rainnc'] -fieldinfo['precip-24hr']['fname'] = ['rainnc'] -fieldinfo['precip-48hr']['fname'] = ['rainnc'] fieldinfo['precipacc']['fname'] = ['rainnc'] -fieldinfo['precipacc']['filename'] = 'diag' fieldinfo['sbcape']['fname'] = ['sbcape'] -fieldinfo['sbcape']['filename'] = 'diag' fieldinfo['mlcape']['fname'] = ['mlcape'] -fieldinfo['mlcape']['filename'] = 'diag' fieldinfo['mucape']['fname'] = ['cape'] -fieldinfo['mucape']['filename'] = 'diag' fieldinfo['sbcinh']['fname'] = ['sbcin'] -fieldinfo['sbcinh']['filename'] = 'diag' fieldinfo['mlcinh']['fname'] = ['mlcin'] -fieldinfo['mlcinh']['filename'] = 'diag' fieldinfo['pwat']['fname'] = ['precipw'] -fieldinfo['pwat']['filename'] = 'diag' fieldinfo['mslp']['fname'] = ['mslp'] -fieldinfo['mslp']['filename'] = 'diag' fieldinfo['td2']['fname'] = ['surface_dewpoint'] fieldinfo['td2depart']['fname'] = ['surface_dewpoint'] -fieldinfo['thetae']['fname'] = ['t2m', 'q2', 'surface_pressure'] -fieldinfo['rh2m']['fname'] = ['t2m', 'surface_pressure', 'q2'] fieldinfo['pblh']['fname'] = ['hpbl'] fieldinfo['hmuh']['fname'] = ['updraft_helicity_max'] fieldinfo['hmuh03']['fname'] = ['updraft_helicity_max03'] @@ -41,62 +27,63 @@ fieldinfo['hmwind']['fname'] = ['wind_speed_level1_max'] fieldinfo['hmgrp']['fname'] = ['grpl_max'] fieldinfo['cref']['fname'] = ['refl10cm_max'] -fieldinfo['cref']['filename'] = 'diag' fieldinfo['ref1km']['fname'] = ['refl10cm_1km'] -fieldinfo['ref1km']['filename'] = 'diag' for ztop in ['3','1']: fieldinfo['srh'+ztop]['fname'] = ['srh_0_'+ztop+'km'] - fieldinfo['srh'+ztop]['filename'] = 'diag' for ztop in ['6','1']: fieldinfo['shr0'+ztop+'mag']['fname'] = ['uzonal_'+ztop+'km', 'umeridional_'+ztop+'km', 'uzonal_surface', 'umeridional_surface'] - fieldinfo['shr0'+ztop+'mag']['filename'] = 'diag' fieldinfo['zlfc']['fname'] = ['lfc'] fieldinfo['zlcl']['fname'] = ['lcl'] -fieldinfo['zlcl']['filename'] = 'diag' # only zlcl filename needed to be changed from upp, not zlfc for plev in ['200', '250','300','500','700','850','925']: fieldinfo['hgt'+plev]['fname'] = ['height_'+plev+'hPa'] fieldinfo['speed'+plev]['fname'] = ['uzonal_'+plev+'hPa','umeridional_'+plev+'hPa'] fieldinfo['temp'+plev]['fname'] = ['temperature_'+plev+'hPa'] for plev in ['500', '700', '850']: fieldinfo['td'+plev]['fname'] = ['dewpoint_'+plev+'hPa'] - fieldinfo['vort'+plev] = {'levels' : [0,9,12,15,18,21,24,27,30,33], 'cmap': readNCLcm('prcp_1'), 'fname': ['vorticity_'+plev+'hPa'], 'filename':'diag'} -fieldinfo['vortpv'] = {'levels' : [0,9,12,15,18,21,24,27,30,33], 'cmap': readNCLcm('prcp_1'), 'fname': ['vort_pv'], 'filename':'diag'} + fieldinfo['vort'+plev] = {'levels' : np.array([0,9,12,15,18,21,24,27,30,33])*1e-5, 'cmap': readNCLcm('prcp_1'), 'fname': ['vorticity_'+plev+'hPa']} +fieldinfo['vortpv'] = {'levels' : [0,9,12,15,18,21,24,27,30,33], 'cmap': readNCLcm('prcp_1'), 'fname': ['vort_pv']} for plev in ['300', '500', '700', '850', '925']: fieldinfo['rh'+plev]['fname'] = ['relhum_'+plev+'hPa'] fieldinfo['speed10m']['fname'] = ['u10', 'v10'] fieldinfo['speed10m-tc']['fname'] = ['u10','v10'] fieldinfo['stp']['fname'] = ['sbcape','lcl','srh_0_1km','uzonal_6km','umeridional_6km','uzonal_surface','umeridional_surface'] -fieldinfo['stp']['filename'] = 'diag' fieldinfo['crefuh']['fname'] = ['refl10cm_max', 'updraft_helicity_max'] -fieldinfo['crefuh']['filename'] = 'diag' fieldinfo['wind10m']['fname'] = ['u10','v10'] -fieldinfo['shr06'] = { 'fname' : ['uzonal_6km','umeridional_6km','uzonal_surface','umeridional_surface'], 'filename': 'diag', 'skip':50 } -fieldinfo['shr01'] = { 'fname' : ['uzonal_1km','umeridional_1km','uzonal_surface','umeridional_surface'], 'filename': 'diag', 'skip':50 } +fieldinfo['shr06'] = { 'fname' : ['uzonal_6km','umeridional_6km','uzonal_surface','umeridional_surface'] } +fieldinfo['shr01'] = { 'fname' : ['uzonal_1km','umeridional_1km','uzonal_surface','umeridional_surface'] } # Enter wind barb info for list of pressure levels for plev in ['200', '250', '300', '500', '700', '850', '925']: - fieldinfo['wind'+plev] = { 'fname' : ['uzonal_'+plev+'hPa', 'umeridional_'+plev+'hPa'], 'filename':'diag', 'skip':50} + fieldinfo['wind'+plev] = { 'fname' : ['uzonal_'+plev+'hPa', 'umeridional_'+plev+'hPa'] } +mesh_config = { + "uni" : ( 1, '/glade/campaign/mmm/parc/ahijevyc/MPAS/uni/2018103000/init.nc'), + "15-3km_mesh" : (10, "/glade/p/mmm/parc/schwartz/MPAS/15-3km_mesh/init.nc"), + "15km_mesh" : (10, "/glade/p/mmm/parc/schwartz/MPAS/15km_mesh/init.nc"), + } -def makeEnsembleListMPAS(wrfinit, fhr, ENS_SIZE, g193=False): + +def makeEnsembleList(Plot): + initdate = Plot.initdate + fhr = Plot.fhr + ENS_SIZE = Plot.ENS_SIZE + meshstr = Plot.meshstr # create lists of files (and missing file indices) for various file types - file_list = { 'wrfout':[], 'diag':[] } - missing_list = { 'wrfout':[], 'diag':[] } + file_list = [] + missing_list = [] missing_index = 0 for hr in fhr: - wrfvalidstr = (wrfinit + datetime.timedelta(hours=hr)).strftime('%Y-%m-%d_%H.%M.%S') - yyyymmddhh = wrfinit.strftime('%Y%m%d%H') + validstr = (initdate + datetime.timedelta(hours=hr)).strftime('%Y-%m-%d_%H.%M.%S') + yyyymmddhh = initdate.strftime('%Y%m%d%H') for mem in range(1,ENS_SIZE+1): - diag = '/glade/p/mmm/parc/schwartz/MPAS_ens_15-3km_mesh/POST/%s/ens_%d/diag.%s.nc'%(yyyymmddhh,mem,wrfvalidstr) - if g193: - diag = '/glade/p/mmm/parc/schwartz/MPAS_ens_15-3km_mesh/POST/%s/ens_%d/diag_latlon_g193.%s.nc'%(yyyymmddhh,mem,wrfvalidstr) + diag = f'/glade/campaign/mmm/parc/schwartz/MPAS_ensemble_paper/{meshstr}/{yyyymmddhh}/ens_{mem}/diag.{validstr}.nc' logging.debug(diag) - if os.path.exists(diag): file_list['diag'].append(diag) + if os.path.exists(diag): file_list.append(diag) else: - missing_list['diag'].append(missing_index) + missing_list.append(missing_index) logging.warning(f"{diag} does not exist") missing_index += 1 logging.debug(f"file_list {file_list}") - if not file_list['diag']: + if not file_list: logging.info('Empty file_list') - return (file_list, missing_list) + return file_list, missing_list diff --git a/mpas_vort_cell.cpython-310-x86_64-linux-gnu.so b/mpas_vort_cell.cpython-310-x86_64-linux-gnu.so index b5c4e0daef5dc08e31ac2392554d65dc5861c788..5d7a553adfe096bb4eb78763bbb51e146177c59f 100755 GIT binary patch delta 1860 zcmZ9N2~1Q+7{}i?94_l3Ai{!FyV6BiL>2@EF_3#?%;JXL*KFUpDib@BjVZd^4GSGv}U@ zTK02;+B$`wxJSp0Jw3X*98ZflOCi$NwUpbW7YV^QF#wvTz1F$fYF>;O^r`7;{p;HE zR}Q4N)5j1bCWZ!dmw193u9hf4hLsYfQMxSI2Vr!v0UP+!b z;9mO}$5ndSSSgWEkaO51bPDjPz0|Sntt=NVWGir%RO+aID_f8vWV`8d>3y;2T#1Z} zt_l7O1D)MOS|KRJ4bFZLfUVAMU`_j-y}^B}8ONz>4$I3JqigoaD~xgFG1Oa!S-kp5O;NAO}Ntq?sh8XE774Wkhx5uFiofj~G8gIDP4&kp6d%zhtMwY{Dd=V+Pw3)+kk(JB12&;>B7U-smg1K0r z8i8e~j!GuCXXBZu-H?RyqN`Zj6e4lElIAUynZQ!IFQ%E08#B;r zc?>kt(&aw_dFF%(@$Te-6Yh?WCto<>?f7~K#r&1m$kFL^-uo_u9Gi}-6OKYDeVyP; zEZ&iF+%v0_Fj3=-hAbD+xX`BYp$5%luwPJ= zHExsTZ^eO4X-$eu#asoo9Id6=T~qo|h2D z9-fz*a9q%Jo;Nk+xN1gU#&vx>-^SR_c!%-Mw>;m;KG2|k<}>HGTt*MZ!;BG(ojc2Fc7+0L zVeKW_S8N5Kr$23x0+iv?lDovK2M4!$n#Zwu4{-Q>@w@1vjfaz{)hb~WKiA$R4!yW{ zn~He#;^4N~)BSsS-b*;D#@dI#YO{sJ_0p-^e*vh#e|9KgD^=_~NX+AI@_d;E8#R=g zl}$E*GHSE?B4M7sk7MByI_ffE06)@6*@4ucQ#xKB;d$AFu_IW29b3Zyy{9{ChF;~# zw6;3S%yB49Fd5!(^bMb*&Hh-ECk-;3RUS;6_uH8m_ioagy~79fUDV>BDPYSdDx9KU zfvubDumD8hK(%^T6G+FFLsE7FbQ~Hs-T?a#A2(v*k$q&e0dE{x57ww?96S8iG|q%} z)Hb?8GPX2Ip%gnBqu5~yO-lB@chgnUt;4^XRBXtA<|yMRyP8!j)7N~01+=im+!Vr4 zdu*89I?Ioj7_}S6byk)S*@lik*dn&Yf(zCb|6OF27yK+_#ezdd`z}l(z~QY+O0l6q fHm0&l;*Jnw!6h9bY`&Epp>PiSSX+%2mtFq_&(xl1 delta 1993 zcmZ8idr*{B6#vcz)@5a5F=R!}7G{Y@xVt>c2d;|oSc^czLKKh{cHNbS>w*GC1Db(^ zIN+&N)D_3lG#}MXg=)-HtR^kSG%G2xClxg`Ib$<9-OlbuSl&PG@1EcN-E+?U&V1*# z-(auaVC^yRYDeremA+Z>4SjRRN<*MaEnD3kdz(YfRm;XG;tbdm+WyIb%+|=2pD2D! zH(yGrT(YiG@8Lr*OU0y4j0-Y=1@nW9Fc({bj9%)^pe<0$=d0gSLLY{NX`vru!;CPD z`C&$;>BMKlvcqe)>wKYh7y3|e1S;?a0r(s zEQd^fD&a1G>3A`5Ib7kUBr?w9oa72v%#S3m02t=uAAFaA0k2N4fI*%y&0JaZRp#?>iiIEu$oGGRa7Oo^q>7%|HOEqHp?$$9C?-mD+^$|y;LvER_K zHglp(jWH1wCR6vyl*vo^$yAU`J#NAt^ZZGlC;8kw;B-pFH@kkdc#;ldm^iJgT{fCGOoe2l5bVR}&^m1tkl zQm8~xl+AKNF=yE{HZcS@Ez1B8e!T2KxQUA545kUi?BbP7r@=RipStHtFnzw(=}D&t zubmPi>2&^COKX73p!1gIA}+G(;R4oKV?vLSF017?(xsB_mmsRvVLWb)f+l{=>JRWG ze!YC3qCSVlOl-7I!2=}`3cJ%*Ub+^~ml$wSX(-o}&SPT}s;YHmwz`^C?ouqWM`D9L z@_*bBdlELdmHbD$Ka5YVcKj>WRj#$UtDPmbDzt(QyPQ#D{&n-HgN`_iVHoNe}74ee_x1X!drxg3ClkaqMxw-Lm}R# zCeTmluaKmm3qr&b<`BEVx28^{#rt0>Yusg}9-j^WW0`T~eGBKSY5wl606bD})05 zD#ROvS%en|%L#80ZpQF6(V)Rpniq5DnqX=iyl$-)s7f6*1FY{Xx@!%J;j=>YNm#k& zUhJ*SrG^_`XM{~Szixna_hQ_-4A$3+?sfM~{H#|9`k6<^cR(@dBE!A>^>sf0#Ndv4 zBTV2Q)Nf;o;g5u93?#2}_)86A{XoZG+i;SR#`#PREXJN^R)7m5nzT@hGn$Oyj%z~b zMm(8IUecLi3a;hCRw{C&eZLP3X+uT*Esdo(Sc_9FBa9b`~R%_4&N~q=e+oS#9 z9IoG~g+|=5^Q!k5a_`#jg^9bjFl8qm+Fb-D4D|S3pqZX2@CD|3qTw{I_h?}NcX%?X zV_)|e>3qnZcbKjjFYU=7m*HKRUN6hLGU$Oj*mZym{@7lHKNMsBOIN893Hxij+@bwV z!Rj>n8lwaEs_w=GCp3Y#UsLCaX@QMvWJM!Ccw!8rGQZYsR7ssWneD`%yJvd?#`Vmm Se4RaW=nXwc^I`m{C;A`J+qks= diff --git a/mpas_vort_cell.f90 b/mpas_vort_cell.f90 index 4be643b..d16006e 100644 --- a/mpas_vort_cell.f90 +++ b/mpas_vort_cell.f90 @@ -1,18 +1,16 @@ -subroutine mpas_vort_cell1( nEdgesOnCell, verticesOnCell, maxEdges, nVertLevels, nCells, nVertices, fieldv, fieldc ) +subroutine vert2cell(nEdgesOnCell, verticesOnCell, maxEdges, nVertLevels, nCells, nVertices, fieldv, fieldc) implicit none -! input - integer, intent(in) :: maxEdges, nVertLevels, nCells, nVertices +!f2py required :: maxEdges, nVertLevels, nCells, nVertices integer, intent(in) :: nEdgesOnCell(nCells) integer, intent(in) :: verticesOnCell(maxEdges,nCells) real*8, intent(in) :: fieldv(nVertLevels,nVertices) real*8, intent(out) :: fieldc(nVertLevels,nCells) -! local - integer i,j,k real*8 factor + do k=1,nVertLevels do i=1,nCells factor = 1./nEdgesOnCell(i) @@ -24,4 +22,4 @@ subroutine mpas_vort_cell1( nEdgesOnCell, verticesOnCell, maxEdges, nVertLevels, end do end do return -end +end subroutine vert2cell diff --git a/webplot.py b/webplot.py index a55429d..2a64e6f 100755 --- a/webplot.py +++ b/webplot.py @@ -5,15 +5,16 @@ import logging import matplotlib.colors as colors import matplotlib.pyplot as plt -from mpas import fieldinfo, makeEnsembleListMPAS -import mpas_vort_cell +import mpas +from mpas import fieldinfo, mesh_config, makeEnsembleList +import vert2cell # created with f2py3 -c mpas_vort_cell.f90 -m vert2cell from mpl_toolkits.basemap import * import pandas as pd import pdb import pickle import os import scipy.ndimage as ndimage -from scipy import interpolate +from scipy.interpolate import griddata from scipy.spatial import qhull import subprocess import re @@ -21,7 +22,7 @@ import time import xarray -logging.basicConfig(level=logging.INFO) +logging.basicConfig(format='%(asctime)s - %(message)s', level=logging.INFO) class webPlot: '''A class to plot data from NCAR ensemble''' @@ -33,14 +34,22 @@ def __init__(self, domain=None): self.debug = self.opts['debug'] self.domain = domain self.fhr = self.opts['fhr'] - self.mesh = self.opts['mesh'] + self.meshstr = self.opts['meshstr'] + self.nbarbs = self.opts['nbarbs'] self.nlat_max = self.opts['nlat_max'] self.nlon_max = self.opts['nlon_max'] self.title = self.opts['title'] + + self.get_mpas_mesh() self.createFilename() + self.loadMap() + self.data, self.missing_members = readEnsemble(self) + self.plotFields() + self.plotTitleTimes() - def createFilename(self, prefx=''): + def createFilename(self): for f in ['fill', 'contour','barb']: # CSS added this for loop and everything in it + prefx = self.meshstr if 'name' in self.opts[f]: if 'thresh' in self.opts[f]: prefx = self.opts[f]['name']+'_'+self.opts[f]['ensprod']+'_'+str(self.opts[f]['thresh']) # CSS @@ -62,221 +71,25 @@ def createFilename(self, prefx=''): os.makedirs(subdir_path) # prepend subdir_path to outfile. self.outfile = os.path.join(subdir_path, self.outfile) - - - def toServer(self, url="nova.mmm.ucar.edu:/web/htdocs/projects/mpas/plots/."): - rsync_opts = '-RLv' - #result = subprocess.run(['rsync', rsync_opts, '--timeout=10', '--bwlimit=3', self.outfile, url], check=True, stdout=subprocess.PIPE) # tried capture_output=True but this version of subprocess doesn't recognize it. - # send directly to koa instead of nova. Carter set up ssh keys. Not working. Get return status=14 - result = subprocess.run(['rsync', '-e', "'ssh -vi /home/ahijevyc/.ssh/id_rsa'", '-avR', self.outfile, url], check=True, stdout=subprocess.PIPE) - #The --contimeout option may only be used when connecting to an rsync daemon - if result.returncode != 0: - print(result) + self.outfile = os.path.realpath(self.outfile) def loadMap(self, overlay=False): - pk_file = os.path.join(os.getenv('TMPDIR'), f"{self.mesh}_{self.domain}_{self.nlon_max}x{self.nlat_max}.pk") + pk_file = os.path.join(os.getenv('TMPDIR'), f"{self.meshstr}_{self.domain}_{self.nlon_max}x{self.nlat_max}.pk") if not os.path.exists(pk_file): - saveNewMap(self, pk_file, init_file='/glade/campaign/mmm/parc/ahijevyc/MPAS/uni/2018103000/init.nc') - logging.info(f"loadMap {pk_file}") - (self.fig, self.ax, self.m, self.lons, self.lats, self.min_grid_spacing_km, self.delta_deg, + saveNewMap(self, pk_file) + logging.debug(f"loadMap: use old pickle file {pk_file}") + (self.fig, self.ax, self.m, self.lons, self.lats, self.delta_deg, self.lon2d, self.lat2d, self.x2d, self.y2d, self.ibox, self.x, self.y, self.vtx, self.wts) = pickle.load(open(pk_file, 'rb')) - def readEnsemble(self): - self.data, self.missing_members = readEnsemble(self.initdate, fhr=self.fhr, fields=self.opts, ENS_SIZE=self.ENS_SIZE, mesh=self.mesh) - - def plotDepartures(self): - from collections import OrderedDict - hly_inventory, hly_forecast = OrderedDict(), OrderedDict() - - fieldname = self.opts['fill']['name'] - if fieldname == 't2depart': normals = open('/glade/u/home/sobash/RT2015_gpx/hly-temp-normal.txt').readlines() - elif fieldname == 'td2depart': normals = open('/glade/u/home/sobash/RT2015_gpx/hly-dewp-normal.txt').readlines() - - # figure out local times for this UTC time because NCDC - utcoffset = (time.mktime(time.gmtime()) - time.mktime(time.localtime()))/3600.0 #utc to local time offset for mountain time - monthday = (self.initdate + timedelta(hours=self.shr)).strftime(' %m %d ') #search for this string in normals file (see below) - hr_pt = (self.initdate + timedelta(hours=self.shr) - timedelta(hours=utcoffset+1)).hour # hour in pacific time - hr_mt = (self.initdate + timedelta(hours=self.shr) - timedelta(hours=utcoffset)).hour # hour in mountain time - hr_ct = (self.initdate + timedelta(hours=self.shr) - timedelta(hours=utcoffset-1)).hour # hour in central time - hr_et = (self.initdate + timedelta(hours=self.shr) - timedelta(hours=utcoffset-2)).hour # hour in eastern time - - # get interpolated forecast at climate station locations (in F) - if len(self.x.shape) == 1: - fi = interpolate.RegularGridInterpolator((self.y2d[:,0], self.x2d[0,:]), self.latlonGrid(self.data['fill'][0]), fill_value=-9999, bounds_error=False, method='linear') - if len(self.x.shape) == 2: - fi = interpolate.RegularGridInterpolator((self.y[:,0], self.x[0,:]), self.data['fill'][0], fill_value=-9999, bounds_error=False, method='linear') - with open('/glade/u/home/sobash/RT2015_gpx/hly-inventory.txt') as f: - for line in f: - stn = line.split() - lonlat = list(map(float, stn[1:3][::-1])) - x_ob, y_ob = self.m(*list(zip(lonlat))) - hly_forecast[stn[0]] = fi((y_ob,x_ob))[0] - hly_inventory[stn[0]] = (y_ob[0], x_ob[0], lonlat[0], lonlat[1]) - - # read in file of normals for this hour and plot departures - if fieldname == 't2depart': cmap = plt.get_cmap('RdBu_r') - elif fieldname == 'td2depart': cmap = plt.get_cmap('BrBG') - norm = colors.BoundaryNorm([-50,-19.999,-14.999,-9.999,-4.999,0,5,10,15,20,50], cmap.N) #want to have bins so they both dont include bndrys - self.m.set_axes_limits(ax=self.ax) # need to do this if no other basemap functions have been called - - for line in normals: - if monthday in line: - stn = line.split() - try: y_ob, x_ob, lon, lat = hly_inventory[stn[0]] - except: continue - - if lon < -114: localhr = hr_pt #PT - elif lon > -102 and lon <= -85: localhr = hr_ct #CT - elif lon > -85: localhr = hr_et #ET - else: localhr = hr_mt #MT - - forecast = hly_forecast[stn[0]] - normal = float(stn[localhr+3][:-1])/10.0 - departure = float(forecast - normal) - - if x_ob < self.m.xmax and x_ob > self.m.xmin and y_ob < self.m.ymax and y_ob > self.m.ymin and normal > -50: - if abs(departure) < 5: size = 5**2 - elif abs(departure) < 10: size = 8**2 - elif abs(departure) < 15: size = 11**2 - elif abs(departure) < 20: size = 13**2 - else: size = 15**2 - cs = self.m.scatter(x_ob, y_ob, s=size, c=departure, cmap=cmap, norm=norm, linewidths=1.25, ax=self.ax) - - # make custom legend - fontdict = {'family':'monospace', 'size':9 } - x0, y0 = self.ax.transAxes.transform((0,0)) - x, y = self.fig.transFigure.inverted().transform((x0+10,y0+10)) - w, h = self.fig.get_size_inches()*self.fig.dpi - cax = self.fig.add_axes([x,y,130/float(w),114/float(h)]) - cax.set_facecolor('#dddddd') - cax.set_xticks([]) - cax.set_yticks([]) - for i in list(cax.spines.values()): i.set_linewidth(0.5) - - sizes, start_y, start_c, labels = [5,8,11,13,15], 0.84, 1, ['0-5F', '5-10F', '10-15F', '15-20F', '>= 20F'] - cax.text(0.2,0.94,'Below', va='center', ha='center', fontdict=fontdict, transform=cax.transAxes) - cax.text(0.8,0.94,'Above', va='center', ha='center', fontdict=fontdict, transform=cax.transAxes) - for i in range(len(sizes)): - cax.scatter(0.2,start_y-i*0.18,s=sizes[i]**2,c=(-start_c-i*5), cmap=cmap, norm=norm, transform=cax.transAxes) - cax.text(0.5,start_y-i*0.18,labels[i], va='center', ha='center', fontdict=fontdict, transform=cax.transAxes) - cax.scatter(0.8,start_y-i*0.18,s=sizes[i]**2,c=(start_c+i*5), cmap=cmap, norm=norm, transform=cax.transAxes) - - def plotInterp(self): - with open('stations.txt') as f: content = f.readlines() - latlons = [ [ float(num[:-1]) for num in line.split()[-2:] ] for line in content ] - latlons = np.array(latlons)[:,::-1] - latlons[:,0] = -1*latlons[:,0] - - #latlons = zip(self.lons[10::40,10::40].flatten(), self.lats[10::40,10::40].flatten()) - - f = interpolate.RegularGridInterpolator((self.y[:,0], self.x[0,:]), self.data['fill'][0], fill_value=-9999, bounds_error=False, method='linear') - x_ob, y_ob = self.m(*list(zip(*latlons))) - fcst_val = f((y_ob,x_ob)) - - self.m.set_axes_limits(ax=self.ax) # need to do this if no other basemap functions have been called - - fontdict = {'family':'monospace', 'size':9 } - #self.ax.cla() - #self.ax.axis('off') - for i in range(fcst_val.size): - if x_ob[i] < self.m.xmax and x_ob[i] > self.m.xmin and y_ob[i] < self.m.ymax and y_ob[i] > self.m.ymin and fcst_val[i] != -9999: - self.ax.text(x_ob[i], y_ob[i], int(round(fcst_val[i])), fontdict=fontdict, ha='center', va='center') - - def plotReports(self): - #import csv, re, urllib2 - #url = 'http://www.spc.noaa.gov/climo/reports/%s_rpts_raw.csv'%(self.initdate.strftime('%y%m%d')) - #print url - #url = 'http://www.spc.noaa.gov/climo/reports/yesterday.csv' - #url = 'http://www.spc.noaa.gov/climo/reports/today.csv' - #response = urllib2.urlopen(url) - #cr = csv.reader(response) - - import sqlite3 - colors = ['red', 'green', 'blue'] - conn = sqlite3.connect('/glade/u/home/sobash/2013RT/REPORTS/reports_all.db') - c = conn.cursor() - #for i,table in enumerate(['reports_torn', 'reports_hail', 'reports_wind']): - for i,table in enumerate(['reports_torn', 'reports_hail', 'reports_wind']): - if table == 'reports_hail': c.execute("SELECT slat, slon, datetime FROM %s WHERE size > 2 AND datetime > '2015-06-04 18:00' AND datetime <= '2015-06-05 00:00' ORDER BY datetime asc"%table) - if table == 'reports_torn': c.execute("SELECT slat, slon, datetime FROM %s WHERE datetime > '2015-06-04 18:00' AND datetime <= '2015-06-05 00:00' ORDER BY datetime asc"%table) - if table == 'reports_wind': c.execute("SELECT slat, slon, datetime FROM %s WHERE datetime > '2015-06-04 18:00' AND datetime <= '2015-06-05 00:00' ORDER BY datetime asc"%table) - rpts = c.fetchall() - #rpts.extend(c.fetchall()) - olats, olons, dt = zip(*rpts) - x_ob, y_ob = self.m(olons, olats) - self.m.scatter(x_ob, y_ob, color=colors[i], edgecolor=None, s=25, ax=self.ax) - - #lats, lons, type = [], [], [] - #report_type = 'hail' - #for row in cr: - # if re.search('Hail', row[0]): report_type = 'hail' - # if re.search('Wind', row[0]): report_type = 'wind' - # if re.search('Tornado', row[0]): report_type = 'torn' - - # if re.search('^\d{4}', row[0]): - # lats.append(float(row[5])) - # lons.append(float(row[6])) - # type.append(report_type) - - #x_ob, y_ob = self.m(lons, lats) - #self.m.scatter(x_ob, y_ob, color='k', edgecolor='k', s=3, ax=self.ax) - - def plotWarnings(self): - try: self.m.readshapefile('/glade/u/home/sobash/SHARPpy/OBS/sbw_shp/%s'%self.initdate.strftime('%Y%m%d12'),'warnings',drawbounds=True, linewidth=1, color='black', ax=self.ax) - except IndexError: print('IndexError - likely no warnings present') - - def plotOutlook(self): - self.m.readshapefile('/glade/u/home/sobash/RT2015_gpx/outlook_shp/day1otlk_20150515_1200_cat','day1otlk_20150515_1200_cat',drawbounds=True, linewidth=1, color='green', ax=self.ax) - - def plotMRMS(self, thresh=40.0): - validstr = (self.initdate+timedelta(hours=self.shr)).strftime('%Y%m%d%H') - - # READ IN MRMS GRID - fh = xarray.open_dataset('/glade/p/nmmm0001/sobash/MRMS/mrms_grid.nc') - lats = fh['lat_0'] - lons = fh['lon_0'] - - # READ IN DATA - fh = xarray.open_dataset('/glade/scratch/sobash/mrms_tmp/cref_mrms_%s.nc'%validstr) - mrms = fh['CREF'] - - lats, lons = np.meshgrid(lats, lons) - x, y = self.m(lons, lats) - - cs1 = self.m.contourf(x, y, mrms.T, levels=[thresh,1000], colors=['k'], ax=self.ax) - - x0, y1 = self.ax.transAxes.transform((0,1)) - fontdict = {'family':'monospace', 'size':11} - self.ax.text(x0+10, y1-15, 'MRMS CREF >= %d dBZ'%thresh, fontdict=fontdict, transform=None) - - def plotMRMSmax(self, field='qpe'): - validstr = (self.initdate+timedelta(hours=self.shr-1)).strftime('%Y%m%d%H') - #validstr = (self.initdate+timedelta(hours=self.shr-1)).strftime('%Y%m%d00') - - # READ IN MRMS GRID - fh = xarray.open_dataset('/glade/p/nmmm0001/sobash/MRMS/mrms_grid.nc') - lats = fh['lat_0'] - lons = fh['lon_0'] - - # READ IN DATA - if field == 'qpe': - fname = '/glade/scratch/sobash/mrms_tmp/qpe_mrms_hrmax_%s.nc'%validstr - field = 'QPE01' - levels = [25.4,1000] - if field == 'cref': - fname = '/glade/scratch/sobash/mrms_tmp/cref_mrms_hrmax_%s.nc'%validstr - field = 'CREF' - levels = [40,1000] - - fh = xarray.open_dataset(fname) - mrms = fh[field] - - # plot - lats, lons = np.meshgrid(lats, lons) - x, y = self.m(lons, lats) - - cs1 = self.m.contourf(x, y, mrms.T, levels=levels, colors=['k'], ax=self.ax) + def get_mpas_mesh(self): + path = self.opts["init_file"] + mpas_mesh = xarray.open_dataset(path) + lonCell = mpas_mesh['lonCell'] + lonCell = np.degrees(lonCell) #convert radians to degrees + mpas_mesh["lonCell"] = lonCell + mpas_mesh["latCell"] = np.degrees(mpas_mesh["latCell"]) #convert radians to degrees + self.mpas_mesh = mpas_mesh def plotTitleTimes(self): if self.opts['over']: return @@ -292,34 +105,28 @@ def plotTitleTimes(self): self.ax.text(x1, y1+20, initstr, horizontalalignment='right', transform=None) self.ax.text(x1, y1+5, validstr, horizontalalignment='right', transform=None) - # Plot missing members (use wrfout count here, if upp missing this wont show that) - if len(self.missing_members['wrfout']) > 0: - missing_members = sorted(set([ (x%self.ENS_SIZE)+1 for x in self.missing_members['wrfout'] ])) #get member number from missing indices + # Plot missing members + if len(self.missing_members): + missing_members = sorted(set([ (x%self.ENS_SIZE)+1 for x in self.missing_members ])) #get member number from missing indices missing_members_string = ', '.join(str(x) for x in missing_members) self.ax.text(x1-5, y0+5, 'Missing member #s: %s'%missing_members_string, horizontalalignment='right') def plotFields(self): if 'fill' in self.data: if self.opts['fill']['ensprod'] == 'paintball': self.plotPaintball() - elif self.opts['fill']['ensprod'] in ['stamp', 'maxstamp']: self.plotStamp() + elif self.opts['fill']['ensprod'].endswith("stamp"): self.plotStamp() else: self.plotFill() if 'contour' in self.data: if self.opts['contour']['ensprod'] == 'spaghetti': self.plotSpaghetti() - elif self.opts['contour']['ensprod'] == 'stamp': self.plotStamp() + elif self.opts['contour']['ensprod'].endswith('stamp'): self.plotStamp() else: self.plotContour() if 'barb' in self.data: - #self.plotStreamlines() + assert not self.opts['contour']['ensprod'].endswith('stamp'), "TODO: postage stamp barbs" self.plotBarbs() - - if self.opts['fill']['name'] in ['t2depart', 'td2depart']: self.plotDepartures() def plotFill(self): - if self.opts['fill']['name'] == 'ptype': self.plotFill_ptype(); return - if self.opts['fill']['name'][0:6] == 'ptypes': self.plotFill_ptypes(); return - elif self.opts['fill']['name'] == 'crefuh': self.plotReflectivityUH(); return - if self.autolevels: min, max = self.data['fill'][0].min(), self.data['fill'][0].max() levels = np.linspace(min, max, num=10) @@ -338,103 +145,21 @@ def plotFill(self): norm = colors.BoundaryNorm(levels, cmap.N) data = self.data['fill'][0] + data = self.latlonGrid(data) # regrid 1D mesh that needs to be smoothed if self.opts['fill']['name'] in ['avo500', 'vort500', 'pbmin']: - print("plotFill: regridding 1D mesh "+self.opts['fill']['name']+" to lat lon") - data = self.latlonGrid(data) - # smooth some of the fill fields - if self.opts['fill']['name'] == 'avo500': data = ndimage.gaussian_filter(data, sigma=4) - if self.opts['fill']['name'] == 'vort500': data = ndimage.gaussian_filter(data, sigma=4) - if self.opts['fill']['name'] == 'pbmin' : data = ndimage.gaussian_filter(data, sigma=2) - cs1 = self.m.contourf(self.x2d, self.y2d, data, levels=levels, cmap=cmap, norm=norm, extend='max', ax=self.ax) - elif self.opts['fill']['ensprod'] in ['neprob', 'neprobgt', 'neproblt']: - data = self.latlonGrid(data) + # smooth some of the fill fields. use .values to preserve DataArray attributes. + data.values = ndimage.gaussian_filter(data, sigma=4) + elif self.opts['fill']['ensprod'].startswith('neprob'): miles_to_km = 1.60934 roi = 25 * miles_to_km / self.min_grid_spacing_km logging.debug(f"roi {roi}") data = compute_neprob(data, roi=int(roi), sigma=float(fields['sigma']), type='gaussian') data = data+0.001 #hack to ensure that plot displays discrete prob values -# assume the data array has been interpolated to lat/lon - cs1 = self.m.contourf(self.x2d, self.y2d, data, levels=levels, cmap=cmap, norm=norm, extend='max', ax=self.ax) - elif self.vtx is not None: # This is probably not an irregular mesh like MPAS - # use ibox and tri - # Sometimes you get a warning kwarg tri is ignored. - # Tried removing tri=True but got IndexError: too many indices for array - #print("plotFill: starting contourf with ",self.ibox.shape," array "+self.opts['fill']['name']) - #cs1 = self.m.contourf(self.x, self.y, self.data['fill'][0][self.ibox], tri=True, levels=levels, cmap=cmap, norm=norm, extend='max', ax=self.ax) #MPAS - data = self.latlonGrid(self.data['fill'][0]) - logging.debug("plotFill: starting contourf with 2d array") - cs1 = self.m.contourf(self.x2d, self.y2d, data, levels=levels, cmap=cmap, norm=norm, extend='max', ax=self.ax) - else: - # Added for HRRR - cs1 = self.m.contourf(self.x2d, self.y2d, data, levels=levels, cmap=cmap, norm=norm, extend='max', ax=self.ax) - - self.plotColorbar(cs1, levels, tick_labels, extend, extendfrac) - - def plotFill_ptype(self): - ml_type = np.zeros(self.data['fill'][0].shape) - ml_type_prob = np.zeros(self.data['fill'][0].shape) - - for i in [1,2,3,4]: - pts = (self.data['fill'][i-1] > ml_type_prob+0.001) - ml_type_prob[pts] = self.data['fill'][i-1][pts] - ml_type[pts] = i+0.001 - - cmap = colors.ListedColormap(['#7BBF6A', 'red', 'orange', 'blue']) - norm = colors.BoundaryNorm([1,2,3,4,5], cmap.N) - - x = (self.x[1:,1:] + self.x[:-1,:-1])/2.0 - y = (self.y[1:,1:] + self.y[:-1,:-1])/2.0 - cs1 = self.m.pcolormesh(x, y, np.ma.masked_equal(ml_type[1:,1:], 0), cmap=cmap, norm=norm, edgecolors='None', ax=self.ax) - - # make axes for colorbar, 175px to left and 30px down from bottom of map - x0, y0 = self.ax.transAxes.transform((0,0)) - x, y = self.fig.transFigure.inverted().transform((x0+175,y0-29.5)) - cax = self.fig.add_axes([x,y,0.985-x,y/3.0]) - cb = plt.colorbar(cs1, cax=cax, orientation='horizontal') - cb.outline.set_linewidth(0.5) - cb.set_ticks([0.5,1.5,2.5,3.5,4.5,5.5]) - cb.set_ticklabels(['Rain', 'Freezing Rain', 'Sleet', 'Snow']) - cb.ax.tick_params(length=0) - - def plotFill_ptypes(self): - types = self.data['fill'] - ntypes = len(types) - - # Plot where hourly precip of each type is greater than zero. - # Colors match the regular ptype plot, but are translucent, so you - # can see them underneath each other. - # Data arrays, colors, and levels defined in fieldinfo.py - alpha = 0.25 - colors = self.opts['fill']['colors'] - threshes = self.opts['fill']['levels'] - type_labels= self.opts['fill']['arrayname'] # Get label directly from fieldinfo arrayname. - type_labels = ['Rain', 'Snow', 'Sleet', 'Freezing Rain'] # Hard-coded - if any(len(lst) != ntypes for lst in [colors, threshes, type_labels]): - print("data, colors, threshes, and type_labels must all be same length") - print(ntypes, colors, threshes, type_labels) - sys.exit(1) - # make axes for colorbar, 175px to left and 35px down from bottom of map - x0, y0 = self.ax.transAxes.transform((0,0)) - x, y = self.fig.transFigure.inverted().transform((x0+175,y0-35)) - # Width of space where colorbar will go. - cbwidth = (0.985-x)/ntypes - for i in range(ntypes): - levels = self.opts['fill']['levels'][i] - # Mask pixels less than threshold - type = np.ma.masked_less(types[i],levels[0]) - cs = self.m.contourf(self.x, self.y, type, levels=[0,np.max(type)], colors=colors[i], ax=self.ax) - cax = self.fig.add_axes([x+(i*cbwidth),y,0.85*cbwidth,y/3.0]) - cs.set_alpha(alpha) - cb = plt.colorbar(cs, cax=cax, orientation='horizontal', ticks=[]) - cb.outline.set_visible(False) - cb.ax.set_title(type_labels[i]) - - data = ndimage.gaussian_filter(types[i], sigma=10) - cs2 = self.m.contour(self.x, self.y, data, colors=colors[i], levels=levels, linewidths=1.5, ax=self.ax) - cb = plt.colorbar(cs2, cax=cax, orientation='horizontal') - cb.ax.tick_params(labelsize=9,length=0) - cb.outline.set_edgecolor(colors[i]) + + cs1 = self.m.contourf(self.x2d, self.y2d, data, levels=levels, cmap=cmap, norm=norm, extend='max', ax=self.ax) + label = f"{data.long_name} [{data.units}]" + self.plotColorbar(cs1, levels, tick_labels, extend, extendfrac, label=label) def plotReflectivityUH(self): levels = self.opts['fill']['levels'] @@ -453,77 +178,69 @@ def plotReflectivityUH(self): for i in cs2.collections: i.remove() - #maxuh = self.data['fill'][1].max() - #self.ax.text(0.03,0.03,'Domain-wide UH max %0.f'%maxuh ,ha="left",va="top",bbox=dict(boxstyle="square",lw=0.5,fc="white"), transform=self.ax.transAxes) - self.plotColorbar(cs1, levels, tick_labels) - def plotColorbar(self, cs, levels, tick_labels, extend='neither', extendfrac=0.0): + def plotColorbar(self, cs, levels, tick_labels, extend='neither', extendfrac=0.0, label=""): # make axes for colorbar, 175px to left and 30px down from bottom of map x0, y0 = self.ax.transAxes.transform((0,0)) x, y = self.fig.transFigure.inverted().transform((x0+175,y0-29.5)) cax = self.fig.add_axes([x,y,0.985-x,y/3.0]) - cb = plt.colorbar(cs, cax=cax, orientation='horizontal', extend=extend, extendfrac=extendfrac, ticks=tick_labels) + cb = plt.colorbar(cs, cax=cax, orientation='horizontal', extend=extend, extendfrac=extendfrac, ticks=tick_labels, label=label) + cb.ax.xaxis.set_label_position('top') cb.outline.set_linewidth(0.5) def interpolatetri(self, values, vtx, wts): - logging.info("interpolatetri") return np.einsum('nj,nj->n', np.take(values, vtx), wts) def latlonGrid(self, data): - # TODO: Handle ensemble - 3d data + data = data.metpy.dequantify() # Allow units to transfer to gridded array via attribute # apply ibox to data data = data[self.ibox] if hasattr(self, "vtx") and hasattr(self, "wts"): - data_gridded = self.interpolatetri(data, self.vtx, self.wts) - logging.info("reshape") + logging.debug("latlonGrid: interpolatetri(vtx and wts)") + # by using .values, Avoid interpolatetri ValueError: dimensions ('nCells',) must have the same length as the number of data dimensions, ndim=2 + data_gridded = self.interpolatetri(data.values, self.vtx, self.wts) data_gridded = np.reshape(data_gridded, self.lat2d.shape) else: - logging.info("latlonGrid: interpolating to latlon grid with griddata()") - data_gridded = interpolate.griddata((self.lons, self.lats), data, (self.lon2d, self.lat2d), method='nearest') + logging.info("latlonGrid: interpolate to latlon grid with griddata()") + data_gridded = griddata((self.lons, self.lats), data, (self.lon2d, self.lat2d), method='nearest') + data_gridded = xarray.DataArray(data = data_gridded, coords = dict(lat=self.lat2d[:,0], lon=self.lon2d[0]), attrs=data.attrs) return data_gridded def plotContour(self): - if self.opts['contour']['name'] in ['sbcinh','mlcinh']: linewidth, alpha = 0.5, 0.75 else: linewidth, alpha = 1.5, 1.0 data = self.data['contour'][0] - if self.vtx is not None: # Interpolate to latlon Grid - data = self.latlonGrid(data) - if self.opts['contour']['name'] in ['t2-0c']: data = ndimage.gaussian_filter(data, sigma=2) - else: data = ndimage.gaussian_filter(data, sigma=25) + data_gridded = self.latlonGrid(data) + + if self.opts['contour']['name'] in ['t2-0c']: data_gridded.values = ndimage.gaussian_filter(data_gridded, sigma=2) + else: data_gridded.values = ndimage.gaussian_filter(data_gridded, sigma=25) - cs2 = self.m.contour(self.x2d, self.y2d, data, levels=self.opts['contour']['levels'], colors='k', linewidths=linewidth, ax=self.ax, alpha=alpha) + cs2 = self.m.contour(self.x2d, self.y2d, data_gridded, levels=self.opts['contour']['levels'], colors='k', linewidths=linewidth, ax=self.ax, alpha=alpha) plt.clabel(cs2, fontsize='small', fmt='%i') def plotBarbs(self): - skip = self.opts['barb']['skip'] - if self.domain != 'CONUS': skip = int(skip*0.45) - if self.domain == 'NA': skip = int(skip*2) + + skip = max([*self.x2d.shape, *self.y2d.shape])/self.nbarbs + skip = int(skip) + logging.debug(f"plotBarbs: nbarbs={self.nbarbs} skip={skip}") if self.opts['fill']['name'] == 'crefuh': alpha=0.5 else: alpha=1.0 - - logging.debug("plotBarbs: starting barbs") - # skip interval was intended for 2-D fields + logging.debug(f"plotBarbs: starting barbs {[x.name for x in self.data['barb']]}") + # skip interval intended for 2-D fields if len(self.x.shape) == 2: - cs2 = self.m.barbs(self.x[::skip,::skip], self.y[::skip,::skip], self.data['barb'][0][::skip,::skip], self.data['barb'][1][::skip,::skip], color='black', alpha=alpha, length=5.5, linewidth=0.25, sizes={'emptybarb':0.05}, ax=self.ax) + cs2 = self.m.barbs(self.x[::skip,::skip], self.y[::skip,::skip], self.data['barb'][0][::skip,::skip], self.data['barb'][1][::skip,::skip], + alpha=alpha, length=5.5, linewidth=0.25, sizes={'emptybarb':0.05}, ax=self.ax) if len(self.x.shape) == 1: u2d = self.latlonGrid(self.data['barb'][0]) v2d = self.latlonGrid(self.data['barb'][1]) # rotate vectors so they represent the direction properly on the map projection u10_rot, v10_rot, x, y = self.m.rotate_vector(u2d, v2d, self.lon2d, self.lat2d, returnxy=True) - #cs2 = self.m.barbs(self.x2d[::skip,::skip], self.y2d[::skip,::skip], u2d[::skip,::skip], v2d[::skip,::skip], color='black', alpha=alpha, length=5.5, linewidth=0.25, sizes={'emptybarb':0.05}, ax=self.ax) - cs2 = self.m.barbs(x[::skip,::skip], y[::skip,::skip], u10_rot[::skip,::skip], v10_rot[::skip,::skip], color='black', alpha=alpha, length=5.5, linewidth=0.25, sizes={'emptybarb':0.05}, ax=self.ax) + cs2 = self.m.barbs(x[::skip,::skip], y[::skip,::skip], u10_rot[::skip,::skip], v10_rot[::skip,::skip], + alpha=alpha, length=5.5, linewidth=0.25, sizes={'emptybarb':0.05}, ax=self.ax) - def plotStreamlines(self): - speed = np.sqrt(self.data['barb'][0]**2 + self.data['barb'][1]**2) - lw = 5*speed/speed.max() - cs2 = self.m.streamplot(self.x[0,:], self.y[:,0], self.data['barb'][0], self.data['barb'][1], color='k', density=3, linewidth=lw, ax=self.ax) - cs2.lines.set_alpha(0.5) - cs2.arrows.set_alpha(0.5) #apparently this doesn't work? - def plotPaintball(self): rects, labels = [], [] colorlist = self.opts['fill']['colors'] @@ -540,7 +257,8 @@ def plotSpaghetti(self): proxy = [] colorlist = self.opts['contour']['colors'] levels = self.opts['contour']['levels'] - data = ndimage.gaussian_filter(self.data['contour'][0], sigma=[0,4,4]) + data = self.data['contour'][0] + data.values = ndimage.gaussian_filter(data, sigma=[0,4,4]) for i in range(self.data['contour'][0].shape[0]): #cs = self.m.contour(self.x, self.y, data[i,:], levels=levels, colors=[colorlist[i]], linewidths=2, linestyles='solid', ax=self.ax) cs = self.m.contour(self.x, self.y, data[i,:], levels=levels, colors='k', alpha=0.6, linewidths=1, linestyles='solid', ax=self.ax) @@ -563,7 +281,6 @@ def plotStamp(self): levels = self.opts['fill']['levels'] cmap = colors.ListedColormap(self.opts['fill']['colors']) norm = colors.BoundaryNorm(levels, cmap.N) - filename = self.opts['fill']['filename'] memberidx = 0 for j in range(0,num_rows): @@ -587,9 +304,9 @@ def plotStamp(self): thisax.text(0.03,0.97,member+1,ha="left",va="top",bbox=dict(boxstyle="square",lw=0.5,fc="white"), transform=thisax.transAxes) # plot, unless file that has fill field is missing, then skip - if member not in self.missing_members[filename] and member < self.ENS_SIZE: - data = self.latlonGrid(self.data['fill'][0][memberidx,:]) - logging.debug(f"plotStamp: starting contourf with regridded array {memberidx}") + if member not in self.missing_members and member < self.ENS_SIZE: + data = self.latlonGrid(self.data['fill'][0].isel(mem=memberidx)) + logging.debug(f"plotStamp: starting contourf with regridded array member {memberidx}") cs1 = self.m.contourf(self.x2d, self.y2d, data, levels=levels, cmap=cmap, norm=norm, extend='max', ax=thisax) memberidx += 1 @@ -597,9 +314,10 @@ def plotStamp(self): if self.opts['fill']['name'] in ['goesch3', 'goesch4', 't2', 'precipacc' ]: ticks = levels[:-1][::2] # CSS added precipacc else: ticks = levels[:-1] + label = f"{data.long_name} [{data.units}]" # add colorbar to figure cax = fig.add_axes([0.51,0.3,0.48,0.02]) - cb = plt.colorbar(cs1, cax=cax, orientation='horizontal', ticks=ticks, extendfrac=0.0) + cb = plt.colorbar(cs1, cax=cax, orientation='horizontal', ticks=ticks, extendfrac=0.0, label=label) cb.outline.set_linewidth(0.5) cb.ax.tick_params(labelsize=9) @@ -614,34 +332,26 @@ def plotStamp(self): # add NCAR logo and text below logo x, y = fig.transFigure.transform((0.51,0)) fig.figimage(plt.imread('ncar.png'), xo=x, yo=y+15, zorder=1000) - plt.text(x+10, y+5, 'ensemble.ucar.edu', fontdict={'size':9, 'color':'#505050'}, transform=None) + #plt.text(x+10, y+5, 'ensemble.ucar.edu', fontdict={'size':9, 'color':'#505050'}, transform=None) def getInitValidStr(self): initstr = self.initdate.strftime(' Init: %a %Y-%m-%d %H UTC') - if len(self.fhr) == 1: - validstr = (self.initdate+datetime.timedelta(hours=self.fhr)).strftime('Valid: %a %Y-%m-%d %H UTC') - else: - shr = min(self.fhr) - ehr = max(self.fhr) - # match precip or precip-24hr, but not precipacc - # accept precip-24hr, precip-48hr, precip-120hr, etc. - if self.opts['fill']['name'] == 'precip' or is_precip_diff(self.opts['fill']['name']): - # do not subtract 1 from start hour if array is difference of accumulated precipitation - validstr1 = (self.initdate + datetime.timedelta(hours=shr)).strftime('%a %Y-%m-%d %H UTC') - else: - validstr1 = (self.initdate + datetime.timedelta(hours=shr-1)).strftime('%a %Y-%m-%d %H UTC') - validstr2 = (self.initdate+datetime.timedelta(hours=ehr)).strftime('%a %Y-%m-%d %H UTC') - validstr = "Valid: %s - %s"%(validstr1, validstr2) + shr = min(self.fhr) + ehr = max(self.fhr) + fmt = '%a %Y-%m-%d %H UTC' + validstr = "Valid: " + (self.initdate+datetime.timedelta(hours=shr)).strftime(fmt) + if ehr > shr: # range of valid times + validstr += " - " + (self.initdate+datetime.timedelta(hours=ehr)).strftime(fmt) return initstr, validstr def saveFigure(self, trans=False): # place NCAR logo 57 pixels below bottom of map, then save image if 'ensprod' in self.opts['fill']: # CSS needed incase not a fill plot - if not trans and self.opts['fill']['ensprod'] not in ['stamp', 'maxstamp']: + if not trans and not self.opts['fill']['ensprod'].endswith('stamp'): x, y = self.ax.transAxes.transform((0,0)) - #self.fig.figimage(plt.imread('ncar.png'), xo=x, yo=(y-44), zorder=1000) - plt.text(x+10, y-54, 'ensemble.ucar.edu', fontdict={'size':9, 'color':'#505050'}, transform=None) + self.fig.figimage(plt.imread('ncar.png'), xo=x, yo=(y-44)) + #plt.text(x+10, y-54, 'ensemble.ucar.edu', fontdict={'size':9, 'color':'#505050'}, transform=None) plt.savefig(self.outfile, dpi=90, transparent=trans) @@ -653,57 +363,65 @@ def saveFigure(self, trans=False): else: ncolors = 255 #command = '/glade/u/home/sobash/pngquant/pngquant %d %s --ext=.png --force'%(ncolors,self.outfile) if os.environ['NCAR_HOST'] == "cheyenne": - bindir= '/glade/u/home/ahijevyc/bin_cheyenne/' + quant = '/glade/u/home/ahijevyc/bin_cheyenne/pngquant' else: - bindir= '/glade/u/home/ahijevyc/bin/' - command = bindir + 'pngquant %d %s --ext=.png --force'%(ncolors,self.outfile) + quant = '/glade/u/home/ahijevyc/bin/pngquant' + command = f"{quant} {ncolors} {self.outfile} --ext=.png --force" ret = subprocess.check_call(command.split()) plt.clf() + logging.info(f"created {self.outfile}") def parseargs(): '''Parse arguments and return dictionary of fill, contour and barb field parameters''' parser = argparse.ArgumentParser(description='Web plotting script for NCAR ensemble') - parser.add_argument('-d', '--date', default=datetime.datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0), - help='initialization datetime') - parser.add_argument('--fhr', nargs='+', type=float, default=[12], help='list of forecast hours') - parser.add_argument('-f', '--fill', help='fill field (FIELD_PRODUCT_THRESH), field keys:'+','.join(list(fieldinfo.keys()))) - parser.add_argument('-c', '--contour', help='contour field (FIELD_PRODUCT_THRESH)') + parser.add_argument('date', help='initialization datetime') + parser.add_argument('--autolevels', action='store_true', help='use min/max to determine levels for plot') parser.add_argument('-b', '--barb', help='barb field (FIELD_PRODUCT_THRESH)') - parser.add_argument('-bs', '--barbskip', help='barb skip interval') - parser.add_argument('--ENS_SIZE', type=int, default=10, help='ensemble size') - parser.add_argument('-t', '--title', help='title for plot') - parser.add_argument('--nlon_max', default=1500, help='max pts in longitude dimension') - parser.add_argument('--nlat_max', default=1500, help='max pts in latitude dimension') - parser.add_argument('-m', '--mesh', default='rt2015', choices=['mpas','rt2015','hrrr','uni'], help='mesh') - parser.add_argument('-al', '--autolevels', action='store_true', help='use min/max to determine levels for plot') + parser.add_argument('-c', '--contour', help='contour field (FIELD_PRODUCT_THRESH)') parser.add_argument('-con', '--convert', default=True, action='store_false', help='run final image through imagemagick') - parser.add_argument('-i', '--interp', default=False, action='store_true', help='plot interpolated station values') - parser.add_argument('-sig', '--sigma', default=2, help='smooth probabilities using gaussian smoother') - parser.add_argument('--debug', action='store_true', help='turn on debugging') - parser.add_argument('-v', '--verif', default=None, help='plot verification data') + parser.add_argument('-d', '--debug', action='store_true', help='turn on debugging') + parser.add_argument('-f', '--fill', help='fill field (FIELD_PRODUCT_THRESH), field keys:'+','.join(list(fieldinfo.keys()))) + parser.add_argument('--fhr', nargs='+', type=float, default=[12], help='list of forecast hours') + parser.add_argument('--meshstr', type=str, default='uni', help='mesh id or path to defining mesh') + parser.add_argument('--nbarbs', type=int, default=50, help='max barbs in one dimension') + parser.add_argument('--nlon_max', type=int, default=1500, help='max pts in longitude dimension') + parser.add_argument('--nlat_max', type=int, default=1500, help='max pts in latitude dimension') parser.add_argument('--over', default=False, action='store_true', help='plot as overlay (no lines, transparent, no convert)') + parser.add_argument('-sig', '--sigma', default=2, help='smooth probabilities using gaussian smoother') + parser.add_argument('-t', '--title', help='title for plot') - opts = vars(parser.parse_args()) - if opts['interp']: opts['over'] = True - - # opts = { 'date':date, 'timerange':timerange, 'fill':'sbcape_prob_25', 'ensprod':'mean' ... } - # now, convert underscore delimited fill, contour, and barb args into dicts + opts = vars(parser.parse_args()) # argparse.Namespace in form of dictionary + if opts["debug"]: + logging.getLogger().setLevel(logging.DEBUG) + + # Based on meshstr, define ENS_SIZE, and init_file + meshstr = opts["meshstr"] + if meshstr in mesh_config: + opts["ENS_SIZE"], opts["init_file"] = mesh_config[meshstr] + else: + assert os.path.exists(meshstr), (f"--meshstr must be a recognized mesh id {mesh_config.keys()} " + "or path to file (with lat/lon). not '{meshstr}'") + opts["init_file"] = meshstr + opts["ENS_SIZE"] = 1 + meshstr = os.path.basename(os.path.dirname(meshstr)) + + # now, convert slash-delimited fill, contour, and barb args into dicts for f in ['contour','barb','fill']: thisdict = {} if opts[f] is not None: - input = opts[f].lower().split('_') + input = opts[f].split('/') - assert len(input) > 1, f"{f} has 2-3 components separated by _. Add '_mean'?" + assert len(input) > 1, f"{f} has 2-3 components separated by /. Add '/mean'?" thisdict['name'] = input[0] thisdict['ensprod'] = input[1] - thisdict['arrayname'] = fieldinfo[input[0]]['fname'] + thisdict['arrayname'] = fieldinfo[input[0]]['fname'] # name of variable in input file # assign contour levels and colors if (input[1] in ['prob', 'neprob', 'probgt', 'problt', 'neprobgt', 'neproblt', 'prob3d']): if len(input)<3: - print("your -f option has less than 3 components. It needs name, ensprod, and thresh.") + logging.error(f"your {f} option has less than 3 components. It needs name, ensprod, and thresh.") sys.exit(1) thisdict['thresh'] = float(input[2]) if int(opts['sigma']) != 40: thisdict['levels'] = np.arange(0.1,1.1,0.1) @@ -733,234 +451,60 @@ def parseargs(): if 'arraylevel' in fieldinfo[input[0]]: thisdict['arraylevel'] = fieldinfo[input[0]]['arraylevel'] - # get barb-skip for barb fields - if opts['barbskip'] is not None: thisdict['skip'] = int(opts['barbskip']) - elif 'skip' in fieldinfo[input[0]]: thisdict['skip'] = fieldinfo[input[0]]['skip'] - - # get filename - if 'filename' in fieldinfo[input[0]]: thisdict['filename'] = fieldinfo[input[0]]['filename'] - else: thisdict['filename'] = 'wrfout' - opts[f] = thisdict return opts -def makeEnsembleList(wrfinit, fhr, ENS_SIZE): - # create lists of files (and missing file indices) for various file types - file_list = { 'wrfout':[], 'upp': [], 'diag':[] } - missing_list = { 'wrfout':[], 'upp': [], 'diag':[] } - - EXP_DIR = os.getenv('EXP_DIR', '/glade/scratch/wrfrt/realtime_ensemble/ensf') - - missing_index = 0 - for hr in fhr: - wrfvalidstr = (wrfinit + datetime.timedelta(hours=hr)).strftime('%Y-%m-%d_%H:%M:%S') - yyyymmddhh = wrfinit.strftime('%Y%m%d%H') - for mem in range(1,ENS_SIZE+1): - wrfout = '%s/%s/wrf_rundir/ens_%d/wrfout_d02_%s'%(EXP_DIR,yyyymmddhh,mem,wrfvalidstr) - diag = '%s/%s/wrf_rundir/ens_%d/diags_d02.%s.nc'%(EXP_DIR,yyyymmddhh,mem,wrfvalidstr) - #diag = '/glade/scratch/sobash/FOR_MORRIS/%s/mem%d/diags_d02_f%03d.nc'%(yyyymmddhh,mem,hr) - diag = '/glade/scratch/sobash/FOR_MORRIS/%s/mem%d/diags_d02_f%03d.nc'%(yyyymmddhh,mem,hr) - upp = '%s/%s/post_rundir/mem_%d/fhr_%d/WRFTWO%02d.nc'%(EXP_DIR,yyyymmddhh,mem,hr,hr) - #ens1 = '/glade/p/nmm0001/romine/rt2015/ens_1km/%s/mem%02d_%s.nc'%(yyyymmddhh,mem,yyyymmddhh) - if os.path.exists(wrfout): file_list['wrfout'].append(wrfout) - else: missing_list['wrfout'].append(missing_index) - if os.path.exists(diag): file_list['diag'].append(diag) - else: missing_list['diag'].append(missing_index) - if os.path.exists(upp): file_list['upp'].append(upp) - else: missing_list['upp'].append(missing_index) - missing_index += 1 - return (file_list, missing_list) - -def makeEnsembleListStan(wrfinit, fhr, ENS_SIZE): - # create lists of files (and missing file indices) for various file types - file_list = { 'wrfout':[], 'upp': [], 'diag':[] } - missing_list = { 'wrfout':[], 'upp': [], 'diag':[] } - - EXP_DIR = os.getenv('EXP_DIR', '/glade/scratch/trier/jun4-5/') - - missing_index = 0 - for h in fhr: - wrfvalidstr = (wrfinit + datetime.timedelta(hours=h)).strftime('%Y-%m-%d_%H:%M:%S') - yyyymmddhh = wrfinit.strftime('%Y%m%d%H') - for mem in range(1,ENS_SIZE+1): - wrfout = '%s/ens_%d/wrfout_d02_%s'%(EXP_DIR,mem,wrfvalidstr) - diag = '%s/ens_%d/diags_d02.%s.nc'%(EXP_DIR,mem,wrfvalidstr) - print(diag) - if os.path.exists(wrfout): file_list['wrfout'].append(wrfout) - else: missing_list['wrfout'].append(missing_index) - if os.path.exists(diag): file_list['diag'].append(diag) - else: missing_list['diag'].append(missing_index) - missing_index += 1 - return (file_list, missing_list) - -def makeEnsembleListHREF(wrfinit, fhr, ENS_SIZE): - # create lists of files (and missing file indices) for various file types - file_list = { 'wrfout':[], 'diag':[] } - missing_list = { 'wrfout':[], 'diag':[] } - missing_index = 0 - wrfinit_prev = (wrfinit - datetime.timedelta(hours=12)) - - for hr in fhr: - yyyymmddhh = wrfinit.strftime('%Y%m%d%H') - yyyymmddhh_p = wrfinit_prev.strftime('%Y%m%d%H') - hr_p = hr - 12 - init = wrfinit.strftime('%H') - init_p = wrfinit_prev.strftime('%H') - - for mem in range(1,ENS_SIZE+1): - if mem == 1: diag = '/glade/scratch/sobash/HREF/%s/hiresw.t%sz.arw_5km.f%02d.conus.grib2'%(yyyymmddhh,init,hr) - if mem == 2: diag = '/glade/scratch/sobash/HREF/%s/hiresw.t%sz.arw_5km.f%02d.conusmem2.grib2'%(yyyymmddhh,init,hr) - if mem == 3: diag = '/glade/scratch/sobash/HREF/%s/hiresw.t%sz.nmmb_5km.f%02d.conus.grib2'%(yyyymmddhh,init,hr) - if mem == 4: diag = '/glade/scratch/sobash/HREF/%s/nam.t%sz.conusnest.hiresf%02d.tm00.grib2'%(yyyymmddhh,init,hr) - if mem == 5: diag = '/glade/scratch/sobash/HREF/%s/hiresw.t%sz.arw_5km.f%02d.conus.grib2'%(yyyymmddhh_p,init_p,hr_p) - if mem == 6: diag = '/glade/scratch/sobash/HREF/%s/hiresw.t%sz.arw_5km.f%02d.conusmem2.grib2'%(yyyymmddhh_p,init_p,hr_p) - if mem == 7: diag = '/glade/scratch/sobash/HREF/%s/hiresw.t%sz.nmmb_5km.f%02d.conus.grib2'%(yyyymmddhh_p,init_p,hr_p) - if mem == 8: diag = '/glade/scratch/sobash/HREF/%s/nam.t%sz.conusnest.hiresf%02d.tm00.grib2'%(yyyymmddhh_p,init_p,hr_p) - - print(diag) - - if os.path.exists(diag): file_list['diag'].append(diag) - else: missing_list['diag'].append(missing_index) - missing_index += 1 - - - return (file_list, missing_list) - -def makeEnsembleListHRRR(wrfinit, fhr, ENS_SIZE): - # create lists of files (and missing file indices) for various file types - file_list = { 'diag':[], 'wrfout':[]} - missing_list = { 'diag':[], 'wrfout':[]} - missing_index = 0 - - for hr in fhr: - yyyymmddhh = wrfinit.strftime('%Y%m%d%H') - yyyymmdd = wrfinit.strftime('%Y%m%d') - init = wrfinit.strftime('%H') - - for mem in range(1,ENS_SIZE+1): - if mem == 1: diag = '/glade/scratch/ahijevyc/hrrr/%s/%s_i%s_f%03d_HRRR-NCEP_wrfprs.nc'%(yyyymmddhh,yyyymmdd,init,hr) - print(diag) - - if os.path.exists(diag): file_list['diag'].append(diag) - else: missing_list['diag'].append(missing_index) - missing_index += 1 - - - return (file_list, missing_list) - -def makeEnsembleListArchive(wrfinit): - # create lists of files (and missing file indices) for various file types - file_list = { 'wrfout':[], 'upp': [], 'diag':[] } - missing_list = { 'wrfout':[], 'upp': [], 'diag':[] } - - missing_index = 0 - yyyymmddhh = wrfinit.strftime('%Y%m%d%H') - for mem in range(1,11): - ens1 = '/glade/scratch/sobash/RT2015/%s/mem%d_surrogate_%s.nc'%(yyyymmddhh,mem,yyyymmddhh) - #ens1 = '/glade/scratch/sobash/RT2013_1KMENS/%s/mem%02d_%s.nc'%(yyyymmddhh,mem,yyyymmddhh) - print(ens1) - if os.path.exists(ens1): file_list['wrfout'].append(ens1) - else: missing_list['wrfout'].append(missing_index) - if os.path.exists(ens1): file_list['diag'].append(ens1) - else: missing_list['diag'].append(missing_index) - if os.path.exists(ens1): file_list['upp'].append(ens1) - else: missing_list['upp'].append(missing_index) - missing_index += 1 - return (file_list, missing_list) - -def makeEnsembleListNSC(wrfinit, fhr): - # create lists of files (and missing file indices) for various file types - file_list = { 'wrfout':[], 'diag':[] } - missing_list = { 'wrfout':[], 'diag':[] } - - EXP_DIR = os.getenv('EXP_DIR', '/glade/scratch/wrfrt/realtime_ensemble/ensf') - - missing_index = 0 - for hr in fhr: - wrfvalidstr = (wrfinit + datetime.timedelta(hours=hr)).strftime('%Y-%m-%d_%H_%M_%S') - yyyymmddhh = wrfinit.strftime('%Y%m%d%H') - for mem in range(1,2): - diag = '%s/%s/diags_d01_%s.nc'%(EXP_DIR,yyyymmddhh,wrfvalidstr) - print(diag) - if os.path.exists(diag): file_list['diag'].append(diag) - else: missing_list['diag'].append(missing_index) - missing_index += 1 - return (file_list, missing_list) - -def makeEnsembleListDA(wrfinit, fhr): - # create lists of files (and missing file indices) for various file types - file_list = { 'wrfout':[], 'diag':[] } - missing_list = { 'wrfout':[], 'diag':[] } - - #EXP_DIR = os.getenv('EXP_DIR', '/glade/scratch/hclin/CONUS/wrfda/expdir/rt/fcst_15km') - #EXP_DIR = os.getenv('EXP_DIR', '/glade/scratch/sobash/VSE/1km_pbl7') - EXP_DIR = os.getenv('EXP_DIR', '/glade/scratch/schwartz/VSE/3km_pbl7') - missing_index = 0 - for hr in fhr: - wrfvalidstr = (wrfinit + datetime.timedelta(hours=hr)).strftime('%Y-%m-%d_%H:%M:%S') - yyyymmddhh = wrfinit.strftime('%Y%m%d%H') - for mem in range(1,2): - wrfout = '%s/%s/wrfout_d01_%s'%(EXP_DIR,yyyymmddhh,wrfvalidstr) - #diag = '%s/%s/wrf/join/vse_d01.%s.nc'%(EXP_DIR,yyyymmddhh,wrfvalidstr) - diag = '%s/%s/wrf/diags_d01.%s.nc'%(EXP_DIR,yyyymmddhh,wrfvalidstr) - print(diag) - if os.path.exists(wrfout): file_list['wrfout'].append(wrfout) - else: missing_list['wrfout'].append(missing_index) - if os.path.exists(diag): file_list['diag'].append(diag) - else: missing_list['diag'].append(missing_index) - missing_index += 1 - return (file_list, missing_list) - -def readEnsemble(wrfinit, fhr=None, fields=None, ENS_SIZE=10, mesh=None): + +def readEnsemble(Plot): + initdate = Plot.initdate + fhr = Plot.fhr + fields = Plot.opts + ENS_SIZE = Plot.ENS_SIZE ''' Reads in desired fields and returns 2-D arrays of data for each field (barb/contour/field) ''' logging.debug(fields) datadict = {} - file_list = defaultdict(lambda: []) - missing_list = defaultdict(lambda: []) - #file_list, missing_list = makeEnsembleList(wrfinit, fhr, ENS_SIZE) #construct list of files - #file_list, missing_list = makeEnsembleListNSC(wrfinit, fhr) #construct list of files - #file_list, missing_list = makeEnsembleListStan(wrfinit, fhr, ENS_SIZE) #construct list of files - if mesh == 'mpas': - file_list, missing_list = makeEnsembleListMPAS(wrfinit, fhr, ENS_SIZE, g193=False) #construct list of files - if mesh == 'uni' and ENS_SIZE == 1: + file_list = [] + missing_list = [] + if Plot.meshstr in ['15-3km_mesh', '15km_mesh']: + file_list, missing_list = makeEnsembleList(Plot) #construct list of files + elif Plot.meshstr == 'uni' and ENS_SIZE == 1: idir = "/glade/campaign/mmm/parc/ahijevyc/MPAS" - file_list['diag'] = [os.path.join(idir, mesh, wrfinit.strftime('%Y%m%d%H'), - (wrfinit+datetime.timedelta(hours=f)).strftime('diag.%Y-%m-%d_%H.%M.%S.nc')) for f in fhr] - #file_list, missing_list = makeEnsembleListArchive(wrfinit, fhr) #construct list of files - #file_list, missing_list = makeEnsembleListHybrid(wrfinit, fhr) #construct list of files - #file_list, missing_list = makeEnsembleListHREF(wrfinit, fhr, ENS_SIZE) #construct list of files - #file_list, missing_list = makeEnsembleListHRRR(wrfinit, fhr, 1) #construct list of files - if not file_list: - logging.error("no Ensemble file list. Exiting.") + file_list = [os.path.join(idir, Plot.meshstr, initdate.strftime('%Y%m%d%H'), + (initdate+datetime.timedelta(hours=f)).strftime('diag.%Y-%m-%d_%H.%M.%S.nc')) for f in fhr] + else: + logging.error("no ensemble files. Exiting.") logging.error("Perhaps add --mesh mpas to make_webplot.py command line") sys.exit(1) # loop through fill field, contour field, barb field and retrieve required data for f in ['fill', 'contour', 'barb']: if not list(fields[f].keys()): continue - logging.debug(f"Reading field: {fields[f]['name']} from {fields[f]['filename']}") + logging.info(f"read {fields[f]['name']}") # save some variables for use in this function - filename = fields[f]['filename'] arrays = fields[f]['arrayname'] fieldtype = fields[f]['ensprod'] fieldname = fields[f]['name'] if fieldtype in ['prob', 'neprob', 'problt', 'probgt', 'neprobgt', 'neproblt', 'prob3d']: thresh = fields[f]['thresh'] if fieldtype.startswith('mem'): member = int(fieldtype[3:]) - # open Multi-file netcdf dataset - logging.debug(f"opening xarray mfdataset {' '.join(file_list[filename])}") - - fh = xarray.open_mfdataset(file_list[filename],engine='netcdf4',combine="nested", concat_dim='Time') - # This concatenation dimension includes different times AND members. - fh = fh.rename({'Time':'TimeMember'}) - + # open xarray Dataset + logging.info(f"opening xarray Dataset. {len(fhr)} forecast hours x {ENS_SIZE} members") + paths = file_list + logging.debug(f"1-d paths = {paths}") + paths = np.array(paths).reshape(len(fhr), ENS_SIZE).tolist() + logging.debug(f"2-d paths = {paths}") + # Use a nested list of paths so xarray assigns data to both "Time" and "mem" dimensions. + fh = xarray.open_mfdataset(paths,engine='netcdf4',combine="nested", concat_dim=['Time','mem']) + # turn XTIME = b'2017-05-02_00:00:00 ' to proper datetime. + # use .load() or only a '2' will be read + fh["Time"] = pd.to_datetime(fh.xtime.load().isel(mem=0).astype(str).str.strip(), format="%Y-%m-%d_%H:%M:%S") + # loop through each field, wind fields will have two fields that need to be read datalist = [] for n,array in enumerate(arrays): logging.debug(f'Reading data {array}') - #read in 3D array (times*members,ny,nx) from file object if 'arraylevel' in fields[f]: if isinstance(fields[f]['arraylevel'], list): level = fields[f]['arraylevel'][n] @@ -971,171 +515,112 @@ def readEnsemble(wrfinit, fhr=None, fields=None, ENS_SIZE=10, mesh=None): elif level is None: data = fh[array] else: data = fh[array].sel(level=level) - data = data.values # use the numpy array, not the full xarray object. - # Many things that come afterward assume a numpy array, like flatten method. - if fh[array].dims[1] == 'nVertices': - logging.debug("field on vertices, like vorticity_500hPa, put on cells") - fieldv = data - nEdgesOnCell, verticesOnCell = readMPASVertices() + if 'nVertices' in fh[array].dims: + logging.info(f"{fieldname} is on vertices. Put on cells") + # .load() to avoid dask PerformanceWarning: Reshaping is producing a large chunk. + data = data.load().stack(TimeMem=("Time","mem")).T + mpas_mesh = Plot.mpas_mesh + nEdgesOnCell = mpas_mesh.nEdgesOnCell + verticesOnCell = mpas_mesh.verticesOnCell + maxEdges = mpas_mesh.maxEdges.size + nVertLevels = data.TimeMem.size # for now treat Time like vertical dimension + nCells = mpas_mesh.nCells.size + nVertices = data.nVertices.size # verticesOnCell is the transpose of what mpas_vort_cell1 expects - fieldc = mpas_vort_cell.mpas_vort_cell1(nEdgesOnCell, verticesOnCell.T, fieldv) - data = fieldc - - - #data = data.reshape((10,data.shape[1])) - #data = np.swapaxes(data,0,1) # flip first two axes so time is first - - # if all times are in one file, then need to reshape and extract desired times - #data = data.reshape((10,49,*spatial_dimensions)) # reshape - #data = data[:,timerange[0]:timerange[1]+1,:,:] # extract desired times - #data = np.swapaxes(data,0,1) # flip first two axes so time is first - #data = data.reshape((10*((timerange[1]+1)-timerange[0])),data.shape[2],data.shape[3]) #reshape again - - + dataCells = vert2cell.vert2cell(nEdgesOnCell, verticesOnCell.T, maxEdges, nVertLevels, nCells, nVertices, data) + # Assign to new DataArray with nCells dimension. transfer DataArray attributes + data = xarray.DataArray(data = dataCells, coords = dict(TimeMem=data.TimeMem, nCells=fh.nCells), attrs=data.attrs).unstack() # change units for certain fields - if array in ['u10','v10']: data = data*1.93 # m/s > kt - elif re.compile("^uzonal_\d\d+[A-Za-z]+").match(array): data = data*1.93 # m/s > kt - elif re.compile("^umeridional_\d\d+[A-Za-z]+").match(array): data = data*1.93 # m/s > kt - elif re.compile("^[uv]_pv$").match(array): data = data*1.93 # m/s > kt - elif array in ['dewpoint_surface', 't2m']: data = (data - 273.15)*1.8 + 32.0 # K > F - elif array in ['precipw']: data = data*0.0393701 # mm > in - elif array in ['rainnc', 'grpl_max', 'SNOW_ACC_NC']: data = data*0.0393701 # mm > in - elif array in ['T_PL', 'TD_PL', 'SFC_LI']: data = data - 273.15 # K > C - elif re.compile("^temperature_\d\d+hPa$").match(array):data = data - 273.15 # K > C - elif re.compile("^dewpoint_\d\d+hPa$").match(array): data = data - 273.15 # K > C - elif array in ['mslp']: data = data*0.01 # Pa > hPa - elif array in ['UP_HELI_MIN']: data = np.abs(data) - elif array in ['w_velocity_min']: data = data*-1.0 - elif array in ['PVORT_320K']: data = data*1000000 # multiply by 1e6 - elif re.compile("^vorticity_\d\d+hPa").match(array): data = data * 1e5 - elif array == "vort_pv": data = data * 1e5 - elif array in ['PBMIN', 'PBMIN_SFC', 'BESTPBMIN', 'MLPBMIN', 'MUPBMIN']: data = data*0.01 # Pa -> hPa -# elif array in ['LTG1_MAX1', 'LTG2_MAX', 'LTG3_MAX']: data = data*0.20 # scale down excess values + if array in ['u10','v10']: data = data.metpy.convert_units("knot") + elif "zonal" in array or "meridional" in array: data = data.metpy.convert_units("knot") + elif array in ['dewpoint_surface', 't2m']: data = data.metpy.convert_units("degF") + elif array in ['precipw','rainnc','grpl_max','SNOW_ACC_NC']:data = data.metpy.convert_units("inch") + elif array in ['T_PL', 'TD_PL', 'SFC_LI']: data = data.metpy.convert_units("Celsius") + elif array.startswith("temp"): data = data.metpy.convert_units("Celsius") + elif array.startswith("dewp"): data = data.metpy.convert_units("Celsius") + elif array == 'mslp' or 'PB' in array: data = data.metpy.convert_units("hPa") datalist.append(data) # these are derived fields, we don't have in any of the input files but we can compute - logging.info(f"datalist[0].shape={datalist[0].shape}") if 'name' in fields[f]: if fieldname in ['shr06mag', 'shr01mag']: - # derive wind shear from top and bottom level + logging.info(f"derive {fieldname} from {arrays}") # Assume datalist is 4-element list: [bottom_u, bottom_v, top_u, top_v] - datalist = [np.sqrt((datalist[0]-datalist[2])**2 + (datalist[1]-datalist[3])**2)] - elif fieldname[0:5] == 'speed' or fieldname in ['bunkmag']: - # derive speed from u and v components. different than wrf. It already has speed in S_PL array. - datalist = [np.sqrt(datalist[0]**2 + datalist[1]**2)] + shearmag = ((datalist[0]-datalist[2])**2 + (datalist[1]-datalist[3])**2)**0.5 + shearmag.attrs["long_name"] = fieldname.replace("shr","").replace("0","0-").replace("mag", "km shear magnitude") + datalist = [shearmag] + elif fieldname.startswith('speed') or fieldname == 'bunkmag': + logging.info(f"derive speed from {arrays}") + speed = (datalist[0]**2 + datalist[1]**2)**0.5 + speed.attrs["long_name"] = datalist[0].attrs["long_name"].replace("zonal wind"," wind") + datalist = [speed] elif fieldname in ['shr06','shr01']: datalist = [datalist[0]-datalist[2], datalist[1]-datalist[3]] elif fieldname == 'uhratio': datalist = [compute_uhratio(datalist)] elif fieldname == 'stp': datalist = [computestp(datalist)] # GSR in fields are T(K), mixing ratio (kg/kg), and surface pressure (Pa) elif fieldname == 'thetae': datalist = [compute_thetae(datalist)] elif fieldname == 'rh2m': datalist = [compute_rh(datalist)] - elif fieldname == 'sspf': datalist = [ compute_sspf(datalist, file_list['upp']) ] #elif fieldname == 'pbmin': datalist = [ datalist[1] - datalist[0][:,0,:] ] elif fieldname == 'pbmin': datalist = [ datalist[1] - datalist[0] ] # CSS changed above line for GRIB2 elif fieldname in ['thck1000-500', 'thck1000-850'] : datalist = [ datalist[1]*0.1 - datalist[0]*0.1 ] # CSS added for thicknesses datadict[f] = [] for data in datalist: - - - if is_precip_diff(fieldname): - logging.debug("Deriving accumulated precipitation. Subract ensemble at first time from ensemble at last time") - for i in missing_list[filename]: data = np.insert(data, i, np.nan, axis=0) #insert nan for missing files - # last and first time in the requested time range - ensemble_at_last_time = data[-ENS_SIZE:] - ensemble_at_first_time = data[:ENS_SIZE] - data = ensemble_at_last_time - ensemble_at_first_time - - # perform mean/max/variance/etc to reduce 3D array to 2D - spatial_dimensions = data.shape[1:] # works for 1D meshes like MPAS and 2D grids like WRF - ntimes = int(data.shape[0]/ENS_SIZE) - if (fieldtype == 'mean'): data = np.mean(data, axis=0) - elif (fieldtype == 'pmm'): data = compute_pmm(data) - elif (fieldtype == 'max'): data = np.amax(data, axis=0) - elif (fieldtype == 'min'): data = np.amin(data, axis=0) - elif (fieldtype == 'var'): data = np.std(data, axis=0) - elif (fieldtype == 'maxstamp'): - for i in missing_list[filename]: data = np.insert(data, i, np.nan, axis=0) #insert nan for missing files - data = np.reshape(data, (ntimes,ENS_SIZE,*spatial_dimensions)) - data = np.nanmax(data, axis=0) - elif (fieldtype == 'summean'): - for i in missing_list[filename]: data = np.insert(data, i, np.nan, axis=0) #insert nan for missing files - data = np.reshape(data, (ntimes,ENS_SIZE,*spatial_dimensions)) - data = np.nansum(data, axis=0) - data = np.nanmean(data, axis=0) - elif (fieldtype == 'maxmean'): - for i in missing_list[filename]: data = np.insert(data, i, np.nan, axis=0) #insert nan for missing files - data = np.reshape(data, (ntimes,ENS_SIZE,*spatial_dimensions)) - data = np.nanmax(data, axis=0) - data = np.nanmean(data, axis=0) - elif (fieldtype == 'summax'): - for i in missing_list[filename]: data = np.insert(data, i, np.nan, axis=0) #insert nan for missing files - data = np.reshape(data, (ntimes,ENS_SIZE,*spatial_dimensions)) - data = np.nansum(data, axis=0) - data = np.nanmax(data, axis=0) - elif (fieldtype[0:3] == 'mem'): - for i in missing_list[filename]: data = np.insert(data, i, np.nan, axis=0) #insert nan for missing files - data = np.reshape(data, (ntimes,ENS_SIZE,*spatial_dimensions)) - data = np.nanmax(data, axis=0) - data = data[member-1,:] - elif (fieldtype in ['prob', 'neprob', 'problt', 'probgt', 'neprobgt', 'neproblt']): + if fieldname.startswith("precip"): + fmt = '%a %Y-%m-%d %H UTC' + logging.info(f"Derive accumulated precipitation {data.Time[0].dt.strftime(fmt).data} - {data.Time[-1].dt.strftime(fmt).data}") + # subtract first time from last time + ensemble_at_last_time = data.isel(Time=[-1]) # brackets preserve Time dimension. + ensemble_at_first_time = data.isel(Time=[0]) + data = ensemble_at_last_time # use last time for output data + data.data -= ensemble_at_first_time.data # .data preserves quantity units, metadata, avoids pint.errors.DimensionalityError: Cannot convert from 'dimensionless' to 'inch' + + logging.info(f"perform {fieldtype} on {data.shape} data") + if (fieldtype == 'mean'): + data = data.mean(dim=["Time","mem"], keep_attrs=True) + elif (fieldtype == 'pmm'): + data = compute_pmm(data) + elif (fieldtype == 'max'): + data = data.max(dim=["Time","mem"], keep_attrs=True) + elif (fieldtype == 'min'): + data = data.min(dim=["Time","mem"], keep_attrs=True) + elif (fieldtype == 'var'): + data = data.std(dim=["Time","mem"], keep_attrs=True) + elif (fieldtype == 'maxstamp'): + data = data.max(dim="Time", keep_attrs=True) + elif (fieldtype == 'meanstamp'): + data = data.mean(dim="Time", keep_attrs=True) + elif (fieldtype == 'summean'): + data = data.sum(dim="Time", keep_attrs=True) + data = data.mean(dim="mem", keep_attrs=True) + elif (fieldtype == 'maxmean'): + data = data.max(dim="Time", keep_attrs=True) + data = data.mean(dim="mem", keep_attrs=True) + elif (fieldtype == 'summax'): + data = data.sum(dim="Time", keep_attrs=True) + data = data.max(dim="mem", keep_attrs=True) + elif (fieldtype[0:3] == 'mem'): + data = data.sel(mem=member) + elif 'prob' in fieldtype: if fieldtype.endswith('prob') or fieldtype.endswith('gt'): data = (data>=thresh).astype('float') elif fieldtype.endswith('lt'): data = (data=thresh).astype('float') - for i in missing_list[filename]: data = np.insert(data, i, np.nan, axis=0) - data = np.reshape(data, (ntimes,ENS_SIZE,*spatial_dimensions)) data = compute_prob3d(data, roi=14, sigma=float(fields['sigma']), type='gaussian') - logging.debug(f'field {fieldname} has shape {data.shape} range {data.min()}-{data.min()}') - - logging.info(f"max={data.max()}") - #kernel = np.ones((7,7)) - #data = ndimage.filters.convolve(data, kernel/float(kernel.sum())) - # attach data arrays for each type of field (e.g. { 'fill':[data], 'barb':[data,data] }) - datadict[f].append(data) - - fh.close() - - return (datadict, missing_list) - - -def is_precip_diff(s): - # Is this string an accumulated precipitation difference field? - # Assume it is if it starts with "precip-", ends with "hr" - if s[0:7] == "precip-" and s[-2:] == "hr": - return True - return False - - -def readGrid(file_dir): - f = xarray.open_dataset(file_dir) - lats = f['XLAT'] - lons = f['XLONG'] - return (lats,lons) - -def readMPASVertices(ifile="/glade/p/mmm/parc/schwartz/MPAS/15-3km_mesh/init.nc"): - # Used for regridding field from vertex to cell. latVertex and lonVertex not needed - fh = xarray.open_dataset(ifile) - nEdgesOnCell = fh['nEdgesOnCell'] - verticesOnCell = fh['verticesOnCell'] - return (nEdgesOnCell, verticesOnCell) - -def readGridMPAS(init_file="/glade/p/mmm/parc/schwartz/MPAS/15-3km_mesh/init.nc"): - logging.debug(f"open {init_file}") - fh = xarray.open_dataset(init_file) - latCell = fh['latCell'] - lonCell = fh['lonCell'] - areaCell = fh['areaCell'] # units m^2 - min_grid_spacing_km = 2. * np.sqrt(areaCell.min()/np.pi/1000/1000) - # min_grid_spacing_km used for grid spacing of interpolated lat-lon grid. - fh.close() - latCell, lonCell = np.degrees(latCell), np.degrees(lonCell) #convert radians to degrees - lonCell[lonCell >= 180] = lonCell[lonCell >= 180] - 360 - return (latCell, lonCell, min_grid_spacing_km) + logging.debug(f'field {fieldname} has shape {data.shape} range {data.min()}-{data.min()}') -def saveNewMap(plot, pk_file, init_file="/glade/p/mmm/parc/schwartz/MPAS/15-3km_mesh/init.nc"): + datadict[f].append(data) + + return datadict, missing_list + + + + + +def saveNewMap(plot, pk_file): + logging.info(f"saveNewMap: pk_file={pk_file}") ll_lat, ll_lon, ur_lat, ur_lon = domains[plot.domain]['corners'] lat_1, lat_2, lon_0 = 32., 46., -101. fig_width = 1080 @@ -1147,12 +632,10 @@ def saveNewMap(plot, pk_file, init_file="/glade/p/mmm/parc/schwartz/MPAS/15-3km_ # compute height based on figure width, map aspect ratio, then add some vertical space for labels/colorbar fig_width = fig_width/float(dpi) fig_height = fig_width*m.aspect + 0.93 - #fig_height = fig_width*m.aspect + 1.25 figsize = (fig_width, fig_height) fig.set_size_inches(figsize) # place map 0.7" from bottom of figure, leave rest of 0.93" at top for title (needs to be in figure-relative coords) - #x,y,w,h = 0.01, 0.8/float(fig_height), 0.98, 0.98*fig_width*m.aspect/float(fig_height) #too much padding at top x,y,w,h = 0.01, 0.7/float(fig_height), 0.98, 0.98*fig_width*m.aspect/float(fig_height) ax = fig.add_axes([x,y,w,h]) for i in list(ax.spines.values()): i.set_linewidth(0.5) @@ -1160,77 +643,57 @@ def saveNewMap(plot, pk_file, init_file="/glade/p/mmm/parc/schwartz/MPAS/15-3km_ m.drawcoastlines(linewidth=0.5, ax=ax) m.drawstates(linewidth=0.25, ax=ax) m.drawcountries(ax=ax) - # avoid this error - # UnicodeDecodeError: 'utf-8' codec can't decode byte 0xf1 in position 2: invalid continuation byte - #m.drawcounties(linewidth=0.1, color='gray', ax=ax) - - - if plot.mesh == 'hrrr': - lons=None - lats=None - min_grid_spacing_km = None - delta_deg = None - # Can we - fh = xarray.open_dataset("/glade/scratch/ahijevyc/hrrr/2018120106/20181201_i06_f024_HRRR-NCEP_wrfprs.nc") - lat2d = fh['gridlat_0'] - lon2d = fh['gridlon_0'] - x2d, y2d = m(lon2d,lat2d) - ibox = None - x = None - y = None - vtx = None - wts = None - else: - # load lat/lons - lats, lons, min_grid_spacing_km = readGridMPAS(init_file=init_file) - delta_deg = min_grid_spacing_km / 111 - if m.lonmin > 180 or m.lonmax > 180: - lons[lons<0] = lons[lons<0] + 360. # change -180-0 to 180-360 to match m.lonmin and m.lonmax - # Replace m.lonmax with ur_lon? Otherwise, risk getting +179.999 when NW corner crosses the datetime - nlon = int((m.lonmax - m.lonmin)/delta_deg) - nlat = int((m.latmax - m.latmin)/delta_deg) - if nlon > plot.nlon_max: - nlon = plot.nlon_max - if nlat > plot.nlat_max: - nlat = plot.nlat_max - lon2d, lat2d = np.meshgrid(np.linspace(m.lonmin, m.lonmax, nlon), np.linspace(m.latmin,m.latmax,nlat)) - # Convert to map coordinates instead of latlon to avoid the need to specify latlon=True in contour and barb methods. - x2d, y2d = m(lon2d,lat2d) - # ibox: subscripts within lat/lon box # only used to speed up 1-D array triangulation and plotting - ibox = (m.lonmin-1 <= lons ) & (lons < m.lonmax+1) & (m.latmin-1 <= lats) & (lats < m.latmax+1) - lons = lons[ibox] - lats = lats[ibox] - x, y = m(lons,lats) - - logging.info("interp_weights") - vtx, wts = interp_weights(np.vstack((lons,lats)).T,np.vstack((lon2d.flatten(), lat2d.flatten())).T) - - pickle.dump((fig,ax,m,lons,lats,min_grid_spacing_km,delta_deg,lon2d,lat2d,x2d,y2d,ibox,x,y,vtx,wts), open(pk_file, 'wb')) - + m.drawcounties(linewidth=0.1, color='gray', ax=ax) + # lat/lons from mpas_mesh file + mpas_mesh = plot.mpas_mesh + # min_grid_spacing_km used for grid spacing of interpolated lat-lon grid. + min_grid_spacing_km = 2. * np.sqrt(plot.mpas_mesh["areaCell"].min()/np.pi)/1000 + + lats = mpas_mesh["latCell"] + lons = mpas_mesh["lonCell"] + delta_deg = min_grid_spacing_km / 111 + if lons.max() > 180: + lons[lons>180] -= 360 + # Replace m.lonmax with ur_lon? Otherwise, risk getting +179.999 when NW corner crosses the datetime + nlon = int((m.lonmax - m.lonmin)/delta_deg) + nlat = int((m.latmax - m.latmin)/delta_deg) + nlon = np.clip(nlon, 1, plot.nlon_max) + nlat = np.clip(nlat, 1, plot.nlat_max) + lon2d, lat2d = np.meshgrid(np.linspace(m.lonmin, m.lonmax, nlon), np.linspace(m.latmin,m.latmax,nlat)) + # Convert to map coordinates instead of latlon to avoid latlon=True in contour and barb methods. + x2d, y2d = m(lon2d,lat2d) + # ibox: subscripts within lat/lon box. speed up triangulation in interp_weights + ibox = (m.lonmin-1 <= lons ) & (lons < m.lonmax+1) & (m.latmin-1 <= lats) & (lats < m.latmax+1) + lons = lons[ibox] + lats = lats[ibox] + x, y = m(lons,lats) + + logging.debug(f"saveNewMap: triangulate {len(lons)} pts") + vtx, wts = interp_weights(np.vstack((lons,lats)).T,np.vstack((lon2d.flatten(), lat2d.flatten())).T) + + pickle.dump((fig,ax,m,lons,lats,delta_deg,lon2d,lat2d,x2d,y2d,ibox,x,y,vtx,wts), open(pk_file, 'wb')) + -def drawOverlay(domain='CONUS'): - ll_lat, ll_lon, ur_lat, ur_lon = domains[domain]['corners'] - fig_width = domains[domain]['fig_width'] - lat_1, lat_2, lon_0 = 32.0, 46.0, -101.0 - if domain=='NA': - lon_0 = -115.0 - dpi = 90 +def drawOverlay(plot, pk_file): + logging.info(f"drawOverlay: pk_file={pk_file}") + ll_lat, ll_lon, ur_lat, ur_lon = domains[plot.domain]['corners'] + lat_1, lat_2, lon_0 = 32., 46., -101. + fig_width = 1080 + dpi = 90 fig = plt.figure(dpi=dpi) m = Basemap(projection='lcc', resolution='i', llcrnrlon=ll_lon, llcrnrlat=ll_lat, urcrnrlon=ur_lon, urcrnrlat=ur_lat, \ lat_1=lat_1, lat_2=lat_2, lon_0=lon_0, area_thresh=1000) # compute height based on figure width, map aspect ratio, then add some vertical space for labels/colorbar fig_width = fig_width/float(dpi) - #fig_height = fig_width*m.aspect + 0.93 - fig_height = fig_width*m.aspect + 1.25 + fig_height = fig_width*m.aspect + 0.93 figsize = (fig_width, fig_height) fig.set_size_inches(figsize) - # place map 0.8" from bottom of figure, leave 0.45" at top for title (needs to be in figure-relative coords) - x,y,w,h = 0.01, 0.8/float(fig_height), 0.98, 0.98*fig_width*m.aspect/float(fig_height) - #x,y,w,h = 0.01, 0.7/float(fig_height), 0.98, 0.98*fig_width*m.aspect/float(fig_height) + # place map 0.7" from bottom of figure, leave rest of 0.93" at top for title (needs to be in figure-relative coords) + x,y,w,h = 0.01, 0.7/float(fig_height), 0.98, 0.98*fig_width*m.aspect/float(fig_height) ax = fig.add_axes([x,y,w,h]) #drawcounties doesnt work when called by itself? so have to drawcoastines first with lw=0 @@ -1240,22 +703,24 @@ def drawOverlay(domain='CONUS'): plt.savefig('overlay_counties_%s.png'%domain, dpi=90, transparent=True) def compute_pmm(ensemble): - members = ensemble.shape[0] - spatial_dimensions = ensemble.shape[1:] - ens_mean = np.mean(ensemble, axis=0) - ens_dist = np.sort(ensemble.flatten())[::-1] - pmm = ens_dist[::members] - - ens_mean_index = np.argsort(ens_mean.flatten())[::-1] + members = ensemble.Time.size + ens_mean = ensemble.mean(dim="Time", keep_attrs=True) + logging.info(f"compute_pmm: sort {ensemble.values.size} ensemble values") + ens_dist = np.sort(ensemble.values.flatten())[::-1] + pmm = ens_dist[::members] # brilliant + + logging.debug(f"compute_pmm: sort {ens_mean.values.size} ens_mean values") + ens_mean_index = np.argsort(ens_mean.values.flatten())[::-1] temp = np.empty_like(pmm) temp[ens_mean_index] = pmm - temp = np.where(ens_mean.flatten() > 0, temp, 0.0) - return temp.reshape(spatial_dimensions) + temp = np.where(ens_mean.values.flatten() > 0, temp, 0.0) + ens_mean.values = temp + return ens_mean def compute_neprob(ensemble, roi=0, sigma=0.0, type='gaussian'): if len(ensemble.shape) < 3: - logggin.error('compute_neprob: needs ensemble of 2D arrays, not 1D arrays') + logging.error('compute_neprob: needs ensemble of 2D arrays, not 1D arrays') sys.exit(1) y,x = np.ogrid[-roi:roi+1, -roi:roi+1] kernel = x**2 + y**2 <= roi**2 @@ -1264,23 +729,26 @@ def compute_neprob(ensemble, roi=0, sigma=0.0, type='gaussian'): ens_mean = np.nanmean(ens_roi, axis=0) #ens_mean = np.nanmean(ensemble, axis=0) - if (type == 'uniform'): + if type == 'uniform': y,x = np.ogrid[-sigma:sigma+1, -sigma:sigma+1] kernel = x**2 + y**2 <= sigma**2 ens_mean = ndimage.filters.convolve(ens_mean, kernel/float(kernel.sum())) - elif (type == 'gaussian'): + elif type == 'gaussian': ens_mean = ndimage.filters.gaussian_filter(ens_mean, sigma) + else: + logging.error(f"compute_neprob: unknown filter {type}") + sys.exit(1) return ens_mean -def compute_prob3d(ensemble, roi=0, sigma=0.0, type='gaussian'): - print(ensemble.shape) +def compute_prob3d(ensemble, roi=0, sigma=0.): + logging.info(f"compute_prob3d: roi={roi} sigma={sigma}") y,x = np.ogrid[-roi:roi+1, -roi:roi+1] kernel = x**2 + y**2 <= roi**2 ens_roi = ndimage.filters.maximum_filter(ensemble, footprint=kernel[np.newaxis,np.newaxis,:]) - print(ens_roi.shape) + logging.info(f"ens_roi.shape={ens_roi.shape}") ens_mean = np.nanmean(ens_roi, axis=1) - print(ens_mean.shape) + logging.info(f"ens_mean.shape={ens_mean.shape}") ens_mean = ndimage.filters.gaussian_filter(ens_mean, [2,20,20]) return ens_mean[3,:] @@ -1365,10 +833,6 @@ def interp_weights(xyz, uvw): return vertices, np.hstack((bary, 1 - bary.sum(axis=1, keepdims=True))) -def showKeys(): - print(list(fieldinfo.keys())) - sys.exit() - def computefrzdepth(t): frz_at_surface = np.where(t[0,:] < 33, True, False) #pts where surface T is below 33F max_column_t = np.amax(t, axis=0) From 4bb21492b438b0bafafa7b19ecf7b5a8beb41c3c Mon Sep 17 00:00:00 2001 From: ahijevyc Date: Fri, 17 Feb 2023 15:40:53 -0700 Subject: [PATCH 42/68] clean up imports and method calls --- make_webplot.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/make_webplot.py b/make_webplot.py index 37446b2..3f5be85 100755 --- a/make_webplot.py +++ b/make_webplot.py @@ -1,7 +1,5 @@ import logging -import os import pdb -import sys import time from webplot import webPlot @@ -10,13 +8,13 @@ stime = time.time() regions = ['CONUS', 'NGP', 'SGP', 'CGP', 'MATL', 'NE', 'NW', 'SE', 'SW'] -regions = ['CONUS', 'NGP'] +regions = ['CONUS', 'CGP'] for dom in regions: Plot = webPlot(domain=dom) logging.debug('Writing Image') - Plot.saveFigure(trans=Plot.opts['over']) + Plot.saveFigure() etime = time.time() logging.info(f'End Plotting (took {etime-stime:.2f} sec)') From ef44ce28851d239081d668b3392675c6166301f8 Mon Sep 17 00:00:00 2001 From: ahijevyc Date: Fri, 17 Feb 2023 15:41:04 -0700 Subject: [PATCH 43/68] no more basemap. cartopy. keep figure width constant. replicate a lot of basemap functions like minlon, aspect... call set_extent Simplify calculation of nlons an nlats by doing it in lcc projecion space (meters). place colorbar and logo correctly again with cartopy. Use plt.subplots for postage stamp axes. Rename fieldtype -> ensprod. get neprob working again. --- webplot.py | 493 +++++++++++++++++++++++++++-------------------------- 1 file changed, 253 insertions(+), 240 deletions(-) diff --git a/webplot.py b/webplot.py index 2a64e6f..c0eff56 100755 --- a/webplot.py +++ b/webplot.py @@ -1,14 +1,15 @@ import argparse +import cartopy from collections import defaultdict import datetime -from fieldinfo import domains +from fieldinfo import domains, readNCLcm import logging import matplotlib.colors as colors import matplotlib.pyplot as plt +from metpy.units import units import mpas from mpas import fieldinfo, mesh_config, makeEnsembleList -import vert2cell # created with f2py3 -c mpas_vort_cell.f90 -m vert2cell -from mpl_toolkits.basemap import * +import numpy as np import pandas as pd import pdb import pickle @@ -20,6 +21,10 @@ import re import sys import time +# created with f2py3 -c mpas_vort_cell.f90 -m vert2cell +# Had to fix /glade/u/home/ahijevyc/miniconda3/envs/webplot/lib/python3.11/site-packages/numpy/f2py/src/fortranobject.c +# Moved int i declaration outside for loop. (line 707) +import vert2cell import xarray logging.basicConfig(format='%(asctime)s - %(message)s', level=logging.INFO) @@ -41,7 +46,6 @@ def __init__(self, domain=None): self.title = self.opts['title'] self.get_mpas_mesh() - self.createFilename() self.loadMap() self.data, self.missing_members = readEnsemble(self) self.plotFields() @@ -60,9 +64,9 @@ def createFilename(self): shr = min(self.fhr) ehr = max(self.fhr) if len(self.fhr) == 1: - self.outfile = f"{prefx}_f{shr:03.0f}_{self.domain}.png" + outfile = f"{prefx}_f{shr:03.0f}_{self.domain}.png" else: # CSS - self.outfile = f"{prefx}_f{shr:03.0f}-f{ehr:03.0f}_{self.domain}.png" + outfile = f"{prefx}_f{shr:03.0f}-f{ehr:03.0f}_{self.domain}.png" # create yyyymmddhh/domain/ directory if needed subdir_path = os.path.join(os.getenv('TMPDIR'), self.opts['date'], self.domain) @@ -70,17 +74,25 @@ def createFilename(self): logging.warning(f"webPlot.createFilename(): making new output directory {subdir_path}") os.makedirs(subdir_path) # prepend subdir_path to outfile. - self.outfile = os.path.join(subdir_path, self.outfile) - self.outfile = os.path.realpath(self.outfile) - - def loadMap(self, overlay=False): - pk_file = os.path.join(os.getenv('TMPDIR'), f"{self.meshstr}_{self.domain}_{self.nlon_max}x{self.nlat_max}.pk") - if not os.path.exists(pk_file): - saveNewMap(self, pk_file) - logging.debug(f"loadMap: use old pickle file {pk_file}") - (self.fig, self.ax, self.m, self.lons, self.lats, self.delta_deg, + outfile = os.path.join(subdir_path, outfile) + outfile = os.path.realpath(outfile) + return outfile + + def loadMap(self): + self.pk_file = os.path.join(os.getenv('TMPDIR'), f"{self.meshstr}_{self.domain}_{self.nlon_max}x{self.nlat_max}.pk") + if not os.path.exists(self.pk_file): + saveNewMap(self) + logging.debug(f"loadMap: use old pickle file {self.pk_file}") + (self.ax, self.extent, self.lons, self.lats, self.lon2d, self.lat2d, self.x2d, self.y2d, self.ibox, self.x, self.y, self.vtx, - self.wts) = pickle.load(open(pk_file, 'rb')) + self.wts) = pickle.load(open(self.pk_file, 'rb')) + + def drawOverlay(self): + domain=self.domain + logging.info(f"drawOverlay: domain={domain}") + self.ax.axis('off') + self.ax.set_extent(self.extent, crs=self.ax.projection) + plt.savefig(f'overlay_counties_{domain}.png', transparent=True) def get_mpas_mesh(self): path = self.opts["init_file"] @@ -92,7 +104,6 @@ def get_mpas_mesh(self): self.mpas_mesh = mpas_mesh def plotTitleTimes(self): - if self.opts['over']: return fontdict = {'family':'monospace', 'size':12, 'weight':'bold'} # place title and times above corners of map @@ -101,9 +112,10 @@ def plotTitleTimes(self): x1, y1 = self.ax.transAxes.transform((1,1)) self.ax.text(x0, y1+10, self.title, fontdict=fontdict, transform=None) - initstr, validstr = self.getInitValidStr() - self.ax.text(x1, y1+20, initstr, horizontalalignment='right', transform=None) - self.ax.text(x1, y1+5, validstr, horizontalalignment='right', transform=None) + fontdict = {'family':'monospace'} + initstr, validstr = self.getInitValidStr() + self.ax.text(1, 1, initstr+"\n"+validstr, fontdict=fontdict, horizontalalignment='right', + verticalalignment="bottom", transform=self.ax.transAxes) # Plot missing members if len(self.missing_members): @@ -123,10 +135,11 @@ def plotFields(self): else: self.plotContour() if 'barb' in self.data: - assert not self.opts['contour']['ensprod'].endswith('stamp'), "TODO: postage stamp barbs" + assert not self.opts['barb']['ensprod'].endswith('stamp'), "TODO: postage stamp barbs" self.plotBarbs() def plotFill(self): + if self.opts['fill']['name'] == 'crefuh': self.plotReflectivityUH(); return if self.autolevels: min, max = self.data['fill'][0].min(), self.data['fill'][0].max() levels = np.linspace(min, max, num=10) @@ -145,47 +158,41 @@ def plotFill(self): norm = colors.BoundaryNorm(levels, cmap.N) data = self.data['fill'][0] - data = self.latlonGrid(data) - # regrid 1D mesh that needs to be smoothed + if not self.opts['fill']['ensprod'].startswith('neprob'): + logging.info(f"plotFill: latlonGrid") + data = self.latlonGrid(data) if self.opts['fill']['name'] in ['avo500', 'vort500', 'pbmin']: - # smooth some of the fill fields. use .values to preserve DataArray attributes. + logging.info(f"smooth {data.name}") + # use .values to preserve DataArray attributes. data.values = ndimage.gaussian_filter(data, sigma=4) - elif self.opts['fill']['ensprod'].startswith('neprob'): - miles_to_km = 1.60934 - roi = 25 * miles_to_km / self.min_grid_spacing_km - logging.debug(f"roi {roi}") - data = compute_neprob(data, roi=int(roi), sigma=float(fields['sigma']), type='gaussian') - data = data+0.001 #hack to ensure that plot displays discrete prob values - - cs1 = self.m.contourf(self.x2d, self.y2d, data, levels=levels, cmap=cmap, norm=norm, extend='max', ax=self.ax) + + cs1 = self.ax.contourf(self.x2d, self.y2d, data, levels=levels, cmap=cmap, norm=norm, extend='max') label = f"{data.long_name} [{data.units}]" self.plotColorbar(cs1, levels, tick_labels, extend, extendfrac, label=label) def plotReflectivityUH(self): levels = self.opts['fill']['levels'] cmap = colors.ListedColormap(self.opts['fill']['colors']) + extend, extendfrac = 'neither', 0.0 norm = colors.BoundaryNorm(levels, cmap.N) tick_labels = levels[:-1] - cs1 = self.m.contourf(self.x2d, self.y2d, self.latlonGrid(self.data['fill'][0]), levels=levels, cmap=cmap, norm=norm, extend='max', ax=self.ax) + logging.info(f"plotReflectivityUH: latlonGrid {self.data['fill'][0].long_name}") + data = self.latlonGrid(self.data["fill"][0]) + cs1 = self.ax.contourf(self.x2d, self.y2d, data, levels=levels, cmap=cmap, norm=norm, extend='max') + logging.info(f"plotReflectivityUH: latlonGrid {self.data['fill'][1].long_name}") uh = self.latlonGrid(self.data['fill'][1]) - self.m.contourf(self.x2d, self.y2d, uh, levels=[100,1000], colors='black', ax=self.ax, alpha=0.3) - cs2 = self.m.contour( self.x2d, self.y2d, uh, levels=[100], colors='k', linewidths=0.5, ax=self.ax) - # for some reason the zero contour is plotted if there are no other valid contours - # are there some small negatives due to regridding? No. - if 0.0 in cs2.levels: - print("webplot.plotReflectivityUH has zero contour for some reason. Hide it") - for i in cs2.collections: - i.remove() + minUH = 100 + logging.info(f"plotReflectivityUH: UH shading and contour above {minUH}") + self.ax.contourf(self.x2d, self.y2d, uh, levels=[minUH,1000], colors='black', alpha=0.3) + cs2 = self.ax.contour( self.x2d, self.y2d, uh, levels=[minUH], colors='k', linewidths=0.5) - self.plotColorbar(cs1, levels, tick_labels) + label = f"{data.long_name} [{data.units}], shaded {uh.long_name} above ${minUH} {uh.units}$" + self.plotColorbar(cs1, levels, tick_labels, extend, extendfrac, label=label) def plotColorbar(self, cs, levels, tick_labels, extend='neither', extendfrac=0.0, label=""): - # make axes for colorbar, 175px to left and 30px down from bottom of map - x0, y0 = self.ax.transAxes.transform((0,0)) - x, y = self.fig.transFigure.inverted().transform((x0+175,y0-29.5)) - cax = self.fig.add_axes([x,y,0.985-x,y/3.0]) - cb = plt.colorbar(cs, cax=cax, orientation='horizontal', extend=extend, extendfrac=extendfrac, ticks=tick_labels, label=label) + cb = plt.colorbar(cs, ax=self.ax, location="bottom", orientation='horizontal', extend=extend, shrink=0.65, + anchor=(1,1), panchor=(1,0), aspect=55, pad=0.03, extendfrac=extendfrac, ticks=tick_labels, label=label) cb.ax.xaxis.set_label_position('top') cb.outline.set_linewidth(0.5) @@ -193,16 +200,17 @@ def interpolatetri(self, values, vtx, wts): return np.einsum('nj,nj->n', np.take(values, vtx), wts) def latlonGrid(self, data): + logging.debug(f"latlonGrid: {data.name}") data = data.metpy.dequantify() # Allow units to transfer to gridded array via attribute # apply ibox to data - data = data[self.ibox] + data = data.isel(nCells=self.ibox) if hasattr(self, "vtx") and hasattr(self, "wts"): logging.debug("latlonGrid: interpolatetri(vtx and wts)") # by using .values, Avoid interpolatetri ValueError: dimensions ('nCells',) must have the same length as the number of data dimensions, ndim=2 data_gridded = self.interpolatetri(data.values, self.vtx, self.wts) data_gridded = np.reshape(data_gridded, self.lat2d.shape) else: - logging.info("latlonGrid: interpolate to latlon grid with griddata()") + logging.info("latlonGrid: interpolate with griddata()") data_gridded = griddata((self.lons, self.lats), data, (self.lon2d, self.lat2d), method='nearest') data_gridded = xarray.DataArray(data = data_gridded, coords = dict(lat=self.lat2d[:,0], lon=self.lon2d[0]), attrs=data.attrs) return data_gridded @@ -211,12 +219,15 @@ def plotContour(self): if self.opts['contour']['name'] in ['sbcinh','mlcinh']: linewidth, alpha = 0.5, 0.75 else: linewidth, alpha = 1.5, 1.0 data = self.data['contour'][0] - data_gridded = self.latlonGrid(data) - if self.opts['contour']['name'] in ['t2-0c']: data_gridded.values = ndimage.gaussian_filter(data_gridded, sigma=2) - else: data_gridded.values = ndimage.gaussian_filter(data_gridded, sigma=25) + if not self.opts['contour']['ensprod'].startswith('neprob'): + logging.info(f"plotContour: latlonGrid") + data = self.latlonGrid(data) + + if self.opts['contour']['name'] in ['t2-0c']: data.values = ndimage.gaussian_filter(data, sigma=2) + else: data.values = ndimage.gaussian_filter(data, sigma=25) - cs2 = self.m.contour(self.x2d, self.y2d, data_gridded, levels=self.opts['contour']['levels'], colors='k', linewidths=linewidth, ax=self.ax, alpha=alpha) + cs2 = self.ax.contour(self.x2d, self.y2d, data, levels=self.opts['contour']['levels'], colors='k', linewidths=linewidth, alpha=alpha) plt.clabel(cs2, fontsize='small', fmt='%i') def plotBarbs(self): @@ -230,23 +241,20 @@ def plotBarbs(self): logging.debug(f"plotBarbs: starting barbs {[x.name for x in self.data['barb']]}") # skip interval intended for 2-D fields - if len(self.x.shape) == 2: - cs2 = self.m.barbs(self.x[::skip,::skip], self.y[::skip,::skip], self.data['barb'][0][::skip,::skip], self.data['barb'][1][::skip,::skip], - alpha=alpha, length=5.5, linewidth=0.25, sizes={'emptybarb':0.05}, ax=self.ax) - if len(self.x.shape) == 1: - u2d = self.latlonGrid(self.data['barb'][0]) - v2d = self.latlonGrid(self.data['barb'][1]) - # rotate vectors so they represent the direction properly on the map projection - u10_rot, v10_rot, x, y = self.m.rotate_vector(u2d, v2d, self.lon2d, self.lat2d, returnxy=True) - cs2 = self.m.barbs(x[::skip,::skip], y[::skip,::skip], u10_rot[::skip,::skip], v10_rot[::skip,::skip], - alpha=alpha, length=5.5, linewidth=0.25, sizes={'emptybarb':0.05}, ax=self.ax) + u = self.latlonGrid(self.data['barb'][0])[::skip,::skip].values.flatten() + v = self.latlonGrid(self.data['barb'][1])[::skip,::skip].values.flatten() + x = self.lon2d[::skip,::skip].flatten() + y = self.lat2d[::skip,::skip].flatten() + # transform orients the barbs properly on projection + logging.info(f"plotBarbs: barbs") + cs2 = self.ax.barbs(x, y, u, v, alpha=alpha, length=5.5, linewidth=0.25, sizes={'emptybarb':0.05}, transform=cartopy.crs.PlateCarree()) def plotPaintball(self): rects, labels = [], [] colorlist = self.opts['fill']['colors'] levels = self.opts['fill']['levels'] for i in range(self.data['fill'][0].shape[0]): - cs = self.m.contourf(self.x, self.y, self.data['fill'][0][i,self.ibox], tri=True, levels=levels, colors=[colorlist[i%len(colorlist)]], ax=self.ax, alpha=0.5) + cs = self.ax.contourf(self.x, self.y, self.data['fill'][0][i,self.ibox], tri=True, levels=levels, colors=[colorlist[i%len(colorlist)]], alpha=0.5) rects.append(plt.Rectangle((0,0),1,1,fc=colorlist[i%len(colorlist)])) labels.append("member %d"%(i+1)) @@ -260,83 +268,68 @@ def plotSpaghetti(self): data = self.data['contour'][0] data.values = ndimage.gaussian_filter(data, sigma=[0,4,4]) for i in range(self.data['contour'][0].shape[0]): - #cs = self.m.contour(self.x, self.y, data[i,:], levels=levels, colors=[colorlist[i]], linewidths=2, linestyles='solid', ax=self.ax) - cs = self.m.contour(self.x, self.y, data[i,:], levels=levels, colors='k', alpha=0.6, linewidths=1, linestyles='solid', ax=self.ax) + cs = self.ax.contour(self.x, self.y, data[i,:], levels=levels, colors='k', alpha=0.6, linewidths=1, linestyles='solid') #proxy.append(plt.Rectangle((0,0),1,1,fc=colorlist[i])) #plt.legend(proxy, ["member %d"%i for i in range(1,11)], ncol=5, loc='right', bbox_to_anchor=(1.0,-0.05), fontsize=11, \ # frameon=False, borderpad=0.25, borderaxespad=0.25, handletextpad=0.2) def plotStamp(self): - fig_width_px, dpi = 1280, 90 - fig = plt.figure(dpi=dpi) - num_rows, num_columns = 3, 4 - fig_width = fig_width_px/dpi - width_per_panel = fig_width/float(num_columns) - height_per_panel = width_per_panel*self.m.aspect - fig_height = height_per_panel*num_rows - fig_height_px = fig_height*dpi - fig.set_size_inches((fig_width, fig_height)) + num_rows, num_columns = 3, 4 + fig, axs = plt.subplots(nrows=num_rows, ncols=num_columns, + subplot_kw={'projection':self.ax.projection}, + figsize=(14,8)) + fig.subplots_adjust(bottom=0.01, top=0.99, left=0.01, right=0.99, wspace=0.01, hspace=0.01) - levels = self.opts['fill']['levels'] - cmap = colors.ListedColormap(self.opts['fill']['colors']) - norm = colors.BoundaryNorm(levels, cmap.N) - - memberidx = 0 - for j in range(0,num_rows): - for i in range(0,num_columns): - member = num_columns*j+i - if member > 9: break - spacing_w, spacing_h = 5/float(fig_width_px), 5/float(fig_height_px) - spacing_w = 10/float(fig_width_px) - x, y = i*width_per_panel/float(fig_width), 1.0 - (j+1)*height_per_panel/float(fig_height) - w, h = (width_per_panel/float(fig_width))-spacing_w, (height_per_panel/float(fig_height))-spacing_h - if member == 9: y = 0 - - logging.debug(f'member {member} creating axes at {x},{y}') - thisax = fig.add_axes([x,y,w,h]) - - thisax.axis('on') - for axis in ['top','bottom','left','right']: thisax.spines[axis].set_linewidth(0.5) - self.m.drawcoastlines(ax=thisax, linewidth=0.3) - self.m.drawstates(linewidth=0.15, ax=thisax) - self.m.drawcountries(ax=thisax, linewidth=0.3) - thisax.text(0.03,0.97,member+1,ha="left",va="top",bbox=dict(boxstyle="square",lw=0.5,fc="white"), transform=thisax.transAxes) - - # plot, unless file that has fill field is missing, then skip - if member not in self.missing_members and member < self.ENS_SIZE: - data = self.latlonGrid(self.data['fill'][0].isel(mem=memberidx)) - logging.debug(f"plotStamp: starting contourf with regridded array member {memberidx}") - cs1 = self.m.contourf(self.x2d, self.y2d, data, levels=levels, cmap=cmap, norm=norm, extend='max', ax=thisax) - memberidx += 1 - - # use every other tick for large colortables, remove last tick label for both - if self.opts['fill']['name'] in ['goesch3', 'goesch4', 't2', 'precipacc' ]: ticks = levels[:-1][::2] # CSS added precipacc - else: ticks = levels[:-1] - - label = f"{data.long_name} [{data.units}]" - # add colorbar to figure - cax = fig.add_axes([0.51,0.3,0.48,0.02]) - cb = plt.colorbar(cs1, cax=cax, orientation='horizontal', ticks=ticks, extendfrac=0.0, label=label) - cb.outline.set_linewidth(0.5) - cb.ax.tick_params(labelsize=9) - - # add init/valid text - fontdict = {'family':'monospace', 'size':13, 'weight':'bold'} - initstr, validstr = self.getInitValidStr() - - fig.text(0.51, 0.22, self.title, fontdict=fontdict, transform=fig.transFigure) - fig.text(0.51, 0.22 - 25/float(fig_height_px), initstr, transform=fig.transFigure) - fig.text(0.51, 0.22 - 40/float(fig_height_px), validstr, transform=fig.transFigure) - - # add NCAR logo and text below logo - x, y = fig.transFigure.transform((0.51,0)) - fig.figimage(plt.imread('ncar.png'), xo=x, yo=y+15, zorder=1000) - #plt.text(x+10, y+5, 'ensemble.ucar.edu', fontdict={'size':9, 'color':'#505050'}, transform=None) + levels = self.opts['fill']['levels'] + cmap = colors.ListedColormap(self.opts['fill']['colors']) + norm = colors.BoundaryNorm(levels, cmap.N) + + for member, ax in enumerate(axs.flatten()): + ax.add_feature(cartopy.feature.COASTLINE.with_scale('110m'), linewidth=0.25) + ax.add_feature(cartopy.feature.BORDERS.with_scale('110m'), linewidth=0.25) + ax.add_feature(cartopy.feature.STATES.with_scale('110m'), linewidth=0.05) + ax.add_feature(cartopy.feature.LAKES.with_scale('110m'), edgecolor='k', linewidth=0.25, facecolor='k', alpha=0.05) + ax.text(0.03,0.97,member+1,ha="left",va="top",bbox=dict(boxstyle="square",lw=0.5,fc="white"), transform=ax.transAxes) + # plot, unless file that has fill field is missing, then skip + if member not in self.missing_members and member < self.ENS_SIZE: + data = self.latlonGrid(self.data['fill'][0].isel(mem=member)) + logging.debug(f"plotStamp: starting contourf with regridded array member {member}") + cs1 = ax.contourf(self.x2d, self.y2d, data, levels=levels, cmap=cmap, norm=norm, extend='max') + ax.set_extent(self.extent, crs=self.ax.projection) + if member >= self.ENS_SIZE: + fig.delaxes(ax) + + + # use every other tick for large colortables, remove last tick label for both + if self.opts['fill']['name'] in ['goesch3', 'goesch4', 't2', 'precipacc' ]: ticks = levels[:-1][::2] # CSS added precipacc + else: ticks = levels[:-1] + + label = f"{data.long_name} [{data.units}]" + # add colorbar to figure + cax = fig.add_axes([0.51,0.3,0.48,0.02]) + cb = plt.colorbar(cs1, cax=cax, orientation='horizontal', ticks=ticks, extendfrac=0.0, label=label) + cb.outline.set_linewidth(0.5) + cb.ax.tick_params(labelsize=9) + + # add init/valid text + fontdict = {'family':'monospace', 'size':13, 'weight':'bold'} + initstr, validstr = self.getInitValidStr() + + fig.text(0.51, 0.22, self.title, fontdict=fontdict, transform=fig.transFigure) + pos = axs.flatten()[-2].get_position() + fontdict = {'family':'monospace'} + fig.text(0.51, pos.y0+0.3*pos.height, " "+initstr+"\n"+validstr, fontdict=fontdict, ha="left", + transform=fig.transFigure) + + # add NCAR logo and text below logo + xo, yo = fig.transFigure.transform((0.51,pos.y0)) + fig.figimage(plt.imread('ncar.png'), xo=xo, yo=yo) + #plt.text(x+10, y+5, 'ensemble.ucar.edu', fontdict={'size':9, 'color':'#505050'}, transform=None) def getInitValidStr(self): - initstr = self.initdate.strftime(' Init: %a %Y-%m-%d %H UTC') + initstr = self.initdate.strftime('Init: %a %Y-%m-%d %H UTC') shr = min(self.fhr) ehr = max(self.fhr) fmt = '%a %Y-%m-%d %H UTC' @@ -345,31 +338,37 @@ def getInitValidStr(self): validstr += " - " + (self.initdate+datetime.timedelta(hours=ehr)).strftime(fmt) return initstr, validstr - def saveFigure(self, trans=False): + def saveFigure(self, transparent=False): + outfile = self.createFilename() # place NCAR logo 57 pixels below bottom of map, then save image if 'ensprod' in self.opts['fill']: # CSS needed incase not a fill plot - if not trans and not self.opts['fill']['ensprod'].endswith('stamp'): - x, y = self.ax.transAxes.transform((0,0)) - self.fig.figimage(plt.imread('ncar.png'), xo=x, yo=(y-44)) - #plt.text(x+10, y-54, 'ensemble.ucar.edu', fontdict={'size':9, 'color':'#505050'}, transform=None) + if not transparent and not self.opts['fill']['ensprod'].endswith('stamp'): + img = plt.imread('ncar.png') + self.ax.figure.figimage(img, xo=10, yo=10) + #plt.text(x+10, y-54, 'ensemble.ucar.edu', fontdict={'size':9, 'color':'#505050'}, transform=None) + + self.ax.set_extent(self.extent, crs=self.ax.projection) - plt.savefig(self.outfile, dpi=90, transparent=trans) + plt.savefig(outfile, transparent=transparent, bbox_inches="tight") if self.opts['convert']: - #command = 'convert -colors 255 %s %s'%(self.outfile, self.outfile) + # reduce colors to shrink file size if not self.opts['fill']: ncolors = 48 #if no fill field exists - elif self.opts['fill']['ensprod'] in ['prob', 'neprob', 'probgt', 'problt', 'neprobgt', 'neproblt']: ncolors = 48 + elif "prob" in self.opts['fill']['ensprod']: ncolors = 48 elif self.opts['fill']['name'] in ['crefuh']: ncolors = 48 else: ncolors = 255 - #command = '/glade/u/home/sobash/pngquant/pngquant %d %s --ext=.png --force'%(ncolors,self.outfile) if os.environ['NCAR_HOST'] == "cheyenne": quant = '/glade/u/home/ahijevyc/bin_cheyenne/pngquant' else: quant = '/glade/u/home/ahijevyc/bin/pngquant' - command = f"{quant} {ncolors} {self.outfile} --ext=.png --force" + beforesize = os.path.getsize(outfile) + command = f"{quant} {ncolors} {outfile} --ext=.png --force" + logging.info(f"{beforesize} bytes before reducing colors") + logging.debug(command) ret = subprocess.check_call(command.split()) plt.clf() - logging.info(f"created {self.outfile}") + fsize = os.path.getsize(outfile) + logging.info(f"created {outfile} {fsize} bytes") def parseargs(): '''Parse arguments and return dictionary of fill, contour and barb field parameters''' @@ -384,11 +383,10 @@ def parseargs(): parser.add_argument('-f', '--fill', help='fill field (FIELD_PRODUCT_THRESH), field keys:'+','.join(list(fieldinfo.keys()))) parser.add_argument('--fhr', nargs='+', type=float, default=[12], help='list of forecast hours') parser.add_argument('--meshstr', type=str, default='uni', help='mesh id or path to defining mesh') - parser.add_argument('--nbarbs', type=int, default=50, help='max barbs in one dimension') + parser.add_argument('--nbarbs', type=int, default=32, help='max barbs in one dimension') parser.add_argument('--nlon_max', type=int, default=1500, help='max pts in longitude dimension') parser.add_argument('--nlat_max', type=int, default=1500, help='max pts in latitude dimension') - parser.add_argument('--over', default=False, action='store_true', help='plot as overlay (no lines, transparent, no convert)') - parser.add_argument('-sig', '--sigma', default=2, help='smooth probabilities using gaussian smoother') + parser.add_argument('--sigma', default=2, help='smooth probabilities using gaussian smoother') parser.add_argument('-t', '--title', help='title for plot') opts = vars(parser.parse_args()) # argparse.Namespace in form of dictionary @@ -434,10 +432,10 @@ def parseargs(): thisdict['levels'] = [float(input[2]), 1000] thisdict['colors'] = readNCLcm('GMT_paired') elif (input[1] == 'var'): - if (input[0][0:3] == 'hgt'): + if input[0].startswith('hgt'): thisdict['levels'] = [2,4,6,8,10,15,20,25,30,35,40,45,50,55,60,65,70,75] #hgt thisdict['colors'] = readNCLcm('wind_17lev') - elif (input[0][0:3] == 'spe'): + elif input[0].startswith('spe'): thisdict['levels'] = [1,2,3,4,5,6,7,8,9,10,12.5,15,20,25,30,35,40,45] #iso thisdict['colors'] = readNCLcm('wind_17lev') else: @@ -484,10 +482,10 @@ def readEnsemble(Plot): # save some variables for use in this function arrays = fields[f]['arrayname'] - fieldtype = fields[f]['ensprod'] + ensprod = fields[f]['ensprod'] fieldname = fields[f]['name'] - if fieldtype in ['prob', 'neprob', 'problt', 'probgt', 'neprobgt', 'neproblt', 'prob3d']: thresh = fields[f]['thresh'] - if fieldtype.startswith('mem'): member = int(fieldtype[3:]) + if ensprod in ['prob', 'neprob', 'problt', 'probgt', 'neprobgt', 'neproblt', 'prob3d']: thresh = fields[f]['thresh'] + if ensprod.startswith('mem'): member = int(ensprod[3:]) # open xarray Dataset logging.info(f"opening xarray Dataset. {len(fhr)} forecast hours x {ENS_SIZE} members") @@ -577,38 +575,57 @@ def readEnsemble(Plot): data = ensemble_at_last_time # use last time for output data data.data -= ensemble_at_first_time.data # .data preserves quantity units, metadata, avoids pint.errors.DimensionalityError: Cannot convert from 'dimensionless' to 'inch' - logging.info(f"perform {fieldtype} on {data.shape} data") - if (fieldtype == 'mean'): + logging.info(f"get {ensprod} of {data.shape} data") + if (ensprod == 'mean'): data = data.mean(dim=["Time","mem"], keep_attrs=True) - elif (fieldtype == 'pmm'): + elif (ensprod == 'pmm'): data = compute_pmm(data) - elif (fieldtype == 'max'): + elif (ensprod == 'max'): data = data.max(dim=["Time","mem"], keep_attrs=True) - elif (fieldtype == 'min'): + elif (ensprod == 'min'): data = data.min(dim=["Time","mem"], keep_attrs=True) - elif (fieldtype == 'var'): + elif (ensprod == 'var'): data = data.std(dim=["Time","mem"], keep_attrs=True) - elif (fieldtype == 'maxstamp'): + elif (ensprod == 'maxstamp'): data = data.max(dim="Time", keep_attrs=True) - elif (fieldtype == 'meanstamp'): + elif (ensprod == 'meanstamp'): data = data.mean(dim="Time", keep_attrs=True) - elif (fieldtype == 'summean'): + elif (ensprod == 'summean'): data = data.sum(dim="Time", keep_attrs=True) data = data.mean(dim="mem", keep_attrs=True) - elif (fieldtype == 'maxmean'): + elif (ensprod == 'maxmean'): data = data.max(dim="Time", keep_attrs=True) data = data.mean(dim="mem", keep_attrs=True) - elif (fieldtype == 'summax'): + elif (ensprod == 'summax'): data = data.sum(dim="Time", keep_attrs=True) data = data.max(dim="mem", keep_attrs=True) - elif (fieldtype[0:3] == 'mem'): + elif (ensprod[0:3] == 'mem'): data = data.sel(mem=member) - elif 'prob' in fieldtype: - if fieldtype.endswith('prob') or fieldtype.endswith('gt'): data = (data>=thresh).astype('float') - elif fieldtype.endswith('lt'): data = (data=thresh).astype('float') - data = compute_prob3d(data, roi=14, sigma=float(fields['sigma']), type='gaussian') + elif 'prob' in ensprod: + u = data.metpy.units + if ensprod.endswith('lt'): + data.values = data.values < thresh + data.attrs["long_name"] = "less than {thresh} {u}" + else: + data.values = data.values >= thresh + data.attrs["long_name"] = f"greater than or equal to {thresh} {u}" + if ensprod.startswith("ne"): + # grid spacing of interpolated lat-lon grid. + grid_spacing = units.km * np.sqrt(Plot.mpas_mesh["areaCell"].values.min())/1000 + nbrhd = 25 * units.miles + roi = nbrhd / grid_spacing + roi = roi.to_base_units() + logging.info(f"compute neighborhood probability with radius {roi:.2f}") + data = data.stack(TimeMem=("Time","mem")).groupby("TimeMem").apply(Plot.latlonGrid) + data = compute_neprob(data, roi=roi, sigma=float(Plot.opts['sigma']), type='gaussian') + data = data.unstack() + data.attrs["long_name"] += f" within {nbrhd:~}" + data = data.mean(dim=["Time","mem"], keep_attrs=True) + data.attrs["long_name"] = "probability " + data.attrs["long_name"] + data.attrs["units"] = "dimensionless" + elif (ensprod in ['prob3d']): + data = (data.values>=thresh).astype('float') + data = compute_prob3d(data, roi=14, sigma=float(Plot.opts['sigma']), type='gaussian') logging.debug(f'field {fieldname} has shape {data.shape} range {data.min()}-{data.min()}') datadict[f].append(data) @@ -619,89 +636,87 @@ def readEnsemble(Plot): -def saveNewMap(plot, pk_file): - logging.info(f"saveNewMap: pk_file={pk_file}") +def saveNewMap(plot): + logging.info(f"saveNewMap: pk_file={plot.pk_file}") ll_lat, ll_lon, ur_lat, ur_lon = domains[plot.domain]['corners'] lat_1, lat_2, lon_0 = 32., 46., -101. - fig_width = 1080 - dpi = 90 - fig = plt.figure(dpi=dpi) - m = Basemap(projection='lcc', resolution='i', llcrnrlon=ll_lon, llcrnrlat=ll_lat, urcrnrlon=ur_lon, urcrnrlat=ur_lat, \ - lat_1=lat_1, lat_2=lat_2, lon_0=lon_0, area_thresh=1000) - - # compute height based on figure width, map aspect ratio, then add some vertical space for labels/colorbar - fig_width = fig_width/float(dpi) - fig_height = fig_width*m.aspect + 0.93 - figsize = (fig_width, fig_height) - fig.set_size_inches(figsize) - - # place map 0.7" from bottom of figure, leave rest of 0.93" at top for title (needs to be in figure-relative coords) - x,y,w,h = 0.01, 0.7/float(fig_height), 0.98, 0.98*fig_width*m.aspect/float(fig_height) - ax = fig.add_axes([x,y,w,h]) + fig = plt.figure() + + proj = cartopy.crs.LambertConformal(central_longitude=lon_0, standard_parallels=(lat_1,lat_2)) + (llx, lly, llz), (urx, ury, urz) = proj.transform_points( + cartopy.crs.PlateCarree(), + np.array([ll_lon, ur_lon]), + np.array([ll_lat, ur_lat]) + ) + ul_lon, ul_lat = cartopy.crs.PlateCarree().transform_point(llx, ury, proj) + lr_lon, lr_lat = cartopy.crs.PlateCarree().transform_point(urx, lly, proj) + lc_lon, lc_lat = cartopy.crs.PlateCarree().transform_point(0, lly, proj) + uc_lon, uc_lat = cartopy.crs.PlateCarree().transform_point(0, ury, proj) + + # To save time, triangulate within a lat/lon bounding box. + # Get extreme longitudes and latitudes within domain so the entire domain is covered. + # These were attributes of Basemap object, but not cartopy. + # Extreme longitudes are probably in upper left and upper right corners, but also check lower corners. + lonmin = min([ll_lon, ul_lon]) + lonmax = max([lr_lon, ur_lon]) + # Extreme latitudes are probably in lower center and upper center (lc_lat, uc_lat). + latmin = min([ll_lat, lc_lat, lr_lat]) + latmax = max([ul_lat, uc_lat, ur_lat]) + + extent = (llx, urx, lly, ury) # in projection coordinates (x,y) meters + + # y/x aspect ratio was an attribute of Basemap object, but not cartopy. + aspect = (ury - lly) / (urx - llx) + + # Constant figure width, no matter the aspect ratio. + fig.set_size_inches(16,16*aspect) + + ax = plt.axes(projection = proj) for i in list(ax.spines.values()): i.set_linewidth(0.5) - m.drawcoastlines(linewidth=0.5, ax=ax) - m.drawstates(linewidth=0.25, ax=ax) - m.drawcountries(ax=ax) - m.drawcounties(linewidth=0.1, color='gray', ax=ax) + ax.add_feature(cartopy.feature.COASTLINE.with_scale('50m'), linewidth=0.25) + ax.add_feature(cartopy.feature.BORDERS.with_scale('50m'), linewidth=0.25) + ax.add_feature(cartopy.feature.STATES.with_scale('50m'), linewidth=0.05) + ax.add_feature(cartopy.feature.LAKES.with_scale('50m'), edgecolor='k', linewidth=0.25, facecolor='k', alpha=0.05) + if plot.domain != "CONUS": + logging.debug("draw counties") + # Create custom cartopy feature COUNTIES that can be added to the axes. + reader = cartopy.io.shapereader.Reader('/glade/work/ahijevyc/share/shapeFiles/cb_2013_us_county_500k/cb_2013_us_county_500k.shp') + counties = list(reader.geometries()) + COUNTIES = cartopy.feature.ShapelyFeature(counties, cartopy.crs.PlateCarree()) + ax.add_feature(COUNTIES, facecolor="none", linewidth=0.1, alpha=0.25) # lat/lons from mpas_mesh file mpas_mesh = plot.mpas_mesh - # min_grid_spacing_km used for grid spacing of interpolated lat-lon grid. - min_grid_spacing_km = 2. * np.sqrt(plot.mpas_mesh["areaCell"].min()/np.pi)/1000 + # min_grid_spacing is the grid spacing of interpolated lat-lon grid in meters. + min_grid_spacing = np.sqrt(plot.mpas_mesh["areaCell"].values.min()) lats = mpas_mesh["latCell"] lons = mpas_mesh["lonCell"] - delta_deg = min_grid_spacing_km / 111 if lons.max() > 180: lons[lons>180] -= 360 - # Replace m.lonmax with ur_lon? Otherwise, risk getting +179.999 when NW corner crosses the datetime - nlon = int((m.lonmax - m.lonmin)/delta_deg) - nlat = int((m.latmax - m.latmin)/delta_deg) + nlon = int((urx - llx)/min_grid_spacing) # calculate in projection space (meters) + nlat = int((ury - lly)/min_grid_spacing) nlon = np.clip(nlon, 1, plot.nlon_max) nlat = np.clip(nlat, 1, plot.nlat_max) - lon2d, lat2d = np.meshgrid(np.linspace(m.lonmin, m.lonmax, nlon), np.linspace(m.latmin,m.latmax,nlat)) - # Convert to map coordinates instead of latlon to avoid latlon=True in contour and barb methods. - x2d, y2d = m(lon2d,lat2d) - # ibox: subscripts within lat/lon box. speed up triangulation in interp_weights - ibox = (m.lonmin-1 <= lons ) & (lons < m.lonmax+1) & (m.latmin-1 <= lats) & (lats < m.latmax+1) + lon2d, lat2d = np.meshgrid(np.linspace(lonmin, lonmax, nlon), np.linspace(latmin,latmax,nlat)) + # Convert to map coordinates instead of latlon to avoid transform=PlateCarree in contour method. + xyz = proj.transform_points(cartopy.crs.PlateCarree(), lon2d, lat2d) + x2d = xyz[...,0] + y2d = xyz[...,1] + # ibox: subscripts within lat/lon box plus buffer. speed up triangulation in interp_weights + ibox = (lonmin-1 <= lons ) & (lons < lonmax+1) & (latmin-1 <= lats) & (lats < latmax+1) lons = lons[ibox] lats = lats[ibox] - x, y = m(lons,lats) + xyz = proj.transform_points(cartopy.crs.PlateCarree(), lons, lats) + x = xyz[...,0] + y = xyz[...,1] - logging.debug(f"saveNewMap: triangulate {len(lons)} pts") + logging.info(f"saveNewMap: triangulate {len(lons)} pts") vtx, wts = interp_weights(np.vstack((lons,lats)).T,np.vstack((lon2d.flatten(), lat2d.flatten())).T) - pickle.dump((fig,ax,m,lons,lats,delta_deg,lon2d,lat2d,x2d,y2d,ibox,x,y,vtx,wts), open(pk_file, 'wb')) + pickle.dump((ax,extent,lons,lats,lon2d,lat2d,x2d,y2d,ibox,x,y,vtx,wts), open(plot.pk_file, 'wb')) - - -def drawOverlay(plot, pk_file): - logging.info(f"drawOverlay: pk_file={pk_file}") - ll_lat, ll_lon, ur_lat, ur_lon = domains[plot.domain]['corners'] - lat_1, lat_2, lon_0 = 32., 46., -101. - fig_width = 1080 - dpi = 90 - fig = plt.figure(dpi=dpi) - m = Basemap(projection='lcc', resolution='i', llcrnrlon=ll_lon, llcrnrlat=ll_lat, urcrnrlon=ur_lon, urcrnrlat=ur_lat, \ - lat_1=lat_1, lat_2=lat_2, lon_0=lon_0, area_thresh=1000) - - # compute height based on figure width, map aspect ratio, then add some vertical space for labels/colorbar - fig_width = fig_width/float(dpi) - fig_height = fig_width*m.aspect + 0.93 - figsize = (fig_width, fig_height) - fig.set_size_inches(figsize) - - # place map 0.7" from bottom of figure, leave rest of 0.93" at top for title (needs to be in figure-relative coords) - x,y,w,h = 0.01, 0.7/float(fig_height), 0.98, 0.98*fig_width*m.aspect/float(fig_height) - ax = fig.add_axes([x,y,w,h]) - - #drawcounties doesnt work when called by itself? so have to drawcoastines first with lw=0 - m.drawcoastlines(linewidth=0, ax=ax) - m.drawcounties(ax=ax) - ax.axis('off') - plt.savefig('overlay_counties_%s.png'%domain, dpi=90, transparent=True) - def compute_pmm(ensemble): members = ensemble.Time.size ens_mean = ensemble.mean(dim="Time", keep_attrs=True) @@ -719,26 +734,24 @@ def compute_pmm(ensemble): return ens_mean def compute_neprob(ensemble, roi=0, sigma=0.0, type='gaussian'): - if len(ensemble.shape) < 3: - logging.error('compute_neprob: needs ensemble of 2D arrays, not 1D arrays') - sys.exit(1) + roi = np.rint(roi) # round to nearest integer + assert len(ensemble.dims) >= 3, 'compute_neprob: needs ensemble of 2D arrays, not 1D arrays' y,x = np.ogrid[-roi:roi+1, -roi:roi+1] kernel = x**2 + y**2 <= roi**2 ens_roi = ndimage.filters.maximum_filter(ensemble, footprint=kernel[np.newaxis,:]) - ens_mean = np.nanmean(ens_roi, axis=0) - #ens_mean = np.nanmean(ensemble, axis=0) - if type == 'uniform': y,x = np.ogrid[-sigma:sigma+1, -sigma:sigma+1] kernel = x**2 + y**2 <= sigma**2 - ens_mean = ndimage.filters.convolve(ens_mean, kernel/float(kernel.sum())) + smoothed = ndimage.filters.convolve(ens_roi, kernel/float(kernel.sum())) elif type == 'gaussian': - ens_mean = ndimage.filters.gaussian_filter(ens_mean, sigma) + # smooth last 2 dimensions (not TimeMember dimension) + smoothed = ndimage.filters.gaussian_filter(ens_roi, (0,sigma,sigma)) else: logging.error(f"compute_neprob: unknown filter {type}") sys.exit(1) - return ens_mean + ensemble.values = smoothed + return ensemble def compute_prob3d(ensemble, roi=0, sigma=0.): logging.info(f"compute_prob3d: roi={roi} sigma={sigma}") From dda5532a6ba42114d4e317d946f47111e7861de7 Mon Sep 17 00:00:00 2001 From: ahijevyc Date: Sun, 19 Feb 2023 11:59:52 -0700 Subject: [PATCH 44/68] first commit --- environment.yaml | 12 ++++++++++++ ncar.png | 1 + 2 files changed, 13 insertions(+) create mode 100644 environment.yaml create mode 120000 ncar.png diff --git a/environment.yaml b/environment.yaml new file mode 100644 index 0000000..6e7254a --- /dev/null +++ b/environment.yaml @@ -0,0 +1,12 @@ +name: webplot +channels: + - conda-forge + - defaults +dependencies: + - pygrib + - scipy + - xarray + - netcdf4 + - dask + - metpy +prefix: /glade/u/home/ahijevyc/miniconda3/envs/webplot diff --git a/ncar.png b/ncar.png new file mode 120000 index 0000000..c892200 --- /dev/null +++ b/ncar.png @@ -0,0 +1 @@ +/glade/work/ahijevyc/share/rt_ensemble/python_scripts/ncar.png \ No newline at end of file From 1f46c29f49f2cef2dd03f46592709307bb6e6ee4 Mon Sep 17 00:00:00 2001 From: ahijevyc Date: Sun, 19 Feb 2023 12:23:03 -0700 Subject: [PATCH 45/68] update README --- README.md | 43 ++++++++++++++++-- ..._vort_cell.cpython-310-x86_64-linux-gnu.so | Bin 47632 -> 0 bytes 2 files changed, 40 insertions(+), 3 deletions(-) delete mode 100755 mpas_vort_cell.cpython-310-x86_64-linux-gnu.so diff --git a/README.md b/README.md index 6a90086..611c7c3 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,46 @@ # webplot plotting package -This is a simplified version of the webplot plotting library used to create graphics for the NCAR ensemble system. +create graphics for MPAS ensemble For example, to create a 2-m ensemble mean temperature plot using ensemble data initialized at 2017061900, valid at 2017061912: -make_webplot.py -d=2017070500 -f=t2_mean -tr=12 -t='2-m Ensemble Mean Temperature (F)' +make_webplot.py 20170705 --fill t2/mean --fhr 12 --title '2-m Ensemble Mean Temperature (F)' -The package uses basemap objects that are stored as pickle files. These should be created with the saveNewMap() function in webplot.py before attempting to plot. +``` +usage: make_webplot.py [-h] [--autolevels] [-b BARB] [-c CONTOUR] [-con] [-d] [-f FILL] [--fhr FHR [FHR ...]] + [--meshstr MESHSTR] [--nbarbs NBARBS] [--nlon_max NLON_MAX] [--nlat_max NLAT_MAX] + [--sigma SIGMA] [-t TITLE] + date + +Web plotting script for NCAR ensemble + +positional arguments: + date initialization datetime + +options: + -h, --help show this help message and exit + --autolevels use min/max to determine levels for plot + -b BARB, --barb BARB barb field (FIELD_PRODUCT_THRESH) + -c CONTOUR, --contour CONTOUR + contour field (FIELD_PRODUCT_THRESH) + -con, --convert run final image through imagemagick + -d, --debug turn on debugging + -f FILL, --fill FILL fill field (FIELD_PRODUCT_THRESH), field keys:precip,precip-24hr,precip- + 48hr,precipacc,sbcape,mlcape,mucape,sbcinh,mlcinh,pwat,t2,t2depart,t2- + 0c,mslp,td2,td2depart,thetae,thetapv,rh2m,pblh,hmuh,hmneguh,hmuh03,hmuh01,rvort1,sspf,hmup + ,hmdn,hmwind,hmgrp,cref,ref1km,srh3,srh1,shr06mag,shr01mag,zlfc,zlcl,ltg1,ltg2,ltg3,olrtoa + ,thck1000-500,thck1000- + 850,hgt200,hgt250,hgt300,hgt500,hgt700,hgt850,hgt925,speed200,speed250,speed300,speed500,s + peed700,speed850,speed925,temp200,temp250,temp300,temp500,temp700,temp850,temp925,td500,td + 700,td850,td925,rh300,rh500,rh700,rh850,rh925,pvort320k,speed10m,speed10m- + tc,stp,uhratio,crefuh,wind10m,windsfc,wind1km,wind6km,windpv,shr06,shr01,wind200,wind250,w + ind300,wind500,wind700,wind850,wind925,vort500,vort700,vort850,vortpv + --fhr FHR [FHR ...] list of forecast hours + --meshstr MESHSTR mesh id or path to defining mesh + --nbarbs NBARBS max barbs in one dimension + --nlon_max NLON_MAX max pts in longitude dimension + --nlat_max NLAT_MAX max pts in latitude dimension + --sigma SIGMA smooth probabilities using gaussian smoother + -t TITLE, --title TITLE + title for plot +``` diff --git a/mpas_vort_cell.cpython-310-x86_64-linux-gnu.so b/mpas_vort_cell.cpython-310-x86_64-linux-gnu.so deleted file mode 100755 index 5d7a553adfe096bb4eb78763bbb51e146177c59f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 47632 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%0A#UcrJ+wR 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 Date: Sun, 19 Feb 2023 12:25:44 -0700 Subject: [PATCH 46/68] update README --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 611c7c3..623b7a9 100644 --- a/README.md +++ b/README.md @@ -20,12 +20,12 @@ positional arguments: options: -h, --help show this help message and exit --autolevels use min/max to determine levels for plot - -b BARB, --barb BARB barb field (FIELD_PRODUCT_THRESH) + -b BARB, --barb BARB barb field (FIELD/PRODUCT/THRESH) -c CONTOUR, --contour CONTOUR - contour field (FIELD_PRODUCT_THRESH) + contour field (FIELD/PRODUCT/THRESH) -con, --convert run final image through imagemagick -d, --debug turn on debugging - -f FILL, --fill FILL fill field (FIELD_PRODUCT_THRESH), field keys:precip,precip-24hr,precip- + -f FILL, --fill FILL fill field (FIELD/PRODUCT/THRESH), field keys:precip,precip-24hr,precip- 48hr,precipacc,sbcape,mlcape,mucape,sbcinh,mlcinh,pwat,t2,t2depart,t2- 0c,mslp,td2,td2depart,thetae,thetapv,rh2m,pblh,hmuh,hmneguh,hmuh03,hmuh01,rvort1,sspf,hmup ,hmdn,hmwind,hmgrp,cref,ref1km,srh3,srh1,shr06mag,shr01mag,zlfc,zlcl,ltg1,ltg2,ltg3,olrtoa From 8d882002e8ec3308fd89ae85709984a5e3e1c07c Mon Sep 17 00:00:00 2001 From: David Ahijevych Date: Sun, 19 Feb 2023 12:48:27 -0700 Subject: [PATCH 47/68] Update README.md --- README.md | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 623b7a9..3a86611 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,16 @@ create graphics for MPAS ensemble -For example, to create a 2-m ensemble mean temperature plot using ensemble data initialized at 2017061900, valid at 2017061912: +For example, to plot max precipitation accumulation, mean 500hPa vorticity, and wind barbs using data +initialized at 2017061900, valid from 2017061912 to 2017061918: -make_webplot.py 20170705 --fill t2/mean --fhr 12 --title '2-m Ensemble Mean Temperature (F)' +make_webplot.py 20170619 --fill precipacc/max --fhr 12 18 --contour vort500/mean --barb wind500/mean + --title 'max precip accumuation, mean 500hPa vort, mean 500hPa wind barbs' ``` -usage: make_webplot.py [-h] [--autolevels] [-b BARB] [-c CONTOUR] [-con] [-d] [-f FILL] [--fhr FHR [FHR ...]] - [--meshstr MESHSTR] [--nbarbs NBARBS] [--nlon_max NLON_MAX] [--nlat_max NLAT_MAX] - [--sigma SIGMA] [-t TITLE] +usage: make_webplot.py [-h] [--autolevels] [-b BARB] [-c CONTOUR] [-con] [-d] [-f FILL] + [--fhr FHR [FHR ...]] [--meshstr MESHSTR] [--nbarbs NBARBS] + [--nlon_max NLON_MAX] [--nlat_max NLAT_MAX] [--sigma SIGMA] [-t TITLE] date Web plotting script for NCAR ensemble @@ -20,21 +22,25 @@ positional arguments: options: -h, --help show this help message and exit --autolevels use min/max to determine levels for plot - -b BARB, --barb BARB barb field (FIELD/PRODUCT/THRESH) + -b BARB, --barb BARB barb field (FIELD_PRODUCT_THRESH) -c CONTOUR, --contour CONTOUR - contour field (FIELD/PRODUCT/THRESH) + contour field (FIELD_PRODUCT_THRESH) -con, --convert run final image through imagemagick -d, --debug turn on debugging - -f FILL, --fill FILL fill field (FIELD/PRODUCT/THRESH), field keys:precip,precip-24hr,precip- + -f FILL, --fill FILL fill field (FIELD_PRODUCT_THRESH), FIELD options:precip,precip- + 24hr,precip- 48hr,precipacc,sbcape,mlcape,mucape,sbcinh,mlcinh,pwat,t2,t2depart,t2- - 0c,mslp,td2,td2depart,thetae,thetapv,rh2m,pblh,hmuh,hmneguh,hmuh03,hmuh01,rvort1,sspf,hmup - ,hmdn,hmwind,hmgrp,cref,ref1km,srh3,srh1,shr06mag,shr01mag,zlfc,zlcl,ltg1,ltg2,ltg3,olrtoa - ,thck1000-500,thck1000- - 850,hgt200,hgt250,hgt300,hgt500,hgt700,hgt850,hgt925,speed200,speed250,speed300,speed500,s - peed700,speed850,speed925,temp200,temp250,temp300,temp500,temp700,temp850,temp925,td500,td - 700,td850,td925,rh300,rh500,rh700,rh850,rh925,pvort320k,speed10m,speed10m- - tc,stp,uhratio,crefuh,wind10m,windsfc,wind1km,wind6km,windpv,shr06,shr01,wind200,wind250,w - ind300,wind500,wind700,wind850,wind925,vort500,vort700,vort850,vortpv + 0c,mslp,td2,td2depart,thetae,thetapv,rh2m,pblh,hmuh,hmneguh,hmuh03,hmu + h01,rvort1,sspf,hmup,hmdn,hmwind,hmgrp,cref,ref1km,srh3,srh1,shr06mag, + shr01mag,zlfc,zlcl,ltg1,ltg2,ltg3,olrtoa,thck1000-500,thck1000- + 850,hgt200,hgt250,hgt300,hgt500,hgt700,hgt850,hgt925,speed200,speed250 + ,speed300,speed500,speed700,speed850,speed925,temp200,temp250,temp300, + temp500,temp700,temp850,temp925,td500,td700,td850,td925,rh300,rh500,rh + 700,rh850,rh925,pvort320k,speed10m,speed10m- + tc,stp,uhratio,crefuh,wind10m,windsfc,wind1km,wind6km,windpv,shr06,shr + 01,wind200,wind250,wind300,wind500,wind700,wind850,wind925,vort500,vor + t700,vort850,vortpv PRODUCT may be one of [max,maxstamp,min,mean,means + tamp,prob,neprob,problt,neproblt,paintball,stamp,spaghetti] --fhr FHR [FHR ...] list of forecast hours --meshstr MESHSTR mesh id or path to defining mesh --nbarbs NBARBS max barbs in one dimension From f18dd856adff9b6f216af5cce518fa4a8d9d4e22 Mon Sep 17 00:00:00 2001 From: ahijevyc Date: Sun, 19 Feb 2023 12:49:10 -0700 Subject: [PATCH 48/68] explain PRODUCT in usage --- webplot.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/webplot.py b/webplot.py index c0eff56..4d7d3a7 100755 --- a/webplot.py +++ b/webplot.py @@ -380,7 +380,9 @@ def parseargs(): parser.add_argument('-c', '--contour', help='contour field (FIELD_PRODUCT_THRESH)') parser.add_argument('-con', '--convert', default=True, action='store_false', help='run final image through imagemagick') parser.add_argument('-d', '--debug', action='store_true', help='turn on debugging') - parser.add_argument('-f', '--fill', help='fill field (FIELD_PRODUCT_THRESH), field keys:'+','.join(list(fieldinfo.keys()))) + parser.add_argument('-f', '--fill', help='fill field (FIELD_PRODUCT_THRESH), FIELD options:' + f"{','.join(list(fieldinfo.keys()))} PRODUCT may be one of [max,maxstamp,min,mean,meanstamp," + "prob,neprob,problt,neproblt,paintball,stamp,spaghetti]") parser.add_argument('--fhr', nargs='+', type=float, default=[12], help='list of forecast hours') parser.add_argument('--meshstr', type=str, default='uni', help='mesh id or path to defining mesh') parser.add_argument('--nbarbs', type=int, default=32, help='max barbs in one dimension') From f660f3a4f815533aff9d950396c8e5cc51ad9446 Mon Sep 17 00:00:00 2001 From: ahijevyc Date: Tue, 21 Feb 2023 09:12:56 -0700 Subject: [PATCH 49/68] remove make script. get domain from command line, and plot automatically --- README.md | 47 ++++++++++++++++++++++++++--------------------- make_webplot.py | 21 --------------------- webplot.py | 10 ++++++++-- 3 files changed, 34 insertions(+), 44 deletions(-) delete mode 100755 make_webplot.py diff --git a/README.md b/README.md index 3a86611..cecb887 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,23 @@ # webplot plotting package -create graphics for MPAS ensemble +create graphics from MPAS ensemble -For example, to plot max precipitation accumulation, mean 500hPa vorticity, and wind barbs using data -initialized at 2017061900, valid from 2017061912 to 2017061918: +For example, to plot max precipitation accumulation, mean 500hPa vorticity, and mean wind barbs +from the valid period [2017061912, 2017061918]: -make_webplot.py 20170619 --fill precipacc/max --fhr 12 18 --contour vort500/mean --barb wind500/mean +``` +python webplot.py 20170619 --fill precipacc/max --fhr 12 18 --contour vort500/mean --barb wind500/mean --title 'max precip accumuation, mean 500hPa vort, mean 500hPa wind barbs' +``` + +MPAS initialization time provided as first argument. ``` -usage: make_webplot.py [-h] [--autolevels] [-b BARB] [-c CONTOUR] [-con] [-d] [-f FILL] - [--fhr FHR [FHR ...]] [--meshstr MESHSTR] [--nbarbs NBARBS] - [--nlon_max NLON_MAX] [--nlat_max NLAT_MAX] [--sigma SIGMA] [-t TITLE] - date +usage: webplot.py [-h] [--autolevels] [-b BARB] [-c CONTOUR] [-con] [-d] + [--domain {CONUS,NA,SGP,NGP,CGP,SW,NW,SE,NE,MATL}] [-f FILL] [--fhr FHR [FHR ...]] + [--meshstr MESHSTR] [--nbarbs NBARBS] [--nlon_max NLON_MAX] [--nlat_max NLAT_MAX] + [--sigma SIGMA] [-t TITLE] + date Web plotting script for NCAR ensemble @@ -27,20 +32,20 @@ options: contour field (FIELD_PRODUCT_THRESH) -con, --convert run final image through imagemagick -d, --debug turn on debugging - -f FILL, --fill FILL fill field (FIELD_PRODUCT_THRESH), FIELD options:precip,precip- - 24hr,precip- + --domain {CONUS,NA,SGP,NGP,CGP,SW,NW,SE,NE,MATL} + domain of plot + -f FILL, --fill FILL fill field (FIELD_PRODUCT_THRESH), FIELD options:precip,precip-24hr,precip- 48hr,precipacc,sbcape,mlcape,mucape,sbcinh,mlcinh,pwat,t2,t2depart,t2- - 0c,mslp,td2,td2depart,thetae,thetapv,rh2m,pblh,hmuh,hmneguh,hmuh03,hmu - h01,rvort1,sspf,hmup,hmdn,hmwind,hmgrp,cref,ref1km,srh3,srh1,shr06mag, - shr01mag,zlfc,zlcl,ltg1,ltg2,ltg3,olrtoa,thck1000-500,thck1000- - 850,hgt200,hgt250,hgt300,hgt500,hgt700,hgt850,hgt925,speed200,speed250 - ,speed300,speed500,speed700,speed850,speed925,temp200,temp250,temp300, - temp500,temp700,temp850,temp925,td500,td700,td850,td925,rh300,rh500,rh - 700,rh850,rh925,pvort320k,speed10m,speed10m- - tc,stp,uhratio,crefuh,wind10m,windsfc,wind1km,wind6km,windpv,shr06,shr - 01,wind200,wind250,wind300,wind500,wind700,wind850,wind925,vort500,vor - t700,vort850,vortpv PRODUCT may be one of [max,maxstamp,min,mean,means - tamp,prob,neprob,problt,neproblt,paintball,stamp,spaghetti] + 0c,mslp,td2,td2depart,thetae,thetapv,rh2m,pblh,hmuh,hmneguh,hmuh03,hmuh01,rvort1,sspf,hmu + p,hmdn,hmwind,hmgrp,cref,ref1km,srh3,srh1,shr06mag,shr01mag,zlfc,zlcl,ltg1,ltg2,ltg3,olrt + oa,thck1000-500,thck1000- + 850,hgt200,hgt250,hgt300,hgt500,hgt700,hgt850,hgt925,speed200,speed250,speed300,speed500, + speed700,speed850,speed925,temp200,temp250,temp300,temp500,temp700,temp850,temp925,td500, + td700,td850,td925,rh300,rh500,rh700,rh850,rh925,pvort320k,speed10m,speed10m- + tc,stp,uhratio,crefuh,wind10m,windsfc,wind1km,wind6km,windpv,shr06,shr01,wind200,wind250, + wind300,wind500,wind700,wind850,wind925,vort500,vort700,vort850,vortpv PRODUCT may be one + of + [max,maxstamp,min,mean,meanstamp,prob,neprob,problt,neproblt,paintball,stamp,spaghetti] --fhr FHR [FHR ...] list of forecast hours --meshstr MESHSTR mesh id or path to defining mesh --nbarbs NBARBS max barbs in one dimension diff --git a/make_webplot.py b/make_webplot.py deleted file mode 100755 index 3f5be85..0000000 --- a/make_webplot.py +++ /dev/null @@ -1,21 +0,0 @@ -import logging -import pdb -import time -from webplot import webPlot - - - -stime = time.time() - -regions = ['CONUS', 'NGP', 'SGP', 'CGP', 'MATL', 'NE', 'NW', 'SE', 'SW'] -regions = ['CONUS', 'CGP'] - - -for dom in regions: - Plot = webPlot(domain=dom) - logging.debug('Writing Image') - Plot.saveFigure() - -etime = time.time() -logging.info(f'End Plotting (took {etime-stime:.2f} sec)') - diff --git a/webplot.py b/webplot.py index 4d7d3a7..ffdcd1c 100755 --- a/webplot.py +++ b/webplot.py @@ -31,13 +31,13 @@ class webPlot: '''A class to plot data from NCAR ensemble''' - def __init__(self, domain=None): + def __init__(self): self.opts = parseargs() self.initdate = pd.to_datetime(self.opts['date']) self.ENS_SIZE = self.opts['ENS_SIZE'] self.autolevels = self.opts['autolevels'] self.debug = self.opts['debug'] - self.domain = domain + self.domain = self.opts['domain'] self.fhr = self.opts['fhr'] self.meshstr = self.opts['meshstr'] self.nbarbs = self.opts['nbarbs'] @@ -50,6 +50,7 @@ def __init__(self, domain=None): self.data, self.missing_members = readEnsemble(self) self.plotFields() self.plotTitleTimes() + self.saveFigure() def createFilename(self): for f in ['fill', 'contour','barb']: # CSS added this for loop and everything in it @@ -380,6 +381,7 @@ def parseargs(): parser.add_argument('-c', '--contour', help='contour field (FIELD_PRODUCT_THRESH)') parser.add_argument('-con', '--convert', default=True, action='store_false', help='run final image through imagemagick') parser.add_argument('-d', '--debug', action='store_true', help='turn on debugging') + parser.add_argument('--domain', type=str, choices=domains.keys(), default="CONUS", help='domain of plot') parser.add_argument('-f', '--fill', help='fill field (FIELD_PRODUCT_THRESH), FIELD options:' f"{','.join(list(fieldinfo.keys()))} PRODUCT may be one of [max,maxstamp,min,mean,meanstamp," "prob,neprob,problt,neproblt,paintball,stamp,spaghetti]") @@ -852,3 +854,7 @@ def computefrzdepth(t): frz_at_surface = np.where(t[0,:] < 33, True, False) #pts where surface T is below 33F max_column_t = np.amax(t, axis=0) above_frz_aloft = np.where(max_column_t > 32, True, False) #pts where max column T is above 32F + +if __name__ == "__main__": + webPlot() + From 51a58d4a81a0d2e1adfb0d837dab5218d5e22fef Mon Sep 17 00:00:00 2001 From: ahijevyc Date: Tue, 21 Feb 2023 09:18:09 -0700 Subject: [PATCH 50/68] update README --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index cecb887..b930805 100644 --- a/README.md +++ b/README.md @@ -3,16 +3,16 @@ create graphics from MPAS ensemble For example, to plot max precipitation accumulation, mean 500hPa vorticity, and mean wind barbs -from the valid period [2017061912, 2017061918]: +from forecast hours [12, 18]: -``` -python webplot.py 20170619 --fill precipacc/max --fhr 12 18 --contour vort500/mean --barb wind500/mean +```python +python webplot.py 20180619 --fill precipacc/max --fhr 12 18 --contour vort500/mean --barb wind500/mean --title 'max precip accumuation, mean 500hPa vort, mean 500hPa wind barbs' ``` MPAS initialization time provided as first argument. -``` +```python usage: webplot.py [-h] [--autolevels] [-b BARB] [-c CONTOUR] [-con] [-d] [--domain {CONUS,NA,SGP,NGP,CGP,SW,NW,SE,NE,MATL}] [-f FILL] [--fhr FHR [FHR ...]] [--meshstr MESHSTR] [--nbarbs NBARBS] [--nlon_max NLON_MAX] [--nlat_max NLAT_MAX] From c6669cc1e764515a7fa38f6c68fcdc22a78c0559 Mon Sep 17 00:00:00 2001 From: ahijevyc Date: Tue, 21 Feb 2023 09:21:30 -0700 Subject: [PATCH 51/68] update README --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b930805..558a5c8 100644 --- a/README.md +++ b/README.md @@ -5,14 +5,13 @@ create graphics from MPAS ensemble For example, to plot max precipitation accumulation, mean 500hPa vorticity, and mean wind barbs from forecast hours [12, 18]: -```python -python webplot.py 20180619 --fill precipacc/max --fhr 12 18 --contour vort500/mean --barb wind500/mean - --title 'max precip accumuation, mean 500hPa vort, mean 500hPa wind barbs' +``` +python webplot.py 20180619 --fill precipacc/max --fhr 12 18 --contour vort500/mean --barb wind500/mean --title 'max precip accumuation, mean 500hPa vort, mean 500hPa wind barbs' ``` MPAS initialization time provided as first argument. -```python +``` usage: webplot.py [-h] [--autolevels] [-b BARB] [-c CONTOUR] [-con] [-d] [--domain {CONUS,NA,SGP,NGP,CGP,SW,NW,SE,NE,MATL}] [-f FILL] [--fhr FHR [FHR ...]] [--meshstr MESHSTR] [--nbarbs NBARBS] [--nlon_max NLON_MAX] [--nlat_max NLAT_MAX] From 3dd137329e551d36e6ef393f0ee398e47514c7ee Mon Sep 17 00:00:00 2001 From: ahijevyc Date: Tue, 21 Feb 2023 09:22:40 -0700 Subject: [PATCH 52/68] update README --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 558a5c8..87741cd 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,8 @@ For example, to plot max precipitation accumulation, mean 500hPa vorticity, and from forecast hours [12, 18]: ``` -python webplot.py 20180619 --fill precipacc/max --fhr 12 18 --contour vort500/mean --barb wind500/mean --title 'max precip accumuation, mean 500hPa vort, mean 500hPa wind barbs' +python webplot.py 20180619 --fill precipacc/max --fhr 12 18 --contour vort500/mean \ + --barb wind500/mean --title 'max precip accumuation, mean 500hPa vort, mean 500hPa wind barbs' ``` MPAS initialization time provided as first argument. From 7cabb6ac4dec8b3ef0b7b77788c3f53bc0b211e1 Mon Sep 17 00:00:00 2001 From: ahijevyc Date: Tue, 21 Feb 2023 09:52:45 -0700 Subject: [PATCH 53/68] update README --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 87741cd..9f7c5e6 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,12 @@ create graphics from MPAS ensemble -For example, to plot max precipitation accumulation, mean 500hPa vorticity, and mean wind barbs -from forecast hours [12, 18]: +For example, to plot max precipitation accumulation with contours of 500hPa mean height, and +mean 500hPa wind barbs from forecast hours [12, 18]: ``` -python webplot.py 20180619 --fill precipacc/max --fhr 12 18 --contour vort500/mean \ - --barb wind500/mean --title 'max precip accumuation, mean 500hPa vort, mean 500hPa wind barbs' +python webplot.py 20180619 --fill precipacc/max --fhr 12 18 --contour hgt500/mean \ + --barb wind500/mean --title 'Max precip acc, mean 500hPa hgt [m] and wind barbs [kt]' ``` MPAS initialization time provided as first argument. From c9e5a7b68cd12ac3610cafcbdd34db1334f515ef Mon Sep 17 00:00:00 2001 From: ahijevyc Date: Tue, 21 Feb 2023 16:32:03 -0700 Subject: [PATCH 54/68] update README, fix title position --- README.md | 63 +++++++++++++++++++++++++++++++----------------------- mpas.py | 3 ++- webplot.py | 24 +++++++++++++-------- 3 files changed, 53 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 9f7c5e6..e3e32c7 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ For example, to plot max precipitation accumulation with contours of 500hPa mean mean 500hPa wind barbs from forecast hours [12, 18]: ``` -python webplot.py 20180619 --fill precipacc/max --fhr 12 18 --contour hgt500/mean \ +python webplot.py 20170518 --fill precipacc/max --fhr 12 18 --contour hgt500/mean \ --barb wind500/mean --title 'Max precip acc, mean 500hPa hgt [m] and wind barbs [kt]' ``` @@ -15,43 +15,52 @@ MPAS initialization time provided as first argument. ``` usage: webplot.py [-h] [--autolevels] [-b BARB] [-c CONTOUR] [-con] [-d] [--domain {CONUS,NA,SGP,NGP,CGP,SW,NW,SE,NE,MATL}] [-f FILL] [--fhr FHR [FHR ...]] - [--meshstr MESHSTR] [--nbarbs NBARBS] [--nlon_max NLON_MAX] [--nlat_max NLAT_MAX] - [--sigma SIGMA] [-t TITLE] + [--idir IDIR] [--meshstr MESHSTR] [--nbarbs NBARBS] [--nlon_max NLON_MAX] + [--nlat_max NLAT_MAX] [--sigma SIGMA] [-t TITLE] date Web plotting script for NCAR ensemble positional arguments: - date initialization datetime + date model initialization time options: -h, --help show this help message and exit - --autolevels use min/max to determine levels for plot - -b BARB, --barb BARB barb field (FIELD_PRODUCT_THRESH) + --autolevels use min/max to determine levels for plot (default: False) + -b BARB, --barb BARB barb field (FIELD_PRODUCT_THRESH) (default: None) -c CONTOUR, --contour CONTOUR - contour field (FIELD_PRODUCT_THRESH) - -con, --convert run final image through imagemagick - -d, --debug turn on debugging + contour field (FIELD_PRODUCT_THRESH) (default: None) + -con, --convert run final image through imagemagick (default: True) + -d, --debug turn on debugging (default: False) --domain {CONUS,NA,SGP,NGP,CGP,SW,NW,SE,NE,MATL} - domain of plot + domain of plot (default: CONUS) -f FILL, --fill FILL fill field (FIELD_PRODUCT_THRESH), FIELD options:precip,precip-24hr,precip- 48hr,precipacc,sbcape,mlcape,mucape,sbcinh,mlcinh,pwat,t2,t2depart,t2- - 0c,mslp,td2,td2depart,thetae,thetapv,rh2m,pblh,hmuh,hmneguh,hmuh03,hmuh01,rvort1,sspf,hmu - p,hmdn,hmwind,hmgrp,cref,ref1km,srh3,srh1,shr06mag,shr01mag,zlfc,zlcl,ltg1,ltg2,ltg3,olrt - oa,thck1000-500,thck1000- - 850,hgt200,hgt250,hgt300,hgt500,hgt700,hgt850,hgt925,speed200,speed250,speed300,speed500, - speed700,speed850,speed925,temp200,temp250,temp300,temp500,temp700,temp850,temp925,td500, - td700,td850,td925,rh300,rh500,rh700,rh850,rh925,pvort320k,speed10m,speed10m- - tc,stp,uhratio,crefuh,wind10m,windsfc,wind1km,wind6km,windpv,shr06,shr01,wind200,wind250, - wind300,wind500,wind700,wind850,wind925,vort500,vort700,vort850,vortpv PRODUCT may be one - of - [max,maxstamp,min,mean,meanstamp,prob,neprob,problt,neproblt,paintball,stamp,spaghetti] - --fhr FHR [FHR ...] list of forecast hours - --meshstr MESHSTR mesh id or path to defining mesh - --nbarbs NBARBS max barbs in one dimension - --nlon_max NLON_MAX max pts in longitude dimension - --nlat_max NLAT_MAX max pts in latitude dimension - --sigma SIGMA smooth probabilities using gaussian smoother + 0c,mslp,td2,td2depart,thetae,thetapv,rh2m,pblh,hmuh,hmneguh,hmuh03,hmuh01,rvort1,sspf, + hmup,hmdn,hmwind,hmgrp,cref,ref1km,srh3,srh1,shr06mag,shr01mag,zlfc,zlcl,ltg1,ltg2,ltg + 3,olrtoa,thck1000-500,thck1000- + 850,hgt200,hgt250,hgt300,hgt500,hgt700,hgt850,hgt925,speed200,speed250,speed300,speed5 + 00,speed700,speed850,speed925,temp200,temp250,temp300,temp500,temp700,temp850,temp925, + td500,td700,td850,td925,rh300,rh500,rh700,rh850,rh925,pvort320k,speed10m,speed10m- + tc,stp,uhratio,crefuh,wind10m,windsfc,wind1km,wind6km,windpv,shr06,shr01,wind200,wind2 + 50,wind300,wind500,wind700,wind850,wind925,vort500,vort700,vort850,vortpv PRODUCT may + be one of [max,maxstamp,min,mean,meanstamp,prob,neprob,problt,neproblt,paintball,stamp + ,spaghetti] (default: None) + --fhr FHR [FHR ...] list of forecast hours (default: [12]) + --idir IDIR path to model output (default: /glade/campaign/mmm/parc/schwartz/MPAS_ensemble_paper) + --meshstr MESHSTR mesh id ['uni', '15-3km_mesh', '15km_mesh'] or path to file with lat/lon/area of mesh + (default: 15km_mesh) + --nbarbs NBARBS max barbs in one dimension (default: 32) + --nlon_max NLON_MAX max pts in longitude dimension (default: 1500) + --nlat_max NLAT_MAX max pts in latitude dimension (default: 1500) + --sigma SIGMA smooth probabilities using gaussian smoother (default: 2) -t TITLE, --title TITLE - title for plot + title for plot (default: None) ``` + +### Installation + +Use environment.yaml to create conda environment. + +Use f2py3 to install vert2cell. + diff --git a/mpas.py b/mpas.py index 514a2f7..cc629dd 100644 --- a/mpas.py +++ b/mpas.py @@ -64,6 +64,7 @@ def makeEnsembleList(Plot): + idir = Plot.idir initdate = Plot.initdate fhr = Plot.fhr ENS_SIZE = Plot.ENS_SIZE @@ -76,7 +77,7 @@ def makeEnsembleList(Plot): validstr = (initdate + datetime.timedelta(hours=hr)).strftime('%Y-%m-%d_%H.%M.%S') yyyymmddhh = initdate.strftime('%Y%m%d%H') for mem in range(1,ENS_SIZE+1): - diag = f'/glade/campaign/mmm/parc/schwartz/MPAS_ensemble_paper/{meshstr}/{yyyymmddhh}/ens_{mem}/diag.{validstr}.nc' + diag = os.path.join(idir, f"{meshstr}/{yyyymmddhh}/ens_{mem}/diag.{validstr}.nc") logging.debug(diag) if os.path.exists(diag): file_list.append(diag) else: diff --git a/webplot.py b/webplot.py index ffdcd1c..cd90149 100755 --- a/webplot.py +++ b/webplot.py @@ -39,6 +39,7 @@ def __init__(self): self.debug = self.opts['debug'] self.domain = self.opts['domain'] self.fhr = self.opts['fhr'] + self.idir = self.opts['idir'] self.meshstr = self.opts['meshstr'] self.nbarbs = self.opts['nbarbs'] self.nlat_max = self.opts['nlat_max'] @@ -107,16 +108,17 @@ def get_mpas_mesh(self): def plotTitleTimes(self): fontdict = {'family':'monospace', 'size':12, 'weight':'bold'} - # place title and times above corners of map - x0, y1 = self.ax.transAxes.transform((0,1)) - x0, y0 = self.ax.transAxes.transform((0,0)) - x1, y1 = self.ax.transAxes.transform((1,1)) - self.ax.text(x0, y1+10, self.title, fontdict=fontdict, transform=None) + # place title and times above plot + verticalalignment="bottom" + self.ax.text(0, 1, self.title, fontdict=fontdict, horizontalalignment='left', + verticalalignment=verticalalignment, transform=self.ax.transAxes, wrap=True) + logging.info(self.title) fontdict = {'family':'monospace'} initstr, validstr = self.getInitValidStr() + logging.info(initstr+"\n"+validstr) self.ax.text(1, 1, initstr+"\n"+validstr, fontdict=fontdict, horizontalalignment='right', - verticalalignment="bottom", transform=self.ax.transAxes) + verticalalignment=verticalalignment, transform=self.ax.transAxes) # Plot missing members if len(self.missing_members): @@ -374,8 +376,9 @@ def saveFigure(self, transparent=False): def parseargs(): '''Parse arguments and return dictionary of fill, contour and barb field parameters''' - parser = argparse.ArgumentParser(description='Web plotting script for NCAR ensemble') - parser.add_argument('date', help='initialization datetime') + parser = argparse.ArgumentParser(description='Web plotting script for NCAR ensemble', + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + parser.add_argument('date', help='model initialization time') parser.add_argument('--autolevels', action='store_true', help='use min/max to determine levels for plot') parser.add_argument('-b', '--barb', help='barb field (FIELD_PRODUCT_THRESH)') parser.add_argument('-c', '--contour', help='contour field (FIELD_PRODUCT_THRESH)') @@ -386,7 +389,10 @@ def parseargs(): f"{','.join(list(fieldinfo.keys()))} PRODUCT may be one of [max,maxstamp,min,mean,meanstamp," "prob,neprob,problt,neproblt,paintball,stamp,spaghetti]") parser.add_argument('--fhr', nargs='+', type=float, default=[12], help='list of forecast hours') - parser.add_argument('--meshstr', type=str, default='uni', help='mesh id or path to defining mesh') + parser.add_argument('--idir', type=str, default='/glade/campaign/mmm/parc/schwartz/MPAS_ensemble_paper', + help="path to model output") + parser.add_argument('--meshstr', type=str, default='15km_mesh', + help=f'mesh id {list(mesh_config.keys())} or path to file with lat/lon/area of mesh') parser.add_argument('--nbarbs', type=int, default=32, help='max barbs in one dimension') parser.add_argument('--nlon_max', type=int, default=1500, help='max pts in longitude dimension') parser.add_argument('--nlat_max', type=int, default=1500, help='max pts in latitude dimension') From c039473eb4b23b00ccad8a2eb5dbc8152d2bf3f4 Mon Sep 17 00:00:00 2001 From: ahijevyc Date: Tue, 21 Feb 2023 16:33:52 -0700 Subject: [PATCH 55/68] update README --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e3e32c7..5e23a80 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # webplot plotting package -create graphics from MPAS ensemble +### create graphics from MPAS ensemble For example, to plot max precipitation accumulation with contours of 500hPa mean height, and mean 500hPa wind barbs from forecast hours [12, 18]: @@ -60,7 +60,6 @@ options: ### Installation -Use environment.yaml to create conda environment. +Create conda environment described in environment.yaml. Use f2py3 to install vert2cell. - From 5433a4e5804c62e8303f19024dd9f3edb4afbf82 Mon Sep 17 00:00:00 2001 From: ahijevyc Date: Tue, 21 Feb 2023 16:46:51 -0700 Subject: [PATCH 56/68] rm latlonfile.nc --- latlonfile.nc | Bin 12453304 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 latlonfile.nc diff --git a/latlonfile.nc b/latlonfile.nc deleted file mode 100644 index e23a41def729ad3e70ad307956b5d8be8757d36c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12453304 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

=x(vWXIN+Ph}Q=1 z3EJT8Q#N=5@r=u7Z14hppAoXb6Q~QO7z7s%?%QC4YbtDTJ7O0tAiL{v~1(@3&e1>#V7WG+F;}y!HR9if?$&S$_+0jrE;;!#m0PuPL|w zi~ZJr4&@8^UiZ=DAF^uw`w_d?z0Ud*edhgX$-G}@n)kzE^S)7UUWc_6Bg8N^vF76G zB=bJRx{Jmf^WIWO{DK(8#pUKLsUm(+LHvUBYxdKryC$FbMKOG*ydiNy$u|u5o#uZ!7R~^_;xoji|69_b~pQftF)f_R_nRmv7QRb z{GPQKClj|gntH#k-u&&07VFuj-8|p%E~rmQZxNGt$!nfbes_18=czXHJlJHOzt)=P zrZV$fk#C+0GR<>Z9Pf@IhLPRHyQ4UkQpmfbQmp%bGuHiCvvt2yV%;yZrXt|CZV&4% zo@V{UgN}9owaU70BF0havF`H=t^3q@>o(#XhZ5sRr`~R~u?;bb?^12kC#|+=mKeqh z#3F`QY*SacZF)Rpo9<)nMSY8Hx{mn8C9KgXq5O&bJ~EHB7fr0am}c!o3TrQj=iQx4 z{Es+aJMoDwVts>*vB?JW{hKlLw%2^0koOJab_>R4B5NH|7_a*-+0en2Hgp8B%HtW! zr!jWV%i_IGO{}e2wxL_dyQ{;79w1KfIKMmFZK#j3W0YY$4876C`{vlEPY?aCkZ#)@L;~yi#Z*TM~oVe%?k>I(fHVC3Z2>MsnCcww2ff$4geQpB>my ze%CCzc69?bw8e|PC2u3P(_4le#-6@Vid~+uk@t#iWRW<ASxEke{H7uZ!RdOWd(B7D`k_^fh#*A>JY zZs7Q>_{H@F#P7-bd&ovR8fvFMn*t z0?(#dU{i$!20JYfr2LEQzg}#CIm&+QvA|c9UyZl1top~1A0495YI*%1w5FZhIe$|3r^8F1K3{6?^}*2eb%YvM!459zdVG>GwIm^-u1W8=iK#?J+J z{sCNiS)Gkv1LnN3$i{EWvhlkTZT#K^)(`t^{P6~GYccpW#m2p0QGW}#mi?&&a4zk< zQ^EUHDgQa`eM>(7ePSzc_Vy_@k=$+*so6HMuh%9H%C(6@z~M(^*@U(6E?4k(QJqbk z4R*f(T>nRK_?7WCaUB@DmU6d)#n|*vco*Eh!0|7r^KFAo`~ZK5Z?wtnNc6SI-K%YqXEC{d5p$-p;2KFbdE^pv zsT?>+D_o=!PLgAj=fgM3#Z?+?@|s+@%Zg2+8BE>@CuxMY+}Fmtb@ObpZPq5AsjM`-`?3=2JzXrWHdvB_b+9DHz?azTzo$bV_tLa(-1=s!gkdN;vBA9Aim zj-xk({>$$lDlAN_Kb!#9-2pDVi{HY#7cz%$2F?rLJz#co90GoPL_XZuV`21=@H+VO zsT?~qmvn-vb<-Vl;@{&bXJm^L>=tpQuTcR)F!(-o--C>!zwfx=< z&1&~Ki~b*F)+VvGp%rZ^1C6T7q6cT8S+V^Mn$~aId5=l5Mf2({dMujN3FuYpICcv9 zR4mrO@3U7e`ulw52RAZL*sP@7iKLwMZ5o|{Hu)?ok_9io0NNt?K_ltFOm7(4Ho^7 z`X85~XVT{9Y0L|+wdhwfXr7s9pZxxg{8j$Lk45OCP3WXSbK+NB?eynps@v6}tG1%A z`pnsB)|_3?WOqexO-VqHO)@7HZFVoVX{3GP%-MI@oL|hDvp-tw0bS_4P3XN9Xuo;r zz?40NW7%lMzo|qo&O|p}KtparPcBAVMx)K+82!>Yb_pH25luP+ZQAQ<)hA7(VRP&h zv|aqQbJ`RdIPDg5p0m=>&g;!7q5X3U%=tZSmeR)g`R1S*Ic3!SLy9>Um6%iBY|h2} z&p)o1Q;}}YrG@5PR%y=VP3BZ~m~#dH^-9{livL?RZ_d>V=3GO%mQ>BTuXD_~o-#MI zn^RM7&Y!5ucz13}B`um$%dwjq%=vSXIq(VRmXJAhb>`ffL!!Og8qK+#-?!6NeYH7v zB#>IoxigdGHRmsRq@XzsY#Ti0{56Zj@w+H@R~2c>ob}lx&ar;ZoJM{((tac7xtlU~ zb1iqXe>c~0_kuYal1ZFzLj{Ta4J{6pr>tm>$)+Mq498EaeT z(76)abH}bB+r6gPweefm8oOr7=s)Pc=s)Sd=|Aef>Obqh>mKM{=$`1_=pN}_>7MD{ z=^pA{>YnP}>K^M}>z?c0>lx@-=$Yu*=o#r*>6z)-=^5%->Y3`<>KW@<>zV7>s}HCz zs86VGsE??xsL!bHs1KeG2lE#$Amd2RIn#P>Qp2ncYqQ<1grpBno zs>ZCwuEwy&vc|N=_BQG6Sl5_m?8^qkY=LZoY=dlsY=vxwY=>-!Y>8}&Y>RA+Y>jM= zY>#Y^Y>{k|Y|{&~uC0>I8eeg3m~5GBnrxeFoNS$Jo@}3NplsnYe%Ch2M#@&oW?0KIi%#`5^hC?FwDrwB=IFSIK9^eAhBQ zOup%EvzLaec0Q zuY9n4v3#<8vwXCCwS2aG_q9o`FPBfh7~d`*FJCX8FW-ME?FkDA6VNYX7(rM;m_gX# zpgI>z?33eS3tnhs0tlq_7!eYW?!e*z^=DJ!J zvkAK$LHTUXcQDr?Y$uE-tS8JT?6*Ufiv_`i!iF)7_!U@Dm{HhK7*bgBRWRjCU`t_4 zVNGF9VNb3-hDDzt|M3hLqYA4EvkJT3PPv;&!n9YDcR3jN;zAem3j3bHd4+|AiG_`i z;CObXi$%CypnsC(h@G`-ubo z3oiI9`;Rxc98p|RoKf6S9P*kfms9?s+2xq0)80DT5%&}a6&DpJ6*t`jjw-Gy&I)%G zhmCPraawWPH{iH0C%T;15BC)ZejYCT44n8exbXvUWO3y?$rpDPhZdJEhf|ANpV8)W zZED-F8BaP<@w4U-Li)lYa zC9W2fi=J}`bu+xKR+LP=ZKq-ylC-3lru3fA)tFvHYYL({4V1eYl(eWv(4_7qHX)7b zW;7{jR?@D{?{KxOb*ZklC5`JK%BRuhZfIZ9z_w_QX<{*L?7b4SGHGVg&gNVVZ6jLR zlW1x!XlonL*c8K%<|gg!A~d*?6*M`twPV}R=*X8=w{I2NT^AZ2`&**(t)LfvR_khf zucP%jXnuhNR|D+KK@;To189Tmr4^E{BVC4OcmWz>F?yji#iP&`52cQ@#?&da#||_| z;uQa5{`FEC+9W#X8)%d-R-;)Ghv-{E!$i+~yu{Ts@9IS3q^z{giWXPc=Du) z9*j1+FLhF=pU4*NR2r)96ZBSTs{ck?okU|DL~HFrUu|zggIz|8&1VfpBWo}ydszx= zFiKg2fi8ReG;1*8S%Z;fzJ1Hgw}!gg5ufjJz89a&j#nG)ccxWx}kYZiNp6ND-{~!FB-v-cV20mf^ z#M_BB@G|@3#4P%WO?1xKKpQcMX5trrA--`Fv5l+ZZQu`WHgFbsClb5(9rdz!-&A^u z4Xk0iU6J+w7_|Pca;^V^cI$tG7==@7{lpvkyUBZ+*v3N(){hR|U)Ny$*Rnq2l631o zm)OP0l+T;B{_JGy-@m~6_o%V{?c1zBE=2rd(Y)9~@0-LbULaQCXI({i8S#rs;upj! z8moz4)DXX5|6)=J>n%v57BOOYvR~@ry+3o1C(~ z0b&>%c|TMe@r%EOtnZFm>#Jd{#bv}U&a1P&q73UhhP4@om092ZtJb#%<+snU-k-Xx zm;c&3&-#hi^Q|}Rw_ab4^*%@blgT4TL7Wxd(y*1I2T zFj9zVY!`1mKNMIG{-@_%Vix~i<=fqve7n2Udj8R1J*~V)YQq%Yz94P^HtM;UV`mf7 zII)m#UzAwSFUzcF?{e$esls};WbMQ^#3nu}GS3^tC!$&A8A>zH^YOepYMysT1$lQ= z8}E*)Hcv$X?~Y0~&q=Jo_#Lr}gDcFlmUza_OXi7hw{En=Zu(pI8=cl2<+pFux}R&c zZsIB3_p-j?_6ok+y~2098?3v8_{NDXe7BpJMJ8MDefN&!#}TXes=_wCKVzF-B_=UJ ze8L-No1W#pP!IFYsKx}_bThxN>}2i5B5N;lSbI^;+6#~QMt$acf%O${Fh1sk=3A=R z{7vjO8`>q;hW47ap#v*y=rG2x)!0zsiVc<2*%1AH=&C9kx^bTOIx$Y~WxPI`!F!z= zY^a;Ok$fA9(8jBb*LU-5Xp#K?VjH$hvf&+yYdznbA zb`3UipKKfXWulE7j;%ZfTX=FM_Atvv{;@KK|a zvC-Z-;`W8a?~{q+Cc>L-1s}@+(Zh^J=7C4X?LUuLl91|>X zLaPN%OSize#2d=9EN}%|=H3MA$iIu@_tjXS6<_^yfd!uLut0yj1p=iOh|=!#ss-LA z|D#3=EKOP9`y?CNve3p7Yi(@TE*sm6_(2AFzs|C;BZ_S7Sg=ANxZ%uJ8#|xlJo~XK zaL1p5%$;Dn-UkLD|Is!u3HarC+VPh0J+A^Ai==^FR>3e+Ha1WF&nj%}+iV;A5!|zl z&w@!{q}?+uxOUcp2ZDzV&9>l?{LZhn;K?Z#Jk!g1)2_|}y+z5|$V zH}K=$NjAQJo{cl+#}99^ad^l03DY)y>b#A^H^$CYcT=*B zwX_{Spij40nvn||60rP{jJi4Ul=$a%jmvx)EF3R`s9 zWWtn9?zmu+yTK>+O18;gz(F$M9f!g}j>xb{V_QJpsT?~i&L&HjSOY$7lb3mzL)8rT zfMeW}4;SHh<2>A?6OKaO+TEWOb}2QFLedxeL0TIi^F3+2PN*0o!x zXvsonXIbpK)fahKPY)-nO1IGUh0IBVgWU%AYJj7mnT48H;d#mMJ&r#^osFb!I9fkx zgfyOMp=hdwUQV#kYn*R()vji@i_zsA}fw!F%xw;Zrl1!w0uLmtz-r;lP=2;Z6(Fzrxp1 z|Hc&ygWP6Bm{8E91 zr^_w;My-Y4Znp6I9Tr}Ie}Cp92H}V2lUDukesqYfJ1nwY3-cZ8nfF*>kv;M)vUj>g z(&H?Wfwu6=Hj5lmWs$?PEpp_FMe@8BIgb5xSr$2k<7YIYMW9EN^7}&a|CmVpp`Ce| z=ndEUn5S95e9b9~+*)LjI~nokk!9w5mRO{nyl1Jqv7Gl`bfKGY z{((GnlzQ}(AiBz`MWWdjd8y1I|EAvSl>g7HMc(0>=94YFLjJ&^og1<{S_(2w%akyg-? zIxT8t%&ARaZY?@eVX;L|MI$-`t?BF();5rLK5bt}y^HhFtk_nve--)Hrd#y-X5RHq zyZH5JogdvRZF8(4WYG;t7QL6Yo7*h<;Jig2Nww(TODy_$BU&2Q_6&Ld(JwxEb4DTpQHT=wqfcx^%i}p#-gv3qv=t8CY?FYiD-Ru7M-KL_c{Jy zF?t~9`INlRIq#RWxy*Iom!tT#=xPd@BAVkCXpLK+T zZFBsf&K#qSV;%I>1+-S`txGniFdZG1Iz_q6mCr|`%|okYpT6dt%Q3jLa~{`s!K^tK z`p|n@(0;4TxrB0;qN83mjYdq~m8EFMsc6Wwe{DV5auymhb!zy1BkkPeN0Tlw=g*wG zt`_Y&9u2$E)wJ*AKQ*xb*BbQjdGzukS3|#t_861Sy|j5>hB?jrzCUQrgNf#}6q@r8 z^&e*ckri_u%`}H+>O59wPFtHfkB7{8g7ZJQWX@Bx{WSmMnKW}cNY9e~kz&q2X{R%e zv~14C8FQZVoAZ2|IbAj8{EKTqPjohMtebOq>dfiMH>a1jdcEfKRhZ+Y4KK&~E6o{5 zB6XTGNSQ&}@|BYo%^9MtA=(&bJIu9=kPkO;{JEqKb4C+Lv^C1P1GE?5c)(-MSR9Em zWA!8-iMqiwQX#2|)JpP_Xe-Eh#wkBeTjN~EIPJueek+e-DibTKI;yKS)D~^3?btbV zE}fI|x`x=b(3Y+(c8#>9Yu2^vKj^>cKk2{eKkC2gKkL8i9_U`^p6K4_9_e1`p6TA{ z9_n7|p6cG}9_wD~p6lN08R%K)ndsT*8R=Q+nd#Z-8R}W;nd;f<8S7c=nLi$K`+)j_ z`h@z1`iT09`i%OH`jGmP`jq;X`k4Bf`rO?s!ytKs*m20;PzSdUG-u0W%X(G zZT0b*dbiK3?`sTPO`94M8XFoT8Y>zzm*lx)NMlK3N@Gi7Ok+)BPGe7FP-9VJQe#tN zRAW_Rwjjtyp}`(y)U3uO~!8)YM9D`hif zI~|V8mdd8ew#vrJ*2?C}_R0p!7Rx5fHp@oKR?BAdOk+0uLGonNW!q)r?+R^>d8D~M zK)ygeLB2sgLcT&iL%u^kdA-6 zm&vEex5>xJ*U9I}_sIv!7seOHe4~71%vZ{1%6H0#F5u7QQ{`LbW94h*b6>*u$_G!Z zx;|OHSw6Zq&SE}WzFR(AzFauFXklq# zYGLc6;w>bs{p%u&VQ*n@VR2z{VRK>h__7#gk74&EF#IQAd13mu!1luUFD<&5KLGX@ z2k3zdh!cn#h$B2)>2d~fhdbdA;u1CFiCc(ch--**oJHMJIbM+Fa*`u5T#k~-dBj=7 zUBqE_q+M~EExKYHC&qOa;XEI}eZ+ypg#$p*$F8BK!4)`HlP@GWQ zP#jTQaRkmN?kEoV%$&<9#Vy4#8`50PSp)Z^f5o_H8Jtwy^wfEms~+ufxvMzr{&_B^ z-EG$8xLa4oIB$&meg+5r04^*}EN(20JO)=DY;n1>IJCI*LvZSQ;noct6W12!7We)m zy!`wkmy@5I3P-PVx%#2-?gN`#4!?UQoSwRH`>`M4{om%o`K#dm?XCtOEr78W(*~pw zJYR=qK%C=IjyIz#NK3fQ?`jKGY^61vkG61TtE)jAixweG;@4TOMj@?YSB@vvqhT=L z@_XV8^uw68A&p}its{!&5kULsM+1=-B2A=q-qlE?mDIJOon*KginJ7IDyN~PoIw6j zNoX!;Dbiln7NW&)YzOL~&5VAJhVx~-tJ%DTb|VcZf|esqr?(!Bhj@gvo`(`Gru|3* zk`^RQ=#q9cBI=)kc64IU)s7Bhdk|?Y`q3KnqwU*bT9f~~YV%8b`XI;EqNdQYCcJ1= z^=MT^=I=;$wX6G@(X!C4q-j;7U0sI8RhHmtUMG>Cw}2K_=W1eW+g*)JTG`KXV%k|u zLwgS`Z3fLv+L|;rZwZ>4-__ov!QJh+np{nVtI_>|G9}bMxyse>4oA!TWir}cuB-7S zHlq0vYrrqZG{8Buz}L_OqiBQv&03)}!^aZQ5Xqy@#Wclhrd*A&w9VBVk1ujHNNJJM zBzH%fltzhGIrLxj&qcJ$chNAVWlGbOwkeIXqY=$B)73ujL<7B%ysOYe%i~;)^klY% zW_sANtD){S<7%qgbfB>&x>~C=*Ei5!UqFKeclvr0c&|+{YcN{O_qQ3|cSE_G3e0y^ zjrlHWH{aP)<|~XdUmkU`bIq4gWWLl&^D!=b@r^dP(rkl^#3ANL)2yWkvF~F&MOU2- zwpZF9Sa7hBSVmo{4PHy^;u42MTl zXVo_FZiWp^hio9kZ(p(vJl|yl#GeKpnCBZ9#4c`ToyOITd;^0xM@f?n6tX?K!3GW? ze?N}zPP}9Le7@n`Vg1Vq*8dSPi8qO5yvQ01@OHn)YyBN@*8fPp^>1jf{#!%VU%hJm z6*<=b`*Q0)rOEouPyAxu`ZE%&pBPL34#YXOC^YXfv5Jp~Q@mM6{DS1KCw{^9X<`-+ zDSlB-{DPRqHF?A@GKgOguUJRC;&(ywW;L02U)EvlMl9p!an|=g$ND~JJ;l2P);FDC zeG|kq`m3z3Gu8V3&ar#>T~8e2`Y!9cw8;8?zhHf*5YI5m9?IH_{nD&2rOo=bBNp*P ztMx7=S?_y|){DL9ovgIpeqs}y+1C5_X6tQQw%*%WcX3^t^#pTchI`WOZjg1GT-gy zy-^P*^4;zxzT2H_-B%K;C}VqCqjeuktRuVHx_>cm-D_Bnk>IgS*wsy+5|5Z=t;LJH zGirq2U4Gm2L?UZ1N;a>(AhvNyviZKvCjLh(V*3^w+MO|(zQA{87>BuxL)H)toxwOf zpK*Fwtqonz_`8*{dv}5jwba|tldCrLJmYbIaXL}NnkveJC59I2Z0PG*8~Q2NhPP$x z?@ByjZMqF-5|22%i*HU~7f#8v;d3f%xSaUJRoEMF+3=mIHheF2T1##CnHn43)L_Fy z?KT{u&cCrEZ&U7L(lY5MpN(wmv5{SgQKU87$U${B@*83hmTx14*u=9|ZRA4iUu7HL z1H%5@M*O02#YP_V^1TUc6^uFV)I_eB^JCuY@&hKL^}4>kDV=ZL|v?HBe-u!5rR^i0|TmjJ`{qPwR=>lmAl+ar_zL z`OOwc#pmvaKRnoHf!|hG;8^@`q1OUul~~|{WeZ$NjN#f;3)JE_@65B<`p1U~Ebv6g z0-d=Q=<)ErM|^Fl#R9LyS>QkA7MSPQ=V=!BFW4Y%+Qzm`v$0+9`>9}oUw|JD&bF}} z<}g^NjjfxtvEoD{-UYFU}Rn)0zv$0#fHg?yPjomlLJ1ZA#?5P#jevU;*C zTwb>E)hTn|-Bbf^CC-qV$T|t~4gwD!TDUnT0sdWAVH2lw>>Q6xTv!YqUa^U)2EHqo zW)rt`*~DMK#T)!K@j!u1JPJ;JinfSzO?WbFVh~I`7H1QYGMjjrV{fF}#2n}Qi1U1r zY7^g<+r$r@HW?4rho?^NQel&OFn4J!SU!XKO$lL2l&>qd(4FKrE?elnYzsY9ZK21yEc6t0 zIw`ve{^sQz!?Yjluuuf9H8lg@TY&db=Y7ucG3g7q**9#zhkM2?Sa|Ch3n%(4ymNr|?r(+zC&Gmr;KZEwpOtXrDGT?)U*Qm8KkZHwGfxn{Jhg1$ z*J$TI)SaWuht&VHz{2>V@Hbr+{(jCPaS0aLD#Ie%7BDBVoVkg$7TLXtcQLeDWMBAu zMkhSK3%<|x@OF#j!t1e>k>jh~d5=Z87CAG;BIhhw&&EXb~9}`5R>)th7jLJX!_kdMXR;!e^1^@|my6vAzO}46^m-StN*d z5l*wn3yl_ed6_w%B^G&;YkDWqBJY=3uX^&}oiOv*@~bi=I4Z z(bLd(&TO$L{w;bQ$1Y5;C^jHkLHm{HIaTam>$T_&l@`5;bNrd#x6$UE)c-5>?&h3L zezYmdJ~)k5Rg7je!@35xPdOHSHpil!9Tx3MVt#M6MSEw_y|U218qmQwe~|it1Hc)dACq*?6!Hb~M7jgWLDa(7f3nX*Xw- zYYWt&nRBl3B6B9FABr<4Tw+dydeIegUdS`&MasP7H)o3dSF+6ccbPfU&E~x7H3xg> zyv}vJkz~%B8Rq;a$DG*$bKau8w@b`OQXZ-3|Cf}fV=6;g)rr;ZVym`_NwlqV#LiX6mUHVGV%I`@y0+Lg z7P!|OyY^)Fzvw^dzv(~fzv@5hzv~|8Ug)0a-sm3bUg@6c-sv9dUh1Ce-s&FfUhAIg z-s>6YS?HPQ+2|SRS?QVS+36YTS?ZbU+3FeVS?ihW*{ctzFQ`wbZ>W!`uc*(c@2C%{ zFR4$dZ>f(>bh&*_eNTN*eNlZ|V@G31V@YF5V@qR9V@+dDV^3pHW3hp<8k-uU8mk(!8oL_98p|5f8rvG< z8tWSK8vC*VvIVjUvJJ8k7i73LL$*UU}bL*%;Xx*&Nv(*&x{>*`!>rYolbV zWU~&Tj%=80*@0QEZIg}LC(gBbvVF3FyHQ^@aVO3z8!1~UnASQ1lL!|XUTWThn<(>`ZW2r(}J$Alg~Sz z>y;0bFO*M|Z#;xHD}qTK@ray$1FY1``$&CKEOjM(YKubx}sxO&G2XEGJBNf3u76 zg!P2^ZmDrGU^VRs6ABv&BMK{)q*x3)7IwK<(!i9$mWOq?SW}o&*i#r(SX7u)*i;x5 ztSZbJ!>(U|VTEOdX@zZtafNk-d4+w2fxQ(jChi0q3nL3FKLlnLb{2*fmKLVIG1+1m zTUc9|yNvvE3tTKNOnw6A$fLa@I3~<4>@Ey1EH6wiY%h$zRa*@6$FTpGZ~$=u#!rkJ zU=L$lL7X7~cMyjVmk_5Aw-Cn=*AV9r_YemW7ZE49D&%sM3*jo~!dcFMyA-y#Tt=Km z+(sNHW5wk>;y$}nxLhcHDaMUr9O(;bsg2EWVAZs~HlT2+I~-NfO<<;3aMRS?U7;~kmcaz1fCaX@iFal%B-7vB-%iZRY8 z?kEl^F8L~)QrvPJjw!C$1Lxcb_Y?;e7rmcj;-=!LH&aKPwQ|Mfu;Q}fwBoknxJNoJ z=N0$e7Y@9K*X6|G##?m7xN?j$i#yN3pJ>$(e_s}Ai;8rbD%VP#9MHddHue&g8TY%?2O4Q)-WtEr)_Nn>+k7-(+% zj}h?6NT3N#Zr;`AoQK z|Fl^n{Fv?AAy+#Lkw4Uqrr6|ai>+vk_vX2pw81`7)a41lp%G(C5)Y+vCwjH*2J~qN84qR(cuQ>G^1=XOy6+ zhFonmi(}~vXs-EauWd06*7t2bYcT3rgArm4262kXLe^lg|2%WbpIl%KMymN5DN|Qr zzUl_^RdkrIlw(E2F!C48m$PiX%oX#kC7!V>DPh$HSC?&YY0(DfXKZjLXoHT2?|8S^ zpr_gfJBnO}Stv|qfp?Vgq|JgL_Z!Na|d+M$K zwl3?xj`bFo##?`Bs`a0mXZ^=;?67j;7p%S5lQKIr62E9BenI-Ek@yAag<9el#3Q;( zh+pIrzep#3k-+ya7R-A!@rrWFp3NGJb;K)jSIx`V^X^k`-d%}pY_np0-xHVktlawM zh)2BQwZ3uIRP?c?;vd8$9?h}7dw74;Z57s6y<~lt)L7rSan@JZV0}lk{dJx7?YnG! zyHP)3+Is)Tnv2gmt#^)C#4DB78w^@+U#j&!TV=hEuqI;zv5mSS>%E3JNBNBPp3VM= z>>pWcyGD|56T~l?Nq2^c?uy*2mViKRu@$M+%6ff1AXS9HKN3q7Debzh=w3?@( z#60+H&*j88&Z{=hsj2443z_GTD)Xc#nP*q(ZA0u~WzM?6vE8%re7Bp}#0cvtx`xIFR-0yX6+RDUocL;UtmpDA#1C=Hq0ERVfxSTq0=^ebcPMDuBvaZZV&MdT%vK$+^Jk3U~kF$}wSsPjJwUGy!ZRBz4c4pZ~FE(z} z&pY<&v5&bn^6rX_EOufyv9Ujk$BuRpnnVGG$oy^CaQdykC&Bz973L3Px2Mz1|8|Y}7aa3{ooxOeN^NwTHr_9Ye@VsX z?8iPaz0o7^A=sJGQ}H?HWZCFN#Wo6t8@-{yMsLNRtjDk1kAHa#KLh?A?dJE8$3`c~ zf4S90|3kSCs%-R&65fqSJYh?GUE+cTcEi`DH4(Qb4v~$|{2h5GbP(4kzVQ1L)=c16 zudKB|4gT`hkOkIPSm6Ey3;ey!0v*{F_*c*ZgZRndEbAbt^ZJ|x-YvDjC)2#|F`xH7 z;;*+!wXq~(2D>lX7%{-HOz^>B`0}F*Y^htDC02+_%!5@z z#3eewF0~dso_QIkf@jX=SecjiXVzQrngR>nlwiTz*>9|};Qi?qd=w1yG??f46btr| z?=QAsXu*R2kEQdEt8GgEcnC2egwS;x6GF%cA@mR$x`YriH-wN88bVA6p-Zlikt>7{ z;-2cls|`}Z*qWi@60o@1lWmf7gb4L15Fn6J-gqr($68lJV$cQ)DR z2TL~kIe71DFyQwjG#2+8*V8t(Q`E-x0H^K^COx2cy+vu-@{O~{c z;$BhsAn5>pXTbxH@K`99GAA@zr~ux0dZ~qqvn_N%0`ul(Ep!>judcVybp^Z&InhG3 z@YOqMw;{(utL82ANDJ>6fXA+xvrs$vFQ;1QbuV-FikQ1M1J|vv&?x01m2hCT|78C| zxaX$@aAeBOr&#Debr$+@+Cob?7REn>x0<#vai{Q(@bO*Y|9dW47>pL)w}SU5G+Q{! zZ{fq??#CoE7crYTiG>!1GlkE9mlrp{>nZc+*56_faQSQC?AMcjQw{SS;q7-&{vO(c zi-aGVvv9MQ_b)YCcuk3gpF?waamK=(@c%d1?@6*Sejq$pjTXVNAm?_<&?uJBDmc%F z>1Y?!pUr2!X28P#X8Rw?{Fq~5a7FmnREuoUXpyZKERtAkkt8&cT@o#_dznR+b)uWZ zp`YZVqcotW1TAtH`$wX+9D`UzBDMo=v2r&|=?REkl37o<%B1 zm1Sr;>{p}3)Z|&DHp3#=x5!Er~LuFZC{S>A4QiV9YWndk`Cv04*AD)TJ*S9i=x*=^U>i>L618%-J*p{7CnP< zXVJzv<>-RsT`2`~jm7pCaGPl}`mdLTI6D)cS=cy{-{Wde`j#cQ7)W4;HdD}@A z{aZ5{CFSeczh{xT-W3*o0Dbbo9E&z_?$wlivkTgUwO2K3k#blEnG`diUzNdej!<5;i~O}E&h z(Oh)i6!hLr7JZxc-=!>LKMHn;z8{BH>_a!MMMKU)OP)nnZboCyL2Kssm-Xn->FCj3 z^l7elej44Hy5G*DX|w%q0lm8%{d)#ooa2igG<3@UkMl3hTJ#sr@oS?waWm#@o@LG! z)#k+e%-M3$oUOCXNhmjGn-+7n4Vd%0Idis4G-rGAcPKC?sni_Cv9nX1IXgF)liXy^ zF0{2P3BKs0RGG6oW%i)#p0tg|;iS%+vy8U(YBpy%ZKq|DHktE#k2!l&{tp?XDRa_k zZ=W2}v^o3I{(fmBuQ?g{B=YvB?*6mp98gK3%>ye))XU^tnY49K3W<6L&6smA?Ht@l z;`br6e@HcnwhrOES)4zsjMPl>lW6PEBog_D($=9IJG6#WM`|F^PAuuS@^nmPR91DU zt2Sb7sZEaQ96DF*oYibI+-r$l6W69|j9qJrd+quU`Y-xV`fvJ=`mg%W`tQ02x)-`9 zx;MHveqx_7#Vx|h1Ay0^N=y4Skry7zhpdKP*ndNz7SdRBU7dUkq-dX{>odbWDT zde(a8diLrA>I>=<>Kp1K>MQCq>O1N~>Pz$~^{rSRQ(se``N`KB~T| zKKl(@^Ov}&*}5(`x*lp3mOv|8yX`TD;hKJwz*?SV@YF5V=G+jjx~)r zjXjM)jYW+~jZKYFjn$qickF5mYb&>p=F0ZU2Fn)jQRdp_-zFa?Unid@-zOg^Unrj_-^e|S`AYfB(h}E)%9qNg%D2kL z%Gb*0%J<3#%NNThAJE|X=)KvO&zA3&50@{OPv4sJ;^XD(V?JNLUl>4GU=B>vywEFnxGY$1%%1J)4c=xlc}$a5SMCJ{CfMtKOVBFu6R*hLsdSVove*yb8A zPDP8ku??`#`J6{sNSH|2=p@PuD+x0RJ7IfbSW1{m*h&~nSc`ES!(PH*U@>8`7&iM4 z7;PS`Cd?-6CJZMmCrl@7CyeK3UzktWPZ;oJuwZ+G#jv3;qOjsB$_YCPLkdgYlxQ(* zDU2zsxf0AN?8&o4hNT&xS`750r`;BUaf!o!{jv=^*IEc7N z=WpCZ97SBEiF|PvahO}-GU7DiHsUzqI^sOyKH@;)LPyuR+(;ZLz0>7P;!fgFa4B)B z7`GC~64&|+&L!?84i=eoIhhY`CXObq_5z%34ctu}PFzl$?r(5AalEQJm-C7Hoeu{* z6D}xDXbUb!6ju~y+`HN3km8ct!YMbOk8#Wx*Zdm3Den0J9P}Nys5q&(X&)R_T=iu* zD|R5pVZ~*|X~k`Cu5!7qIByyICFL#`KE>~H<0C06t}M>HY|iD-;?nVOY`ArdV~cBl z#{LIz@3-OL!3CF-_xfCp{sP?oDLDJ1aCdR|y8&-c3=7}j)4w=h7f!A!oSfJX3-QrBuQ(K=HO3oHHcTyBHGa;q)n_wqmWjCeT!)q z*Pw5dlXoF8iZjtR3eYr;FK{)F%nme=B3BEMCbBiU%Ks6k*i?vSGUaL~(oi;_rASlh zN0)g6-9=i<8Z;MaFAt!>+<`75O-9;GIlnJVbu}AlH^r1?nu ziD^Jz)L2XtdK+zMoOs20^rE$BOfM0ycpClaQT89`vY|WBqHaPvx~AG-0o^&kD_@!P~vK2 zH=>7KJ&$(A_FOcyLbNo-i2ulJG`41oX>GfqzhO)KnuQE*htd!SzAJGxMQMv~ zqcLJb2l}FDk8Nm>Xq3_-rAgk2Zg~?LWhLdMS)PL~c`C<`&u}$OX`9Q?ICu1+c`~2- z=Xo?x=4j8Ng-R23e$z;M*uIQrD(&o=pPOj?f8rQz_8%H={poGipOVWrFedp1MkU|C zShT(`YxxF7y!AP(sTfMIK2N>%y-5C(9Di`i`tBrt@z)N%;hk)KrPbDVM$r0BOtrov zh+*vSv%Y1C)|XUbeOnN__;JeCZA`RvALZM+iCSAX(qZd*$a`tg);&cGqltADca_*W z`q(;b!MalFoY6}BqK)_kWi#4|U$DP@3-JqL7MqA!{EL{x2Zh8hvWQL-D2r-Nv@;auh_sLT0T{UODb;K{OXL}j@=Mm31HQ#!3J=S|jiuL}U7{<;s*1HvJ zDHdC-XXAqPe8lg!nyhDN+Il=`*7HKS^|Z9{?d}=A-JQ(0yIF&AG4G8!o4k{P)^p^% z^&F63J*gRd`y%(ZZ(r~ZsM$*E{%5sy2WzdnkJQQUHC5K#RBqjOmRNUnzI9(ttfM5E zcSkMq?x+dg9YvgDc^&VLBL1;Ox_Rgy9&m?es>VFwEc5iU|7wSMo+fVbFtLq0IaXb2 zp39cZb6%@?{*-H;qbJOhS;=?1$=jiU?{+8J+V6-}%(AZHAH*ugiCK7g7t~9eZ0(cf zwze^7Yi}n$acwheFUUKKc*ltyti4#WwfKy+yOEzT&DsmrTYSxTE~a^Blrw4TSLP7^ zW4ztiZtL%4EUntacV-xK&y(LZVe5U2qafq$UGhF5rm>M&;E#;k%{pvgJH~wqG0Aks z{vmZXa5UrpWa0~FdTijLWE-f!PTUZ%fxo5L!2Ojr@MxC}v~9A1&K%x1$Cw|coHNI_ zbQt@y1vc<4Wqxk4!7Z^zNs~6X=YkFH7jJ`qOtQgau}cM+Hh4C6>(8VsN!1xPNZW(= zW9ynX+2GStHuy63t(!Ldbv76&vB7^*W`<*5lmBA_@7OEEKE|6Ld++}}b~iJVb!=1E z(R%D@26i>*+SkfBViUwI?jY|0kNF?N#$;lH+OvEigf8{VVZhSTwX2j}sfi4+^Q zMH@aXXv61ld?o&o`!sz0CX2=M@2RrkrVJZ?0-yO@gAKomzwF^R_i8wdZ+zEl!yn;e zza(#gvOg`d4zkupwu`foUD+;YAAUaa$0-}h&9jk{+HK^FWE&}|w2_MgHiGXMsjjk- z+x#|iZ=#Jf;lrP3vXN&eY@{>6M!KmpK-uvY8<_~Y-_-g%-bUurc}FGK;Fm(ydX}>0 zvjVJ9Nn8SqavrALm}QQ*1Rn6#RtwZqc2$lA9wUDZ80W<% z3v?A(U>*DTyg-=a@1|McLom*4g$4edXn`NvSkIcvJDetLbO-R(ZeXvpY8%Y}yB%6( zqeroS0{N$M{Omj%y%1bhp2U0;%2WrLf0DsG6n?Mb_hTtG3fCEZq1i@Xn_>+v2Hyi2?5lfl3Ij?&h< zOE&glvyFY8VPhNpHuhbCjr}yq+Gg-Lc3~VpF}@q?BbMje_`YE4gTdy9*V}mRtc~aA z+4yO#Hh$Iu^Nl!uF>4ztrfj?_)y8k4{_QO`em8Yj#oKsurj0*IThG?o_)FC3^4WMV z#|Ef7x@hALWhRqs{6o@bq_5b=Cy)Qg@&B8)U_2bBH|O}JUJG7DdHhZArZx-SPQCg`3xWfK4^y_Kmib;Bd%n(s zuPj;cO*oQw)`Ek@taoTIhFK z7TOV>xEp+K8Qkp;@IlQ{%gTn|!5ecqc0vuYhA=56%$smRM+jbBqKm6sok)Tkz6JzlA<1 zx6mg`7W$%vIeVEFTJXYk^DXqhsD-g};jMyj;XF7o+TKMWI<{-u~7ctesx8^W6k+OGF z_x>7qJ!P63ev3ggTKGBkJIH?(ZvG~1^yXUFM?K~@g-53>j7&-c|PKN zpS4){UzDBaIu?8u{(-tbbL5zEwErOOtS+<2V^wH3q&1X%mh=Le%*$NcYqa-fwnf0Fk#&pA zd7ZMzkjEmU4HgM;+~Jzg86uON=tq?n0WU{BnYPI1O&0lAjzu=kTI8F0-nX7{zd5$&DbYSDA4cYf5OrPItwp10^F z3+QvC3hH0Y_Aj%{WuCNXbpTC|c5Z1x!m|IPKX^GEdqAgBX^o0ua#yE7x7UpPYphI>sZ##?m+gyj2@_n7WYbTw#-c9J3 zlnay*OQ5|FWh1l+Zi!BC{GBT1lT-JfT<81Q7X6U@kGMYWZS=Ev^iIhyn3W}0(sg*nHyn{)hxIeA>y z3AA})zB&2ipM;Kma*H{qP`-e+&@P-)1LmAYDkQDox=!yh2VCzIQT9ygoK;}X+3DsK z$B}6BoHldLCI7rE(tZw8@-HDGwKL%4w^-n&dYJ-sW7!`7YyL(&wDZvq+rda<1|6Npr4< zBhls+g(S*dL0eaF?20ZD{|A5QToL>&>9_K9Ol4G7byPQlL|bYz*0#>UxpdChxhbb> z(KW@cjce4k>Y8=!vH!qv{U`l5{YU**{b&7m-2>eV-4op#-6P#A-80=g-9z0=-BaCL z-DBNr-E-Z0Jp(-pJrg|}JtI9UJu^K!JwrW9JyShfJ!3sUHL1RK=Lt{i^MPo)|M`K81Nn=W5i!r9L78`RKdm4ipiyD&}n;N4Us~WQ!yK{_T zjb)8#jctu_jdhKAjeXew*#g-F*#_AN*$UYV*$&wd*%H|l*%sLt*&5j#*&f*-*&^8_ z*(TX2*(%vA*{&yNU0WubCfg<(CtD|*C);=T0`JaBb8Vt*qim#XWp%){owA{_rLw8? zxtNWWt(DD{?UfCdEtXAQQRdp{Q#glgwrsa-xNNy>x@@~_yllN}{=Vg|50EdAPmphr zkC3mB&yeqs50Njyr^vU&e2jdJe2#pNe2{#Rd=mFI=A-1R#0+@yEHBX!~yS3a5>>kaKmd`U9Kq3DDEf@ zDK42;>~c$SOmWRLQVK~NR9rN!D8@}=993LZob^4pt2nH`M*vRK-7Vpow+QfJ(T1B<1S#+XZJd5V>1X@NDo<0|n(4b}r$IDSA6jY` znyR$bC&*j1=xVOgUay)*i_Nw5XV;s%27|aoRz7PmYHWQ9?IljK1|z|IKM&Q#t8yM|;1B3X+9)7-oK^vP9hxmcm#2jlYrn9W?tv2f$VQobZ@r;*d ztnaCO>uX}Y#a&C*cT=JDUB&T>=B@9{TLRgdaG>R%fu?4;&>A=in~~Uaif>`1*z0S{DL(aClI$doVZ0s1M!P$ z;uj^vFLH@rB=h}lViE5L%o`Yw`W*y zRipJ@!n%xeO04%3VjM>kyU47vUNB4Vj>*=$1u=>rSbOm=Vi6x?Sx>aedIs99r;8ZI zbBTPro4CZil)1Uldamx`+ZV(w&Y0!f7faTY&GCIxttTbZdbTAVv6N-q-;icV@1$FI zfcQjDymi00VBJqlTlWKg-W}D-yQ7F{TwKh%qtdK9pJPXmpV4UDdlXrBB5NxCFKV7| zi9dYGyP=3@djiBKdJ4?*;=FmVhn@#A%u^dQ&tJ;;Za2rztTWGv34FJ^neTR|m}fV( z+a%lCpNUy~y=ZGcsA=LxI18LZ%VPX6&0+#@Y>qb;#qr9 zz}kyO)?P$ed%-@>ZT%yD;(yh){*?^YPB4~+Yx&L$aS5K|`Y+1({tRPksmTTs(rjSo zP8(RBV*>{eS3H6(BVb@fmJO5;yC`QoU(2|?h4Fh2NkntQY zv4MZI@y&@D8eE8_XY}9@p-lNi+Y( z7W4nW{;v(#?L`}crwwsWhxX65p~ISN=-7E1D!|^Kh2JRU_Z8${S8GGJ;y3Pzv!SLO z8v@G@wYS;OYt-{D+0aOu4LNzN@hh>RPit)GYuf&S`oDJC@HQSB-kDeezG8Skwuh2; z3_j~5{Ms2cHhg}W4PT1Sx(2^@Q=ARgMfuJ|yA3zv>(=Dha0hvB1Z=p!)`myf-!N~( zQ*AcBvuz{+zq?a~jqJI|y2nNvIV6d7kofHrQf=fk<{X?uY~s%i zy!Vmg*WrtAiP}g#^;Wgm$YTpO(uSV~&yBn}XCprHNAc4e@ayj_*~q6kHnI_){(YN` zEJZD_IB)*8;;07Ko5PnPP$IDGPw%1K$)|;J=9$_|<2l36(aAT^QY+ z-+P0z4p_9&!5hcacOBk4980eij6+>+@y8=cLy(fJx1-PFmN+-V!brp4Yjx=V?TEvvP$ zeOhenpe`FjHyAsXc{3+bX2quv1Oa@Gxl zIiIJ!&UqX2_-xGA0FEsH&yx2pnDxU(a4z$3Hqyp-oabjS@n$nNzD=Et?+BJo0Vk(1 zpJ+c~6^E4C_z`pFzPE`oE6QxVnEX=eT*};_t5aocP^7^^ zZ&T(y+Miyq&}Uu?%~f0I8~EV&a~Ark)k43*r{gzSc-vMBC&6ELfeY_h1-FG4@7oO5 zO}Fr&9dKVbZZ2CitnexD;uQ%NKC2e4JZ<5N@-2Kxn}x4fVxC|z^99?PH%Obe z7LDVMWQ(99MDC;A2RYyBLUfcS^pr`9Je_Ef=khJ`VvR*!>9EM_6TDYrfjOtt@nu>B zzZwb9PO#D<(Q34tIy9VG;tr%~_Gf5!w%8(bl!Nm`7HIGLd5iqlZ;}7CSmf6Vi*AvF zUW8V(t>4v;cFILhLbKYv&7%D0Xc`(+daFeV@fLk?6YtTVWqxz? zxA+2W_Ehn{m;&ZQQ+9x3LtW^C0nY8 zd4J?sHrnbDUbNkIbl!UO-V(ImEOg*_bBy{Y($-03=*66~fbyrdpeJ)JD`?{kuI)^I zpG8~6P3D|Sne%9iKI&XR-BRjZG>yK^wrmOwoU)ht(8<%$&MAMzf;m?fnR6BG!0DW8 zsP~svbHL2bbs6SdUt!J-ZRS+d){P0~+>~oh4Q<_AXU;9{=G^Kxrh`dR$ZJq zcaZK>S~BOZMRV%8=DR1%xreg%wwZ%XckU}U=l*PS9!Mfho731~&Z9>6y zizh<*4Wk<*I3t>*Vx~VbIKOTCdf9(Mqn#s zGh((wHbk~WHbu5YHb%BaHb=HcHb}NeHc7TgHcGZiHcPfkHcYlmHZ6p0lZ}(Dlg*Ru zlMR$D>>+P$lWQYoD`hifJ7q&0%l69$$QQ^b$T!GG$XCc`$alzx$d|~c$hXMH$k)i{$oI$xEu($; zq+J#)Dj&6dt?RSoyYON1Wig*7-zFa?Unid@-zOh9gD;d%ly8)el&_S}l#l`e+Oj z2pb3^oRVe{VTM?Iqu9j~S&1&T5XKPJ;6KN($Ie{K4xCSzMA!t3BCHa_EZ>1$gkk2u zGQu>%Ho`ct^fQ^KaIul*YB4oiIEpoAbTav}F|7tjZxWL`TE;kTIxQ_DT4B`%@92b{3gEHb4;uzu@ z;vC{0;vnK8yLGwTWE=K3?}%}h7~o3&bflEIHc9<{WN@E96!ST zFr0s#*VOa1od^UW+3fA8iKTh^EzE^;Uw~M(HeOEfdi`1AWG39h;K-f zK%0<85z{I*qFH>3b|DR8L%`KEd}tfeI9@{QXe&hf*kmKC&_L=bCr#wKN;DG6ODj1i z8SR8}C#1Pr${{H>vQLw%vFub4(_Eyz4F7;0^EFzGG?^*1n+*##JWA}Mzs}We_;15& z@?1^le)ONZ30Ld6hM2~sXg}wdqXkh`n$R({btq-_Ln~V5bG4&wh%@}kyw@Mll)i36 zW2!}KDzhP{z}24C#i2z_x|-DAt6h!i9`e8!G3`nkmb5HsTGFq`h5@26u6stI0{5 z%bRkwx&zVdq}@rwla?2k8q@Y-8s8^qebW3wXnzAyR|}LT2>u&fO_Ekv%l>tw%Q=1_ zT4GVVt1TXl#&}4+t2w4@LW2y%v`A@^F>P`Njgo7RX_kX%mut~5rDaOfe3+QVJ!qV_ zpmknbhxW;Q>k>5371^#PdPI(^kuGN|%`_oBrlH2P)GyFf-;cT)>o8iYG}l+qUY|yT zeFQDGfw)F(0c$YoZGE|q?_i+6uHZQKYW?ALwtoLMTfYo#cE?#;za?udeoivqH>u|P zj5Qel%rRe>G>~h)H`%^G-rqCK_aMjbjORPv^X9v9(tHLpcM{}!{>_g~@?8$Eo(JH`6mW<5n<%KE&S*7pkW zi8ZXzSe|$lQ^_^8}eJ3?rUk-7P1Ib&SY<)Ws!`PCT!y>VVulb(E z$E>G#o7hCaPy8ZE{DSn<1n~>DcM;>bQSl4n7NyO^FY1V2l-s()bBSN1@%`>aTelr$ zM-vX#4Ubc9mQOx^?pE{!b!K@!Dj1ylXpY4S6c65Q`UQ5j`iN!V!hYQ zTW=Zh48~M%KEIF5wch<}t#?l!-|k+r-Z$M zc({>oUvTWNUcP-1wVv}Sa~koBW9O{r5Yq4Yy))}Gww$zX;yvA8by@d^ZPxA7S@$6C zgL)&+x}Qts-BGNwxF=}cHM}e8sxsakMZ9ALv5Vs=e<85_KS2|`_FpTUJ$=nmu+ibCN}XTzgK0j_M(cl7hS)tz2Ml1#4)nz`@vk+PAm`y z?6CD;7u)*(CfLB{jIr%m@38xv4eV2I1BVh@$R!UyKX5iN#+8iKt0ryWMz(iEZD1Ao zEgWx8v4PhcSYx$d17jR}o4k+WZD0-?@O_sJ{IbaV_cV(-^0%3TRPZ| z6>WT5hq%L~*p6$7lireJgLjwMU=zolth2!vnr!fm78~?opT@B{?@<0D(p<9*eouS9 zRGWXBGV>=BgIJz!{sXX$*)!%p-edk#>&<@-HnA+#{8wQs|0+9KioIn2(IED-7CTCv z?kIMZb|OjG*-q?jK6V$o`9lSEc)^CY#m0lPhL#gg*ng7^WfPx1Zklfz<=YVRfQJ5@ zVnY>_xqixqZp*Tvd+TlJ;Yk~6P366Ul{VDX!M7&nY-lvYhTbZ*q4%-(p9gH{-`M`e zWElF=@gTmc1wZ#J zzN<6dhP!8Mc#v`-{MWmM#PO3^Gcjqy3zYvE|H?e8k?p!{WVbRK`F(x$3||-vynQVjlio%9wGm!G#h!L!A4%+WFzZJZDg2ZQT*^f zs%_+BeEJ-}|3lk9gAKOWWP$CnEU;^Z1(t&uGNKkZbjbq8q+1}r$O0?C3g;4sSP7P> z@LAxx2@BjZ!`e?$BV2=uY6LJ?3om5A8OZ3H~`8ymhSCMo+G=Q84Q$xMj2q%v3SUy4G47y@k9x+idg! zaML3kYo&fW`>%qTx*KhDAkId|Ds6N_)JEUSV%|xUjsA=F7r2HYmq#&#^X zvE4gt3|ldF0Q-L|Vr^~>?;9oWRIue)V95*PZS2xC8@oE&#%{>Bv0Eu~SE-FXP-bI~ zke=lCbA>kcO0JEqrA$9%N8)TOJZ)p|_-yP0u;gcz#3VTW9r-_jn>TABCQ%5!O#tV5 zZ9J{g#xuaxhXic=NPZtr{>dFSemZ#j9Om>~1Wqnru<^<&8;Adm-wKw#lYQdo(7H0oQ8vZo@l|ZIxV_A!+Fvy)CXtt zXIW@0+d@&&I~f+5qTa`}{rQrGzM8Ypx19fnP7D37(ZZXRS$OL#3vUPi+$n0|lm-j$ z1^-QlukP=+@FCR}J{*pE^n``;_0 zyi;%C_c|>+J!#=j7g-09%-qB*-ocO$pD%{jm%;BVev3h{-2r_e8C_v_^n<<768?~G zkqoqf%vp;Ziq>&Ni^bjnk;k$8C5!wiXpz(FEpj&H&ttz7t>I$Yyc})eDt=#!)^NiD zT15l%GE>nm&>HTc?)^TCJd|&dM*94fN zAio1i=qI!nn-*Ju~i!Aapb$_X`=;mlZ zTQ#8JkhZT!(_w#C^7p8)=w9ecd!zB}o6MZoC5s+3Wl=PR=;3HSM-`wKk)Jn-e$GwdmQ@J&)fPbXoMz#TLDI-lCV$?v<3UY(t~UM62>x^rmc!-a?t%(WUNa zWj-%;8mgJ!JB{9zYthx@J-UflLq2*~3%b~hMW4$scP&GQMLXH=Nk93T62~q zFmJmFZ8FJX@6Xwf{rzjqIdH+8gNxBKDSuch^TH{6BpT(>w3mzSd0f;Sqt1!6by5pD zDaTGl$1Kb>=k#)OiW<>XyUFBp9=3Gsg zO3MC)I#txYz6tG@wr-@&nnZMA&H>hRZsVMPOGZDY+@1Ak%d~U%j5+t#p*?eb_Xp6V zOVOq`p;eRjFm+cqqiJ)FN2&jK68bm!(!{a%PHPmso$GnJ(VRBgc{azK=NimupEBo# zG;>}oGpD1&oR_A}d4;;2`R2S@ZO&`7^}5fTF7n@4G-oZxusu#U*U*z~PH(O`UfNp6 zwvYY(baQ-Mi;uGFInRLKoWWLe{59qb6__)eOd@}Ta|J5Q8KwLvZI3mXGtN23=gbN6 zJ4BnINpr%Lq$P9k#|{|4iME;JP{*Oo4eW0q|E+jZ6=}+x3F=L>kQU5&oAbThOrm{k zqw@}B-svQ9j(5{YMWk9%J1Iyae=?55HBR#XCUZ$#(`3PKNxzk+V=6;g)rr+r8){2! z(zebKJD1Lx;a)@RT4+nx7Q05SRo5K5_IUTd=s)Sd=|Aef>Obqh>mKM{=$`1_=pN}_ z>7MD{=^pA{>YnP}>K^M}>z?c0>lx@-=$Yu*=o#r*>6z)-=^5%->Y3`<>KW@<>zV7> zs}HCzs86VGsE??xsL!bHs1KC# zt52(MtBCXiR8qXpCsAXv}ErXbfpAX-sKsX^d&CY0PQtT}IMa)R@%R z)EL!R)tJ@T)fhgDx*F3O+Zy8<>l*VK`?3MD1+odU4YCok6^GZlwnH{Vw&cJC*S74J z;MyA59N8Y(AlV|>B-tj}DA_96EZHvEFxfKMG;Et}T+G(V=E?TS2Fe!7CVqu&l#P_F zl+Bdwlns?F{YQ>#TV-QqYh`m~du4+MvBk29Xyz@v`-@ z`Ku`}AMgP63Mq4K5jsq(E~;bXtR*M5r6 zmG6BYA1q%ipDf=jA01#{KD!_PD<3Xj{#uFa+vVfs>*e!Xa$O7{EFerEY#@vvtRT!F z>>vywEFnxGY$1#xtRc*CI@m)PB%l2}>K;?!ViaK&VHRN*VVJ!*CQOseHSADsk!>lr zWmOFO2m{5ikT8+3kuZ|5k}#97lQ5L9R1i!hY$c541#1a&343)kxmZk?OxWy^bQh~N zP)FEJ7*1GDm`>PE7*ANQwA#gf!hphp!i2(x!id6(!i>U>!jQs}Db(93>SD~TvrL#% z*fWMfzXgj5lM0&(qYA4EvkJQk!}`IpeVHz{6~-0TeFn@c>?;f`EPNlBSlGCB!Ntl| z;(22fGWy$FRIGy|BG7zOep3 z!2EB4{lx)>-~!?V;s)Xf;tJvnPrw~kSGZiF9!_!FtjjUPHN-i@J;XtZi(O74ZX%A7 zLp$Ou;x2zkak-2*jkwKL%`vVM<2>R%;y|Clg+7E6O~Q?$aHLVVk~ovN)9Y|3aVc>s z#%+vaiED{--3s>-2dk`dIhnYbIGVWH$&?p&JCge1av8KIZYPenBl+Tdo54SS3c6fS zoKW0Q9Pu5vqBx_tV?P{HTvD9!Ik+YEKE^f0ImJD1g@cNVUQL-xDSIIt^{jc9vz`EV z6^9j}U0`8{ZVr%adQ$F;)w(F?voPxulI;G-fm z0rUj?abUR9)e5?zu6D2n4dKywGzHRK{Jw>B9d#?v99DL@8pMhkSCcp<%L0FlceRS; z?XGsQ9eG=zcl_Lmwo!q`k&4z4bv2JDzomgl3y~)B0`Z8a&`9`?Bli=-kai*sMOun9 zl?%{N7y~h_<=A<&mm)NnE*trMqK)jvJkaeZ7axaKQ{-wk_>!20^DbJBG#zO>-Do_W z#4w)CLi-{A!Ai8CdRG&w>Toq8X+>uduP9)9To+mr`3ImWq5lu>TJ360o9D)~r=fqN zZ%K=Kzs1$2M$xbO(51T2wO&BGYE47SLf5*t!PT~|=XXV&t9hMGnLibxh0UOeRk_;O za&)ldrI=>s|8)}@TBXIbw3!05wJtQaWQ%ES(%jJNV;bC}{C)sUPTHI_x~tIdq}iQ= zb|($*c#p+2y#wN1jW3zD(0Zi(#WcV<^udqN1f>m*qY?VpN;Bkn#5BYvv_xr&x8$KQ z68E@t3C)o>#tO=vz_BCIA~Vq>)5zNeO>x`&m}VK%F6YoNrDeX2ra6YTDUI`Ww9a<4 zQE8v6@?0(SMvh&DHoCGHtu*LrrnzXJhf+>j>h5T&+f!z9)=d1D*n~9KPr6(UHi#DM zBTmspJmUE{)?l!|s>;^i(QfPi8npFSZnE`%PO`)cT_2b-!!MoY`>w-$Uy?p(HQ$D2^9|SV9q+2&zT-_CV>L03yBBzeRMdReQ0C%t-XTSP z!IJrMc?*74o%zz)-)-J}zhmvhuf!?7BTn%pzdvC8ghMQ2h_w~ntgYxEj?v26iiZMx z1EbLTZ;V?1RctR}|IA$L&*ykfs`Vd8{9><77W;zaVDO)JFV*_{NPD#4ieoUu4+2)0g;u_axu%=GcB!#4mDe zUE-p7fAyI6yK3`(L0sa!S@VXQ%)6d78L#vE*#`4Inr_~ES(kB3nRzRTVXUk(?^%iF z1uuDzOf~QRtj9=AHt+V-+brICzpu95FL*c9RH5~TiDj%$w%*stdnRhV&1u$q4{I!H zSYvT@qV--xnKK&scK3w!W+(9NZekcI9N(7rKrKyJ&o>L!^J$9pyqj%3qr@$G%lY<2 z4d1>XcCo72dj8gGJ=c*gZLywnNT-m0RD<;#SZh7YDy(ORBJ0_Vc*OUtx0v0;yQ8Ak z9VAZCSIN7ha(Q%J_(y3h4lcL8w==DTzsNL*tXWp|){oX0%h z6`JSsS@Zm(#ysQ1D!jxhUQXq^-EDlgn|Q|U#47N!o{JmIb9S70@+-`9IPZ?yFULG7 z9NT8e*8Z==*3Prm;v?2yyv3Rde=}<@h+90zw=J5>SbNdJcP}Pcdl66E&rkfX)&`QZ zSUWLe0|z$pof&?gz?eI|#s)4(wt>qSkJm9CYo~0WfpPfAtPMO}ZUZl~?WwSVA@ZF9 z8<_I4wkm@)RV*U)`3Ww#G z|G0GX|0%)zXV04d&)B{zJIr5QYyQ6#n*V<6UNiYmV=G^-G=Fy{@7`lS+=~6g?tQX| zJ#ELX<`bL1PH#)BVHa%la%}Md`PktJ8#=xiyNsPKF2PPu+R&8+Hgto}hHl5UH#FJM zBXKtLRJjek=(nLaQ*6kG?HjoBngE4aXPT@b(QhoPs~ud(MVg z+da&EA101Ed>X#z9FDDQwqgFm@D2Ey+weK}QubkD=1+Fo@bl!q&hd5lsF5lgc4+Ut zY~GcaWWyV0ZFm!9f5C4il(1$Zop}Bnas77IOyEzykX=S>TXq3mlbZffLFta2m&or!4SiaKIJW7N{z-z|Hj*xD#CQK+pm_ zo4`|xV2dQ;63Ji<>J4XrJxG)3V3AZX3CF&T1EYXzgps!KfL)qxbk8aqP0zQ{gHpgY z#2@g@qo>r^Xc4i5l1(;xal4IP1>U&pJPyPQzZ7d!M`sdz+xS~d zVv(3chmBvxu}XedH`{n^mW|&PwDAYP$*aM~f3LCeXTjPp(Z(BWy>&J|Nd7qI*)V0} z{{%08)Mn#faBRNB#=rM4-w2+tIe31XHs&GCSuiEjg3Bu`xNp0~-r00G{2({Wf+v<* z@YFgBp4no-5|0H}!W}LL>l0HBRxetxHqL@~E?MwCem9XGheNEHWDU6Af}I`Ap=w|* z73~h?!$ndoxM2=%LOav7aFsj@ZlwMKW&c}c!C&A<@sk$%U5$lyB2JJJu+VaN&pw+h zlnEC*EY(6s!HM!REOc_Gg;uaVoBa#OFXQ;-@UCm%Z8uQw7LL``S?HcA3$04G&?E4r zC*WCa2^M-G-$I=ndy{?dJaf{hKU%;XwMyoywJ>L`!$Kbg;d|5YK8}C8XrUkDEVKky z+#IgCHT-hB1q<&yXW`x9u{^&p{U>~2orMoAWlco3g^!K1@CoqOf>sNkUT)#y3=5x6 z`IRjezO2~7XguMnE(?Q;!nZD3_>Ly#>SbBDF<{}<9DjV8I7Fp|pPh#TSHXp;yS5B& zJYnI1JPV_hg+oae-awgmgBE^2)xsZB4y+pfD$&B<(8l-87G9j>{RwdUEpjcAfL^d& zi$!*V)9(g%>Qi~J2;puW)}_tDlW_E&dWst^*COwwTjWF9$DT)K%Plg;v45vpI zISKuw1RbRvJ!QtCze}^|4(KgAqpj@bvFM)Ur6pK29W5!Nz@nLD7R{=*Xm*`NbDAug z+l+=o%5Oo_X++y0zlh_-lq*5w0S86PsDJ6KMX&H$^lIu{TZ&$Uc68$$`cWr35@qj9 zMOTV4rxuNeeim&CShTs)qAk>YvK37#)1uG%(5MR0s)81MwTO7bq(wdX7VRa!KZAL_ zT*uI&MMr5bNM1CBSVIMRn8%|3jAMQE&+(a4 zl55Tdw0RNxE6KlvbCsh#UcO+lbq7~>p>-yqdD7nXooJx6e^UXvXao9aka_3mm)Kb6 z&I&YCuInB@bJpjW!%qGx&h=0>+AG)fNC|o@>G5(jTGErW_Y~>r0&||BJ~+vFJ^_uF z>+GP7ms`+)tI>mV(T2%?V*;(X5#6{54VkvRK6K?uH0EUVW_}OSrhgGVntFjubZYXX zTL+_P+SG}#bqdkH$)CtVD{n>{r=F9fW2gn!5C&)L*N61&mXUKQRhsc-6r^vU+$H><-SGvAOK1jYuK1sewK1#ky zK1;sq+EUk-$*0M;$;XvuxIRz5Pd>1)&SE|>zr|ucQod3?Q@&F^RK8R`HJ$Uz$I92r z=gRlW2g?`BC*zytqhr2WK3l%~-}rF(a`|-mcKLYudii|$eqn&od>0c48weu^E4&6~ z5OxrT5S9?8c)Zlb7{VID9Ks&LAi^RyX1Um;lJ>8lyfBNfi!h9^j4;iqv?Yv_SK?wG zVV^_U7ZwsG+MDYVMiN#MW)gN1hT0-GhN)uMN*GI6OPK3(u$M5{`(QC)vJGG}VKiYi zVK!kmVYpZ4Tuk@uyo>RK^@RC^{qD`O7#6%0Oekz9j3}&l8OMbkg&~VGTufQeYB7v? zY@Ed~=b@!82HmI4VwhCeR2WrQb?YS)b`^$=VOe3?ufVp#xWc-^yu!Z1z@r5&CiZgt zO|OfUg_+x^`vm*K(!$jDOj!(L3u_B=Ujz0Q1{W3=CO^B~Vi;XmU6@_i{V?){<@W*8 zFE4d5zOer9YGT-593aL87T^RM;Rc`QS&S=)Gl)9`;SfV`32_Q>3vmo_4RH=}k0v;X zxQIAOE%n7w#8obbv#f->h{K4>oLc8{8*vE2Q*cXhOmR(dPH|6hP;t>V%7~jb z!BHFFs@T{VcfEGr<+9?m;HaR zo46v^)heV}oYv-Q7)Mc0nufHEWd*L*u`S0oLk~d%8TqCjO(YF%#OrD#(n}_a9H?V&t`O=i6EuDtOWW{JsIcQJG zt_GDt`RyCgrqHT>M*I49*43;&EOj-kv7oDIy_t%}g|77!TGu0JUJY%o26lrVO{@rQ zY|7Qh{xpqdM&0ZQS3_HlmL^RNtR{`k{{y<4G`E>}G`K=niyNGCwYit8U9FC16VvXb z;YrKmKg6`Xvlm>gPnzFhXn**Xm=-8aaO<*|Mi|oyr5Q>)oXByt#J)td#d%j_Y(Z;W zRqtw#H=#jZjpis#@|-GHqdbmshY`!jK%10?xg+JbBG&Np0vczNt94HK&^`;%K&M?T z^kw1^Pos%Gf;QSv=4z(bp`A)Yy#Ous^g6Uv${vZvdLTOKa>`44MT1?xM4aM3iM-dQ zfHfHPdPB0C5Als#qPG4oGq(N`Vi@NU+c=GS#}msq zjI>{}t=}`r*6%?6=8NY0agOhJ6Px(hYrc1gPmI-=udm2_uch-2DULr%jN-l~-XT?D zzUxxVSI!!Y67~!8&38P<(QSSE6_{_2P3GIa#eAEyw&Dlk5?__^4U9$WpI~i8Akq4J z8~FxCvh}ZFyE@+b?-)LR z`uS>$xV^dRAsx&zY1zG2VKzS#zF&<5?ic2)yQR&#?=Q9P+C=NV#%tXx`F$oajT0!B&H9XeW~_U+ zYU|#HSjEr8D&~n%eALc&yEDz>?=TOx!t)&KEFMiX&pox~xoO@!R~DM*0%9G7Ddsu0 zinSL(^Q2L4$7$G5lCl}h_nT+|;MH{Rru)!NzZ1DCezBz$?c(lX@+t`04YJ}D=DbD>-zQ3ct@HPJ0pgS1g+U zdTiZoQS&zthkc~k{A()B|5Bm(J(L@mH-E@${z+`!Cwb=oYKeF6`LLVB6t=~NCP#@) z)M8iDv9H+OS(v2waGTLzRrfm z=WOU5Vi?mMHuSGV8~RU~4gIf+_X)=FUco}XHG!{5pS0ma@DE3^e`2i-ukhIab9Kh? zkxlpeU!)o=vKj8X5#Ci>0ZCrlcBdSrwHMni2#-klmeZ zCY#x0l1wHuvq5MOq?)p7tEq;jweVGv24~wf_z`V_M~1e= zw=G`AcP8+c`)$?G!3hmvYln`*7oJ?Ap^63K`XS=`l)b8uc>lD9e%Yd-J4-dxGNYk~ z@wJ`!#OK2r>LX4Ooz+lk<(KbrqRxjI8k*<#^LgI;Sj+n#@!Najv-iixkWp-=5Lvap2Jt!LO&U0Jl=+hgr!0p0}% z-%H&Gz{Za>Y4oW)aB)PVy;T~G%xN^n@rlJ6{nKiVzC-)(wQBSuem?>8ZUwVvfa`Yx z%YUs{vAx0R-&j!W;0nbG!Qw~HDRu(b|D-0xPLC>fPNrhZ+5d69VpqTeu9;A*p7M?P ziv7A&vAe1iyKjYJZOw{3+M?J~ZHhhLp;)L(u>s1Dv?`V)f3iujH|rF8w~{$jCB!3g znN!8NK2N|&+TkX(#3PE}D&{QkAAHK#HbU(AYUS8oQuJV;3*e zSS@*1!-=lnqOpcLjorfU?X-0_^?p~Wv9_?r9?jPnoMG&_%^F)=@FHQV!k6~?#|Nqy>l9GoznP2oad3% z8h?WGcP-U;H*Kv6YkYkpb1C8bL!2|#sd4I?T-#}Py_G3m_RqlK|iTMN9iW^ zFsFpjPEM>=;uLh1(Poov6=fu;t|NFKac$)$Np9$u#8kyXq^ zu0xMQTl`L=lJL-^(D#;+;D^amId(c4TSY1H1=>0{1FerXE{G_3A#Gkn+co4{7reYj z$vVnk&Hgo$#1&}kr%TZkxyFVD=3wV4`O6TxBj>%1z1?ebY*| za{K|xJw*9GG@)-&rlT0mvjy#w?WYTwPhKC)Eq{JN$sUfaU4eGmuH^azTI#%#13AoL zFHv%+3eA=FM;nwJYetWyjbxjWn@BIQ{R-*TMs!^2zrGx8cNscwK6>v~bl+(-;7;`5 zdL`c@e{&{UaSYwKNy)#Lq9t>lIgWo^uH?Tc|4ADHqMI`u-JTmKK;d=Yy2 zw)}2~#-6biO}sA9$#+2S-mzV&#fy~M87+L5kWyKhN-e2SYPVLUzBa8CTraf;`uLvB zO65e9+G|d!eR7n_rS87ue;uuUKibc0Q|cStO8s9*sr|!B3dpP=i;Zo9OrH<%S>PX6cE1N_+MO^Q<+5R@|9aTZv zqSVp+m*T}F+J@7oj-k%6`J@h|zLQ02BvG!Uip2FCmqX%OkBcgGJln_Dkhr$v=af33 zkiO=VLD8vbCf#lG~syx0!C+ z=ippEXZqYV{QA0RBYkab`&xa?zV`Hg$O`_G|C|4#|EvGA|GV#j?}hJ)?~U(~@0IVF z@15_V@1^gl@2&5#@3rr_@4cUapM{@^pN*f9pOv4PpPiqfpQWFvpRJ#AvYcN?%RVXVWnk_hI*C z_i6WS_i^`i_j&hyj{%Pbj|qxmk2#M$k3o+`k4cYB zkI~VFV9a{#dJKCkdrW(5dyIRmd(3<6KNk*cfo+0qgKdOug>A+kngSd0Kq#;&wk@_X zwl%gnwmr5%wner{woSHCwpBly3T&5cm~EMDnr)kHoNb+Lo^79P-~|z-ZK7?XZKQ3b zZKiFfZK!RjZK`dnZLDprZLV#vZLn>zZSny#fsMAUw#~NfwhiB{E3oOd?Y8l@_1Jvd z{mE$M+ipq_Kc8fw6%xg0aFZ zn8Db=7{XY>n8Mg12F4iLsx;;>_Amx97BMCn|uW4bARUjgGKqXFhK_A>@t2NpCY>?%|mBN{6jGa5S@LmEpOQyNi9gyB;waU|C~YV_Rd~uTggA#sK?*fsKXJnD`^Gu`%+y zVC88rv$3-=w6U}?wXwA^wz2k;U~XgYHtHCQ81Vm8?zg`8^asRUjU{*2W)SQ zZ>(?3Z|rXlU@mYVoWR_`9Kl>+C)$ELm_wwwggM0raEo{07;nNgCgB|B9_AqCBIYFK zCgv#SDs9aHcQJ=CmocZQUr?IknCqDHnERLmogNK1k-3pMQX%IxXEJx%yEWia=2RKA zX^xfVTIO8lUglutViRz(7~ISp&0Ni#&D`w~INSqM0jIl@YcR(VWrT(H!#Y@WDNb18xb&G}laXPIFIl(6`~Dufa*pP0dmJ;i_xktk1w* zAA`d_R1k1lb6a!V2Dq*{@0FA>2fko&z=_R`wKd?%M=TAvb6#b@rFW&=_QV;!s1CUH z-{IV|aBp+)SK#92YZ$xyWbCoHVeR6l zXc*QqF3AeC4Qm|MI;?qE`}igr$o@TOB9yTBSfi;yZ#h5AT8u@3hI1TR&bKwgkCh;fi{HZG=WAmf?j0J$l8%Lq=(UrtSMPrvc_}+T2mdG)5VJe4a!>7_c(S; zd7x3{Poi112HMs38E9F=8~%x=^*-9xba$Y2S@Y^|Mgz-53q$vMxG2!ZZbu`#If7=! z@8xJ`KSEctmUil*KwB${1zMXmx9rKZ24^iUt;zi@GtlTJ(dx#~>^7j?_2dRx-ouo+ z7j3T@jqj#Jp!r>fmiNQOXo2W_CsVG3bOh-@_V>vSw8M-oX)Q6WDgLD+&={>X4x%}( zLwkH0O|l&=^1hlto4lF*>)5YFvs}J_hDoe~`3nQb&ZBLvM&m3Dw9cKY&_1`MHP8(_ z=MC>PX~QJHu?Do#`9L#$4DIv*VitFyrQVu_wu;VbjkP*A&|J$Rfd+d7TI>PDF!q{Y z4F+v&TdMv~SzqzDHub+vEaT-l^^X#l=qphFbHpzmty2H}b?U#9IL0rU)qfo^j?4JH zfZsD3)&D)}9$lmULo3w3U$OeXmaG2lSwr#P8Qvk)!#kv!6@HBvMSQ8k{j9fmKCJL# z^$P#KP~la?I2zj(URkLy_cwe2Yd6jyX7OF}k7`o*kX*ij(akq7O8Evxf^T532IIpi z^}SQ2zLzJ}H&UTK#&F*=Me5_e_O%ed_;nrM@Q&~e?>zNg#QKcN3H6obs_#4G9~M#{ zcCv5xV)bRN)cVi5wEm+Rt$#O5>t7{S5v$euKGs`2+d=#yO8kO2#jlA|{5(VJuVSsn zMZ`2JbF}_s(y?rRlf1k|#4jlG6=D_(5#krbEZ%M=eo;&OVj1y^T=hP=S-ow%PwH;s z7`KqOa;thT3#<2h_D?5Xal({(k6fkR1Bh?z$y$sZ8x;CHN1=aoEA;L%g(jyI8YLdl zJEu?=@rj2Q6uO7sTZvQLP@_=oq(T>zDs(#UiYm=k=t$}_u0nejDzrnh)_pdqbsrUI z-3)6eUM3b1UC_F9#aj1dJ>Tx`;@jP`e7k!w-|i-6QB$pT=Qe3wSv%jpi16(T;uc@u zqIFrsJhsc^+ZQ=}`-1d(uGWqdo9NHd+UJN*v@dAwZ;4SfO=#^6-CA3_T5Hc=p|$12 zIF8Tf-BEMAJ1WY%qndbkR0Z#jB6hKPLTlb&jYVRa*7R@Hn&*g5{E=h7WxJ_OYi=mf zn#-oO<~(8*r)6qQ33-PV@ZIiizS~`-HQN%8m@ib%U%S-vCt?vXVj1gMU-5LEdfHfD zaaWOgenH*};ujZ}vG#(s7RxrP=P2?IYGmz2L_Iq$V(mo=!~;(-g=(9+I#W-7JeX~x>RHfNe|>TTetm*#_)XGX?!)#yP7TDRFv7@)wX=4jE_F-)8lR4Psc5U2{r;RawU&+?Sw`gNC$3I@d zH;%CHUoF<)5^VfF3mQBSyANg=#KsMtSjzha@df9_G+2|N!K+F%SYNNfTZl)jn%3a` zi!}I0e8nAky`(_P2x{P@Lf`-<1YA8~vp)s~! z!4JMwqM`SuG&F}_{BH*9A6pdNIhXa2`0u=>iXPmlDDm59$!bMU;`kXW6|KUTUyNVB zvO`hEdi3ThMSsoq>Ul*U01G_YtY{ZLeJyxkV40%W@#ssm^Twp2vx^k{d%mKdlK;PI z4QJMCcu6zwacWoa9w*PC==pNJL?@Vp{PVyo7dL=az%eV!HGI=j4fEfI@0tPAfLYq9 z|73-RU&zt$`e_XhwP`p3CVDkf!*7K&{2ulGLHU1!xfX$|G8Z(mWVJ^22B-aBk46p! zgMBNakrL*NEaUfd_Rs0o$c13FOItK@6*%mtH5&N^$C`7Qhq99SC^3!vF-Id$R%_(> z4vq9~(a0d>(b~xA&u@I(deNujed*t z9n$x}nx~O}Hpedj$Nre|S1#A+btSAH&QWq1d5$iX90~KQ==# zf#=I66sssy><6nAyKpn}jllXpS*6&@Ddr&+D0XX&Vs}vgo*C9%=P1@*rq~lJ6~nK@ z)hZ0E1r|1;2llPiXQ@JE1V@3L7C$U;dgMzQ)=ORaJX|;!~fb8zo-K~2><&D z>AFtEe?}g5I{qul+*zmiJ(Y^zPu+*I6n})az?Jc5S1I08q4@ed#Rp~-AEwSY z_-hfxr>XaDHt!thQT*@A75{i%@dd8uiyVz_*9qq>()cd$@9Yw|a7g3(ErlC*X#5cP z@Zt3uFPhT$v2gqo+BCjw;mfrU?aUwCtnu>;HU6VIjn{N*{BriM&eQlxxcSehhZZq@ zYfR(KTQq)G20T6oK2O^Zah^wL?};+_KI!>o8edCZAIBoO%zIp{@v%9LrxF@}rAy;e zO}qo5O5^VqX#73S|6y3;a}65*ggXD3*Z3D*N^G~B_(P@=i_tEYR4TFiJX%Gw68q(& zU4)b$- zpOAkQZCtyc^t&>CUZ=z_sBG6m}$8kK(cMti3c9X(3$d=g!pr<>Hn zc8Kc>w<{5${V46?cM^$WB_^_zcxAH^f1=ErtI&%o(T#G@kEYO(T9x>y3|)zHeL}nc zsbY@pRwWm;E16NE6+G`bab~BaCV}F10590VClsl|g$s<~o zEF!PC0v)UqJ&f(|mMM86dKcq2S)Q#VSTlJRb(*OW0AdsfNy^~}-EK!y4n8@ZI^VSlV>Y+0_~J?j_=a;_u7PR zwB<^bvwu1o_Zf?ns^Gk5Rw{KCb6?s?9D0Ou$iF2=@O)y&O8uG8<__al(Hl?o1AaU%vkWwobla`Y> z-^zKVuBXiPD@mN=`prt+kWDHh(dG@SNaWuTC2>tR%qjKLMI_q$Dea__x5@LdbQza* z9oKamZi_bEcKRGXS5a{8^fmZeXv^1@zDC;fHT&BAAN*hZpZwqaAN^nbpZ(u`4}33t zPke8Dk9@Cu&wTHE4}C9vPknEFkA1Iw&wcOx4E!woO#E#8jQp(p%>3;94E-$qO#N*A zjQyC@$2izCjC)_vON8DH3XWVz(huoLkr`)&P$K2Q4=iK+)2i+IlC*3#QN8MN5 zXWe&?EDHLv`?UME`?&kM`@H+U$AHIz$3$L6Fh)F9JZ3z0Jcc}$Jf=Lhc5Mm9n#Y{S zp2wiaqQ~Smj9;djkl9;TWOnV+i4qWTWXuSl4KieTWgzZ+iM$aTWp(b+iV+cTWypJv}?A7@`@pJ(4^A821_pJ?A`AKA&aeWrb9TPy21@TD#ITKiV} zSo>Q0T>D=8;GY&L?UU`B?W66h?X&H>?ZfTM?bGes?c?q1?ep#XznxGT3m6j^8yF)P zD;P5vJ1nUTuta92j4g~Y(pbZo!`QIBjHzn61B_*?Wz2O(dw{`=#ZG7ou$eL1;nXu` zGj=nE+ba}cI%B&Xz}L#UEND#lZgzkXjTMa}w2M2!1`JTk(9b z=+`R(th{78z|LT3W9c-eHn!dZ#x~Y|r(9|5{W2Il0TwqVA6Tk1Mt_0dr@`!xf!!Yl z!yC)r1E#-YQfZ8DtbYTT-`L+Az+Awb;G8<8Il{@a0cSXtbC^SzOPEu9eRIGumgEGS zV>>tv93;&}K7y0H4<|84F;{tcsnXmf3WqV5F{d%NF~>32`8}M6ai8Wux7H}liB`gm z%#qBME~L&6;7%2AD8_G^QyoiNN45l9%bY8hbC`pfi+u(D1~*G{w2$CwTuYj}O)U<% zTntWUZfA~Xu4m5oNN&IZe+w7915S9$T)+{{6|Y<#a7S~R!)IHtL#Ip_Xx z&wXh3YaBNx-L4_cQPW)YLpZCst2wN>tU0Z@Z7&?xT=z-#&3#)rz8WrUPW+2Vz>%-o z8gOQF=X2oD=F;ZWC2(MKY;$dM?!EH@4sI?ECpR}wbM*J%>gMdP!rc>a_#wEwIsFSc z0mpBL>)#&^xW6?3YXK|K1gs5Q+z@C56=()0cLo~5x6u-ob_d#mHHIbBM{}_Dkk%lq zMOc%tHt`Y~MGURNnuWECE;Niso0ZlytZn?dBhWgmd06|n7!9NfE#!=qfi_aof>uI% z2h)zVlRYS7EoD365&z8yG#2b@T63}X5+^1RsR}fiuC0Ma^ZV6kHWPt%(}0F^Z6lga zDA0D!$qcj}q2C-`8)!iLw*{JzwV|&>(TdQT{)uMvKIttqq*u_A@Ox=(X-%C5pRNcr zr~A>KR-r-N(iLb@SD{VSpi!Mi{WF>a?aCTfVLO@@b@oBqTGE!*x;Ac|3bZe4U~i#? zy@Ey-LmS(W7ieWqvTyCo8rtn>WH+O!tt<&NHfwE_Re|{Pg|LgBmFzO_~1pOz?m#iYU)*76Mu;ut4V z_84LpOWXJcMxMf7Bd+n)9QFN|wG$sLQs19f@D1;2^~G3E(buWI=Q7pzNVWRzBR+9E z+YMFfyShVt7qiad+yeETLhPcXU44g7sc-+q>f57OeLJwOV(V(H=YOw%cY^rEX00D1 zP7%)1`sZ@B{*eN$zpqH^n@K+>T~$c@BLBo3B@FWQJ-5Q|u-Abvr- z;;lLLP7t3MYEti7;uTM3sP{o)8+WZ#?=Oj0Tt~d((nj@yKYCA_Q19_8)O$phdiQ5d z#vUc=&1BugXH^RQeO93v;u9~k-eR~~p>@P0o~lr&Eu_%hOBEsx8Mp-U-SMXckr zEQOAv+~GY6{of*mvda|Op7j$8#4kQ%O~qTw_;z;#-|p_#x)*2oc6X-MwH9jK9pzfr zP^WcQ5$mXKql&b)n7l(mTARC4Yj-Ksn*Xup;-9Tr^B3Y4lUuZA zq(y5P=Z(wYZ}Rs4on#myXFu~cg=>fyWH#eBCr%y+wqXB-sLn!VWHk@Xf^SxfPE z;t+2WpO~mn5C5y@#ccI-)Ufs zoL3fV;79yknWuppIDSheYphoAUZ*JybTF=;YvH|4a~c>~rh%8*G%&rHZ%!0x;9vE; zcdkc~9TpVXy-1O~T15`Uo*aek(Pl+XCtg}b+~UXBtgC7i`PoWEnwk~4d$l4Dv?}r# zsk=>)KI)9DQskv3MW$CM@;QI0)> z&sf)~!Hts|9N((J*Qh&F##+B>4bC@e@QYT~01{K!4gZrH)zE=44Hc4(Wt%=R^nLQH z@O2mC*XrsubVD`oN+jlRN4AFU-K?R9A{u%E|M>iJ4fW-Jxn`nWLsOIk(++*e@lRH0 z=zsXp%&4NfEmss@6g_}=#Nh>s7Kar5E`InleDyiZ8@Q0)+A`kz7*_P>g^D({DS8+F z{r);d+b0x#3crkGutv%nX7l?30`IC^6y^wDV*u*kz@L`>He? zE!1!_18l?bw`uGBS`E*EUly1rvQ4K(cC6OO*RnLS4>&2mmi4P_gI`9DFW1OPn_1sl zuaT-ujZ}llE(brsCr57N_m+0PE0L{{dz*OYCv7plM!I6mJ7L~PKY7E`%tt9=UP_}z z-cD%beX!d8Z=ca8KN?!W9Go&@5**t!tI9bz?t;ehGcJ+Svs8)QNNyRglDZUH5Z1*gszgzf?gyIDSiXYaX zcoEy*$yHpGJ()76&nR9AM?4=6SPiec6fRdc#ay+m#3Cs7i(>d5T)A@z3C` z+rUq^hubcO&+Z2I-4k7ZU$*xz);NA@{IC_oAr@);m{oAzT;dR|aA5fH8F1lqvWY{$ zzkfuXOKLQJ1w491fyS?=?2WS;Z={W1aU32vzM8uCRcrhK+WrIkkHMFpD$)3JbsFy> zuQ#Oe4bw``O&sG`igUbD1iz=uTcmeOHU2)oTgdw-=lC>RqQp&|%=OG--NQ=eesb(?+P!zR68F!bn-rm+G@zrz zm^VuM&(O~EoO=!D?44F(!(t@{DL=ybtvv^~1hj*FDs8I5U4yOO)tp&Jz{nLCe;)TJagC|QuB+p9<8t7h(VG4G4XRx*yhm+V0gY(y6#ZY3Ce&Wcv}?e1vm8 zx_5@2RA(k>C5iG+QtwIfo?KAssd5tMerissr;ADT zq%etNU7WXTIjNb%wRBCAIQKKO^$hJkL-}WD>zRuGFFDW0T!yl)ldkJF+?LyaprE>#^%Gd}vcJraiVj#y!?O<~{av^8;I8n_$~u8(~{vo3RtuU>jmvVw-|(3F0Bx zn*U;RYVbP1KVU9wRtMAS+-p>*f85N+ceuY+c?`g+dSL8I5sert+Y+FZM2QF zt+dUw?X(TGEwxR3iewvWTWgzZ+iM$aTWp(b+iV+s=VV~BZM$zN3T(M;x^4UQd4a9J zYGq*i?E~x!>=W!8>?6*_SDalC_>S`Yz?ayks44I<_BHl7_C5AN_C@wd`L%(Mvahnw zvhP~b9{4i*H2bz~E7HC$?ejjx_t^*97uqM-&qWq=8c4U7?t6^t29 zn-4I=cgZuRIEM2ZV;E}~a~OMkBNSi}V-jPNU77-{0%kFGNn;pe8Dp9)U>jqccfmTw zJjOo8K*mDGL_=GZ#z?(jrJl9`I~hY6OBqucTNz`m&J8e^v6nH}jbJfjvK5@e7|mGC zn9bPD819Vr0Mi-U8RHr26)jR4`xyfo3mOv|8yX`TD;hI`9gQK=Sn?lW%D=Hc3&u3o zoC0$idnPlL#-hfg#-_%o#;Q+J&)C%%)>zh<*4Wk<*I4&wxdHaAn+mY7F|o0+G4km% z0cIBEze71=X=7?*Yh&zvXy2II*xML-Z_m#Ocr%Q7?bG-NAdhfvb z%>B#(%>^TH!Vuih9MN2{opR=m=8(4+1f0^`(j3!V)11@X^UT74i<*-bQ@#+6YOb0G zk2H5Rht14LbJ{ewHOGA)uFDutb6<1dVYsk4vAMB1vbnN3v$^w~aOhj$(&p6W)|c~J z4M(nmbDvQkaPZ^H15R#kjtxq4b#r!ecR0Mce45jn+neLR3fE7<`OW>U0ay!o5>3F` z0KPP>6<9N{c3=(RiUp-L1#1gukk9i^YYx^P@GZf2zR@JIveFubwTiT6VeR5wG>j>< zj7?}7BWN38G!APW);!wLKCFS ztvao}Sc4h*1P$jyv>j_RQ)o0Pw3_HF+D!o(4*O5E2Aa;j^JqMIXg#HY=40*WVsxG= zw4m>Y(1s#tMAnL!pK8s>+R<+0XQDNI(TcW&KJ_=Wrx~=S*U+328G#06E$W&2K%4sg zqCl%^V*kd%K*PGE1x<@shPADeXrrVc(7X;p`?3bMTSHnC+xU4i8d)Y<8Nb%dUO_vH zp}B29OM5XCXloCnw^?hm=5{lh+I16XaXEn|cXnr>(H&3B!7+ThLTz&{ijB&{~VoT-kn}{Ktt&Jb)H^7xTq$A#U;0 z#jL?VpZ(EFZ8*D48%~aD!*@1oLt%zCTk|c|Idj}Ts@)w9~1BRK_l;ws^lF~dFnrMv-%J0R{!2B)W6G8 z^>4FT;ZKM^d_bJytwjn?bSWGqp0RF=!cR9V{D%U*fe}{tHjaZM!#}A}xO!IM$~uKl zrtWvD6+VoZ#{ZQmynBzrJFtdgYmLz~CMEqho@e7VG*{by!QT6_tH57jKq3z1}b~o!O-tW@7*I7$}M!arAuGV#zXx$?feEXt?Z(lU{NAb?6+sVIi zN^9#nwD!Upt*u~R#4^6sthEOeYwaGa&&XuG#ea!md^n*sZ`EndCf*AbVO_-w!TC?4xdOjf@@!o3IUa+QOtc0}} zt5|z6p`M3wS$k2<+KV>UUJ$3aIFGd#m8`uW*75Cb)=n^P-e5eu$5{E8G4gpt13M6J z*gc2u&V=~>%yJDJy;TF>W89rlpn>zryOh|&H4_@RsX_zI{I-;9;14m@R26GrZ5L~- zvUsmk18c4r*ZB5rI%inL}Z@@SqS&tgw{mnjm(=4`4`?}R--olutFPa^R@BXE!ub!wy(J!ds(E756!cVtphu{9DAC9UG2iY zVk6%oe{&0VcPX)n2zHpb@@`Wa+_#3<1h)A|?0pG#`{Zg3o=Lo6Id&ZFa`2jL4gP$k z27fi7!PVIF2bwk5LA~ej3%%4C>d|0wUV~HkfOl&(xTQmbpW;`xS)`$z@-+n48rqNb z4F@-B=v!?XIiRFHdS{8Xxu^`E$j@_3@9};YWAI=kAH0&6`#9kkyJ76)Sq&jG`wuDtczFqUZN0 zT7z%>2|oF!wTd>*vo4Z4_s;RY#}$e`PQC6b-UAs^bf{3#M4O_oE-3mAd7G(AKZ<^~ zT*Db)jGe&>dlqXr4=iyoI0D-^T#~Ed6Du_Qee!<5JcsIthA*GdFq-dhLpE^E}^ z?=J#-ke;Oe3;gyGe~9LRQ7H2&W#0yKybms!<2?Uq(8#ux8ri9UxC9s}H>{BZz)6Rd zXyoWc8abgyBV}NqGr?HrMKp489qU-ZLf3V&u5}sfTfthtA@9BgjXd0_ktZ@W@?10T z_M~nEJQbtOmzHbf^(gaAz+-<~#rMs&Fb}1i`6ysRIPqvUm@cYC*M5E7!`A%Dp zMmMx;6kj}=qTSc3HTpK^crQ<*e_!C;O`PZRHgIeWcos~YO>82!8=PAQ-pvE|PJn;G z(%<9QsWHXQ0xzGRsn|u-xx7%Z6;Z{oEwNuv?$;HH-32!OU72DJMHG7s+}uUEo<)lF zQExDzSe!OrYEWz{rr1o5V&H|?M;(fNvRN_w$5=*{#&&Aa*lrPx?F9$;Mz+QZiZpgO zyyB=@jUCsZv1O|@b{f3mtPYKxN2>1B*kz=v;1JhSw*fwJD_ny(^4L8~;UJmJt%6fL z)(tml3OLI8TsX@l+=cR~N{zjmp|LkZ8hf`!V;?Nm7;E*$KBfLvc-VGLith*~%4$=b zJ|747#Pi{6hrp4J$Y)*{`NwA{z6|bkDxB*~I2gVt{=<3283XYv*v4+ge@fk(D06F< z;O94NSD|=YgW`{LFjo!!_3RdSUM75xV;ecgXem6fT=7>Z_j;A$?^G)OSN1<7 z|DQ`0|2LfO^9+q|3s23AYJ3;?Xf|<&y-PKom!Me@$+&ueql)CHI-kkhgb>cg~Q(13knIT9FhZU2N{vsnX#CZf#^21)_zXPwugf(~pC4zA!}$D!#{aVg9>0hf zL^ixWAAVm1&xiZxpdswb_Wvza;-EYw;9ZGAbb)WrG7mDL#0ecrENf8W6f}YFbNsA1 zC4PWbu)LAEl+?Xsp1GB130E&ii$Ghb??IcWQsS2jXcepZrYL&C-4P}5If>RuB_86~ zAE%Ufoc5k#|G8yKyhxo;4s$+fGs1a?$s21^0=t`dxfK1R86AbP?_@K76s_a^RZ4tF zoB!Zi=5v);C}Ykk8qFefA8c4Mvq#CrXgOI^N@mX}nS(Z!OUfhVlUKm8rRYwDXgWpI zDXvknq)f@uTqVD^SxM}7`km-!kaspZ)VT@tq(&t#EK>3!+PGx3l9v}NSx4I|(2Z77 z=BJ&+8_>evWU5`smqJQTa!r4lVNUX#l5mXVpD8y> zdXMc7=9$Y(yZ@lQkE3XN9cX;bO23c)ixQ=_$zfjfEbo&+3*2F)Qj3ex3DFCebSafx zLtG&nEisCw$o78djNj-~Dt{UCve6q4u0w~U>|u>)lNo4~{C;~YnkD(i&MI|Wg;FPk znG25ANWV#)G==6_g!aky=?U~u+CHlaeY6#wlw;>DMmsGF~zSD^=UyqR|H zpv_fHO5MeI?p{Dkrp&#SO5K-%*35P**ZliA^l0jVzfupA*G~I?jG$?kp>0ReyE*?8 zJ?P^3=;SSE=!?+TtCf0|W8KtweuYvmu-%iT)SBf=t!-0k-Lz7@99z%1`WlrAcPrID zrPKg*Bjj)79D^Jis!=LR{b9S^A_TQrH+Z=y8qSQP2q-GN5n<*u=kTxsz=OR)ADNNd`)Vn35I#LHIL88sS-h_9{H68FrmHiq}y_vZkv3cD}7GN`x<;L>1*QJe2wXA z%@3~K|H1#o|H=Q&|Iz=||JncD_rUkU_r&+c_sI9k_ssXs_t5v!_tf{+_t^K^_uTj1 z&%n>Z&&1Eh&&bcp&&&)Cn}&)m=6eZYOeeZqaieZ+mmea3yqeaL;u zead~yeawB$ea?N)eb9Z;ebRl?ebjx`eb#-~eb{~3ecFB7ecXNBecpZFW58qKuF_y^ zc#L?gc+7b0cnoq5~%b+hiMMTVBz$)Hc<&)i&0));8C+ z*EZO;*f!a=**1ERavQMOw%xYjw&k|zw(U=s2DZL~V}C3Oe1Lty{n>$Uu#d2>u+OmX zun(~>u}`sYS-CjyHTF67J@!HNMHdzYzR5nyzREu9w8p@PeQ$H%)9l;qNhwGXy0eg~iY2EN%o`el4|az60g z_TdqHdAKp~?e_8Z_4fJp{l);s0>%W!26xk@v4Sy!v4b&$v4k;&v4t@P{VI()meZy& zh_Q$D#)8I#r_+ux;&+<@%y@K8fFTcU3oxaz<=&-AW6fRK1MCR~H5N@{Qe)E(!Ki-) zs~WSu4t6z$HI_A|-3Ycd#x>S8=Iu-b7}!|YnAq6Z7};3)rWFBpHikBqHl{YVHpV^^ ztZmF~>}?EgEPh0OfX$83_w5NVyRo}5{C41fF#Xu4V0&Zy55W4y{Ko#~0OkVb1m*_o z;Rr8Q1f0R#!5qR|;+~}ew=lZl!I55qE5+eVgK#HvD08VUIMritD|4*hl?9y3+zY#y z=3?e#mlp;c&0Ni#&D_l#?mLZ2b2@Xo{5qw%o;lyn9Qz7<^7FQU6Pg>EBfbq+d=1W+ zY797}xuiLzxurR#xu!X%x#w+g(3@KWPHJv?DIC>Y_1uhryPgP#HJ3G~HMh-&d*;%v zIj_0zc5P`coaV&z`7}p<9j^QmoY~yj96AJ-Hm809Zv8ME`#!k#Z|VZ>Z4PcOZcc7) zeqJQt>SeV7cR!lChm{1JJ~te2{GAHYoZs9(tpU7`7GO=_RkVR58iBQfJ~V?DzSIz` zCEQQ4wqT8+fil({E=PN?2JwUTK$AETZNeJGk!TgxEUaBv!`K-u0~=;-Bdu|4M(eQV z@haL!0u97kNH1}TZjL{WM)D9^$-NxAqdw43ep-vBg3eM?j>ZxWv=(bF$B|c58fY=t z&$KpUjb=M^pZ_jGyD34#S%H?*iKa6hXgdRFJl1-w`B?k89}Va?XhFB43H>Y7S$v}$&8>O`=>~31C>RlOVSJto|K+9T< zZuKj+e_kJGU6*#FeJu+#Fl%8Yn*(j^Ab$5n-`Z^!?W{7bp>6zEVW6qu`_dX)tOw04 zGtk~T(diySbF(IAZSJN}pw-o)+5Hf0?yM%XJhZuzNu{;DgV6YL3zXLUwnsNa1B}cq z4>Z9y&=su_M#<~t*s~>phRFDjtRhbF%S50tUWwLNU9QMEIe`Xwd}p9Z78EFg4@+y6 zv~BG&tzpixZB5hK=3q{sbz1YZ_KD9KXr2r-QEQ`@qI;f?mU>1(pq(DQHPBM?$g{Qz zcCgmE;p6N;d$k6eA|5eRz#0s+*r!)(!ykxI+&jk_j67{Cp1Nd2u@yhDl@ z#V=TQab1)8FDvIAQsjMqLjB)uR{v3~zc_e7{rggP33a}bq3{ChCble77+f5FWu?NS zi}?lyF^y-5UHp-l#C?SdH}l)T@70|O|2S9Sb5|*R%2tJsBYsgBSLZHm1HQ)=`XCsBZ&tiRTmQdyI7#t;90!tWzI0sqdNy@rxPtRb{B}G~yb^ z7pw1xa`ojC-`KN;_{9q17o<7TUr4Xj5WlD*eo;pJqL}zaF7XRu7Qg2GQa48U{zVJ% zi(29r#57JKzc``whqh|{*Qv84TkE$YF7YXADn2Mu@0-)=O*N@^Bk_tCW9ogpM!mmZ zq~1GQ)O&NjdRK(h`{Ppep2M-SW$Hba_{BGi)w^GhdUwlHZw9f6Pq!-c0q=}@Gonx` zS0QX+=!KX+pW;Ru8gU@HxmzR)xeRJd=r~^;3>o%&S4Bz&ugG=l?Li5gDo7mo8u46XyEZ$ z4Lr{{?x#NfXke24ccxfdRlypoFl()dG3?yT+AHisK5>S_+Y~vLn8it@ik!Jxk>!($ z)M6*D!>-)S{_QbE?pvft`%*=oCU0${A{(ibh$`}@X+_?p?njx5{3lx*Gl)s-iXGdV z-vda6x!U+0?AJ-e8!CuLTrj1Lmxi_Rnl^20XwXLRz{cNV+u9a_?*zTjtd0HU#3rco za)Q_d@r=!dtZSRWuCBtq7JV6;z*ZMzVt+d|cpP~r$253W1@?JDgO~A(T^hvC4&D~h zAUJ67!B!1Ej_vQpZm*AOa5z_kn`q+L zc4ith^tWo>nOLf!FS2-t;ub}h;1~C4BA#Etnu#3Z`;)}^TNEwFXP&!A(I0gydU>Uy z*RkEuuIO#}&AX=*{XM?+(G1o>u2OU@^#(c=9m`epWy(y`_Fu{WXim|E8Vzp)PS^=t zkiAO7`{LsdD%Eh|Y7HL)4)`AV<#if9r$@sVZqaaUo`$cf((sLxy_LLG(|ltK%B zK_{5td9cEI+8hESB#Ym*~() zX}v~HDb>hX;GXj*HS*(DjnomxxPGxlZi;E-wiOz=J5M9`v)v9BdXoI-+cmPjSR;e9 zF9ky8PP2-A4eYy5yJGu;g%7O+zve5(xC+)ufM?GtVb0D1c()7O zyIiqr7K4MC$J5C6ugSl=jql49EB0_ivB#?vduCd(HDKcK7R91jU5@&*s*X1f$7UQeg;_oTzJF} zr!-cxRbzGF{Obxdc4LXgep#lm=1Ps-U8S+#kscy-zz3c#Rr-BRy*Y3W+8&u^PE~}t zRkZbHE!>2ru?N_6#ofa?7D>FH^QA7+ZF#c9PBqWir*Vn{DEx6+u44+Tk$UH zyjY@mZ@uCJA;pJjD*?B9xl-||2Ii`DFlUYH_}jGNbMx@N4EP`Xal3qt?*JFw75=z8 zX&?6CJmUvtXng6M#*duQcyUzY$9HIa8T|9q3XNCfF`tfO7xZZSBKYTJB^s|=(D=2T z8n3U>_|4S6HLUU5*}gkN<1MWkf1pU?e@HNgZ#nBBX5qdoG`@y5`uL5&X-CK(pVatE zwDlSsb((sAuI3#Gw6i6Pd4sg|Dd*d|NQrGqmDs*fiJihqWWnM0fQ#<~htGq4~IXc5WY`3n^Z-eACfL!ti+`YN?b|#6;UOw?@;1K+WrOY zHF3OoPKn=ep5M~;{Uu5~IIl!|H}A`?MvKT&;#qWr7i*MQM;qZ*B{tIba72lCg%T;+ ze1&6wnpOJU+%wZk{Iy((4>rpGos}DypjtAXlm5|0*!8)4y7_8%sZY@YDYAF^`O-fx)-TDgjP0HM~7|pX*sb5mADMP8-XuEl}Qg=);=bU4AFGD+BiH6#VmP+3J z^GZFywYL>1^)O`_bK-$9wj8j`ZmCi|OVN30Z`~Gj-v}CT zD|#@;2MU$ixCpH{tW=abBkYe-W{l(GWoXTt(Ve-T3F^O;K%ZWYPR;SxIL{Q<`+Ad7 zZ)T!*bMChm(8Rf(nJM&h&iR*irT$9W?@{OdkWwE|{%@5^eMs9|rj`0APpN-YD>X;^ z|BNd2G5>8oN2yPiDfKDW_HWLy(4y3TI+glww^E-)l-f$U&tpn`!S??m+O()!o3?4w zrfr+G3GBBiqgsCP=ig*UXp6+s^YbmvLFw zab34TTW&MmHu>puaZaB*eGOcTuPJ?Pg~7F^uX$1MAN*hZpZwqaAN^nbpZ(u`4}33t zPke8Dk9@Cu&wTHE4}C9vPknEFkA1Iw&wcOx4E!woO#E#8jCfXlX6a|=XXt0?XXe zKI^{gKJ32iKJC8kKJLEmKJUKoG2pS_G2yY{G2*e}G2^l0G32r2G3Bx4G3K%6G3T-8 zG3c@AG3l}CG3v4EG3&AGG3>GIG3~MKG48SMG4HW&8(>>tn_$~u8(~{3Z2N2ji|UlN ziMEZlk+zk#nYNv_q5BpDHr2M3zLd7Lwz)el1UA^Vc$<#2ZBE;0+v<62wr#gu}`sYv5&E@vCpyZu@AB@vQM&a zvX8Q_vd^;bvJbN_vrn^cvyZc{v(K~dvk$Z{v`@5eJcH}8ue8s!@3arKFSSp#Z?%s- zg!T`loPDo-uzm6Fd4X@XkG8MgZfn|ir+v76xqZ5Q`v>@V`+EEQ=`S&Wv4Amwv4Jsy zv4Sy!vBQfTGnRNNOKEIjjA5*CKbYgU4FLveo(nLEv57It^&LuM7GoD<7-Jb@8e>}3r09$4(pV6thj znK9Z+U^Qblo@E-t8Os^d8QVQI8(=+SK4U*)Kx08;LSsW?L}Nu`#yYU$Wwd21X-s(z zZ5U&oS`lDQV^3qyBWD6kYHVtZx=((9S&dzdVZpM-v}tT>jBBhr3+8}w2cES#W> zvGD*HxfiT#%-pr0G=?^oHm1He9ANBUgS8vM+{WI<;6JGfFuAe0F}kt3G5bl?N@I9q zd1LyeSpmj3);H!i_TQ1~$RK_JCm8z_ZeWgJuJA6L!Q8EBYrKRG-rf6nnR|!(YdG!<(r zcTWdeO9Q&fb&Jqossb(MTr`*R@<5|0MyolTde(0Cq0J>|IcPdVU*rTD&)?8`X3%_I zL;Fdf0S!jbgwTkr4RxRqSu0x2_HE=fpcPq5vZi!lGa6GJT2nciQ-Ow#S`=td-{9Du zwShLZ?L1ml7n)UFpk2L=*0qTk#c+P0ZFQq%J)VK))rR(kHg;Pi(88>VT~?{IMs^k& znKiTHIBpHCAd46VZR|Fc*4V7IZTxsvY3m>Xz;dsEihUMci`v_^Rny5+I+fp(eCb`DzR&S;w3=A|{xwAN|Oa}w<{_N5m3 zJim|E1sdtyXr;Ht0`0VJb)co5gFbpncc8Hz$?*f*(O#*8u4FBC!@r3`e7J=*7&)xL zVEsg_UK_&1C%TDEbP&&I-Kq`0$hh2h3dayPW@L<=0et9 zoRy*e6MNKu4BOvar2ahO9@$0e|7t?v&uaJv#umPTQO`FpG87(bQn;VrZsHM-&MSO> zox#4&!n2ujuGjFEP;G+lfy!5U03`-;2mU zyIs8}Emm)Fy?PIsQSZKG>fM#H+h!;O0**SL)s zM?LY3E4C{1L)KlKK^ZkD^sP39xM!g~r}*~8X1;x~z_%|l`SwLN-@YIw5hbl9b+Z4! zVy(M#tJd8#r*&7eHshkO)}2kvyQ5a{?kHjtugq!f zaJSa3tJT^k^R)JX39VhVQfqI{*V?ONT6+<(inEDlEThg*th+dfJaF6E9XIpc?pm$+ z`xd_2&F=*JgVkE|A~B1{SZDFOF0Ey z1}Q;QRLmrqJIPIQliVctM=)iWDWNfFIaQY+VJ(-i4bwX6&g4%pSa+R2Ci)4o7lw~xO0~8 z&(v$+G4eYj8t5t4!0?;~l64x`kgkDE#4$e2*1(s{?d^#rWMc>RCa!Q0w%~}E!p9fz zUZ+OZV3AkJe7_u9aXs_@_Ckg4rEV*AIx-c0u}tCq`rz9-@S^Z^mcn53@V_fGxD_@k zvqgiucWH3ns0I%v7Ey?;IsrR(8nKCUNR`AHE+_B$DGlD%qrs*Q4L;nULF~rh3#A$i z-y613p?RQ_Q=%m z_sTVVFh1eP84Vv#Y@%$rhR5e@I-vx(pKX6RmAlR zi0^M! zMKrQAzIx9djpXHP`s*&)WW6yT8@w)Z3bpa+c7 zNnD~C%u+{OV!1}oE752r_@;Ub*am!aW2;8*0QWR4)@aMTMjxa8Gwi@a*ky?$$ zssA_XzD54~=^FjGMWYMgwzROuwlCG#&M}Scxm08O@_SH;#tsi@tcdcGR-QM>;Jap3 z8v8kz?pJxNl~}=ZDb%|uM`L$XYV4j)jXf~OTHJh%b+F$Bb|bbl7M|DGICbD5V{a_i z*gGpV_Cb@zK55n1f5D^N$LRK8)tyObF429+JFr91Lz@--QN2>{K0Uci(K8lAEHi45><6r4-lR7?U4`(&%4f1&K9IYrktDLO)1{C1S<9o<0QKWY{I zFip`<>lFP0?7dZ;#xpi+JPSO&`#O#9U8wQ>+co}!42}PAxyFmY;w70pe^jAhjl`VB zFDTG>Rh7msr`|OiHGX53#%}}5H&XvTaQs6Z8h?|7c3%pLg*rRSVBlnTwRdNpgACYF-oHg_rDC4`(UU#Np}0Bj7CH)`^n} z6uhg6GVIvI1>{%3cP>xU#MSh(lDu2GG;wF4ChqCc#QjS&(F*r@B2N=fH*2DcdaEim z5u)8NeT_G0BDql$uTgI!{l6R0#0Qz0_?Y8V%z*dp1nnk)_2RFQ( zy1%E7d&z%bxh5as7>{q%WCxs-dpY?c$L*zTpj4BQIZaN~^UNM~{zhLLD4(X^P34;W zaGE#-eg2!iwv@q<;p^MMn=_jf+ZnFDJ9#->JTI8RbAxc|#qjGx;nYViQLLy&F;RBn zjACVQ@iVAWLU9h+ZF4AufMcZu|C@LbG%{ti>~BZ%3{S{N$1&0j=P}}Eh0;?8OlCj`y=u{ zZBy(&MZ_Osil?Emq|Yk8J$lTJXd=7tyBqq1B(p?#f-%DzytYud5)^&=vah&#M#XD*h?_8>Q7y8SKn-yOjLN}^MKPpvxI2}ES z{fQdI6L~ya8&mvmD-?fYk>VR^KTVx~(BJ#?@nHs9RfFQ6a=gzur~k6QWu@Z(Lqpr9 zg6Dff=v`=EJ2ohhHG>XTNUVYFJ<-*^i$=B&8d_e55(mtpp|SsirD$q(XlvvhIi?h37Q^#{1OeWl4D&QRpQreN?gV< zR?zOMEG4d9k3QI<#BY}=fo77ZizspHO0-1UHBkTeE0nmKdidzXee`Mlv4!J2#Q%AO ze*Q$ik5{2tZsb|-G9{i4qi53gFOBG%)OoR(XT@6s?X!>ep#~-T>31-bXU~Je;(Ix<5=O zZBgQN+Yp zsg@L0^0Z9SQc@!+M&evgFC>+d>Pej>j(z%!l1u4#Dg7+ve3#~t=qHuD?L3!J$8}R} z+_s+NzUb3^r;g!c`Iwo(Ii${ozI<+}bEF@iv(MfCA@yJA*Z(AUz})YsP6*w@k+zk#nYNv_p|+*AskW`Buy0#yn`_&9Y-V7KZIf-AZKG|gZL@8=^T@L; zw@tTg-@PHQ^|tvtvTYw=Uw}`rZ%FwF`wIIE`wsgM`x5&U`xg5c`x^Tk`yTrs`y%@! z`zHG+`zrga)$H4c*_YX;*|*up+1J_U+4tE8-kTBlMEgejNc&3r%v#FqL+wlLQ|((T zs{>zapKITHCi$nYP|7FUH`_Iy~wlA-=#_^3}7r^OaL}8 zMo3`=V+LafV+dmjV+vynV+>;rV-8~vV-RBzV-jN%W0Ysx1I%LVVhqy)mNBL=wz(6G zQ%^GHG4?SAG8Qr>GBz?sGFCEXGIlbCGL|x?GPW|tI+Wur?obMQ8G{*%8Iu{CWi|y^ zZL1y`yQMIkvD}AXx=mm^V?1L$W4`!iL6(#)`&_#*W624=h&-QyN>| zo*rP$>*>?j(-_oP)R?qlCcvo1s>ZCwuEwy13rb;HV_Rd~+^zug8vE|FG{C}OVq@cp z&%wyX%Ervb&KtqduYskLU}|IQVKBC_wlQ}X*!yWP_!D^nCcmG0#^}cC#_TKemBR3s zQ_q;**#127memKC-`KyH{>=r<2^MqQ{U|e6*q!s)IXB=Ca0zpY6u0;Yj$y7b4d*cT zcohy}E;2$gH(5pA^OSeMSIk?@Bb??oxQ#iExsEwcRh3d4=p6d`Df>UEP>Lg& zE15I>fWG%9-(1R^Dyvc{js@2;=Sp!ebFhEF#mvdf&0=sgbG5Z_Hgh-bWs1u^3a2x- zyQf|$u2%==Gxu8o2Q(KnCp;%T;E3jmst&lLIi$JdK5$BNOLNTaNsKRZ&lCs!04{1y zYHs>69CbV=;H>7Z=CB=b*)}-s1901W;JA0Ne-m}Ci3J?kT-co0+}Ir1T-lu2+}Rx3 zT-uy^XMVRYO>u2=?iBZa9}fN&T>Niv@;Kbw9Nk>~ML7F29Xd~80whJ_q$ov!< zN^zj2tVe@Mv;-Q<8Z;SeE*)qu)?k{^YJQ)CHd7sFG?$^(R4xg$o2Bi6mUB!R+75kK zG4g_iUPn$mrZN@+|tp*2}^ zvi4*R%34&}@<5w9lC}qT1e(Q%q@#_<^vs#_^s&dwg*zS|oYaB=4o(?Ztssc{!S8CEBGm%u^fDG?xV0rZrA$ zo!RJ^+h?XU(3BPmrWx3f5on~t^Ju2a1MT!NG}J$$m)?n{YHjsOG}d3CwVp?u;`Cb9 zV4%Ss=`|Qjc&`mwY<4@}!I)wVMjGG2V9mvQW$J&UO8xPA^$)hH|0UuYPlwe1NL2mz zQtvj>b?elB8T%Iy+xS_x`cH1>JKjy|KeUGLcoWCigZRac+3HW5;T=+}t(a}#9a6+C zrn0nlc%9a+CLZxDYc2kip|yXY{C3t>T%W16mxZ+U0%8`+=C!u8QEQ9x_y$Hd-@qv0 z8yLhub|_QmOJWnB@P4S7s6y+BQA~C!G?1gvU+ejX_Z;8wrmT^;MP0f=S1nWMR~-tS zv!KwaMG6&D=kSO^2T-;r`#aVsl-8y-pYi^uP4imw<|3_$6T=9zp5nzCt$C6d#e>8$ z8as$zbP>PkCVoLGC;g<8_yzfgI(|VsgZSH;9f(0|>r~(E)#|%0PkomXqxc2oXB4UL1j>$RP~U;n+bgENooJJ`px)0K)w^kt zde^tBcarx;ttA%GMNH%I4)y*qN4<9t&$ynrMs<~Xe?gq%i~{wZK;94M)SF+T-aT8? zn>nMN{}rg`6Jinn2&w09ThtRRR8Jr8h3ahQ+uiHvb+JwS}&g)XgrqFJl|SgX}{5U04V zNUJX;o`J5t`m`|bj%wuHQRL_4X!WiewHiHq)qjaUe3-3O8(CwKAXX73Zh@Uz^#rks zKN728E#9iyVy(J_br9LKU2K7+LrVGTUY+<7Wb1Am>;Kz|M2pUL4nG~~Zip@F&08dzY?rspX9 zt#*a?TCDK?T?!w@?=hXMsmfD$S&PEUiCtV;!@;V~ZYSyPdq}TQt~5oe^x>RJjH>6l!o&js`a`Xz>4H8u~`7hIU?|p&ab$ z{tLmo_lj0xKiNM6TUtT+#ibg$Vjero_8sKkyAJzHo2SyS$4wf-ZVrtSw|F^ULmT+r zl&7K1EgJg2OulhctKo0qANIr#z%PamA@4_(8a|;%!>48Ne!*oL=KP1N@h8{fCvL_! z{GRpmQ9V(ni$YXHL<-b8$S^F|GSf&WS;?KGzdvEInOGm0#p zR^*77BE=y^PH9yH3>W!Dr6N_Oid?x!k>67PwjSd7&M8G!H7F7;V$B5Q zuhQ=C_|r|ZifnFFQfY4pDvHMVsf>sa$O_U(wq za=>T%rLoQxymkbb>sYWD7=No;(X6rS(=~QWHQzN`r?LB%XzXF~ zo|w^CX9>@zG-@nF{>UPY#p&~J;I+TAzbT7vN)&1A-&Gp>3jFzvkfJ-SQ*^g^MfV18 z9+0ExAz)KDO!PRg=SfA1o?fiz+3cTRpylEFX4-Vd_=!a~7mIpo#YkX@k^!D=_-?>iXV3G0MR*mN` zV$CzZN9Jh!*d~pin5OYl!S~BHYP_OE;}?RrFD38F3XNaasqwlkJcm@O@q3ziE(z@a zXtu`xT&CciO+6a#o7VUcIDNc8<5NpD{(7~>->TR6do3EDqp#2C;|n;+R^6J&;P+eb zlHJ-_1Ky&Eyp@_bsETK);2l3&3>SfeoCwc3wG)m4F9DlQ{4xXX0zbJNUUE$_oCaQU zYpW)HSE`Ao&6;?iQ4^2SwjIv$Y`G?0nA1d0jVAiH@cdVuCSqwk4@O_>$$xvfCf=j3 zIe5>f9Djj2|68WXZFx2=6P~sUTx=1%@4FjWPoJyFgDNx${+~QDqRC_Cc}9)AQ{a?K ztKfMJ@Vz#8Ul06`^54M!YG`*o9IuYw+u3iN(&QgPnr!LPZqUcKk}kPMTHhRO}uK=px&cDxQ(9cxI2{JEJ}9 zwn*_kBWM%o7Dqge{7f)+%0x&T&Si;y$tyzh5G^_14EAh=HCB9Xx#4hCTwgOEmT?ytwVjpyuNxNA*e+%_&h_%DdpHa}t`@DV6A9JxZKG`(|mnw06l@d49pv$4n-P(XoNB?({8cFx8P~s1j zXnM&<9zqvQM;|TWne#fHKWG0-G}yEpCATg^S6zX=+Jw&9#q;eE;s)rrJIyMYwMEHY z(0#M{#SSMIk@iH>%|VC#F7$AZU%$A-s<$BM^{$BxI4$CAgC$Ck&K z$C}5S$DYTa$D+ri$EL@q$EwGy$F6FEvFtJJvF$PLvF82`CoA($JUBc3as zGoCx1L!L{XQ=VI%W1efCbDn#igPx0?lb)NNqn@jtv!1)2!|5x6InCVm98b-4&-wo_ z_dmx5*cNQYCO9r)8(~{vn_=7WZbe{AY*TDoY-4O|Ugg)e$2Q2e$TrEg$u?@RJ+N7} zUAAGiWwvRyZMJc?b+&o7eYSzNg|>;djkb}tmA09-owlL2rM9WIt+uhYwYIsoy|%%& z#kR?|&9>3D)wbET-L~Pj<;ys}ZM$u}ZT(4^f$g^surIJruy3%Bu&=PsupJv}?A7@`@pJ(4^A2^n)luxv8 zT!W9aue8s!@3arKFMT``_*VN^`&#?lyXFHQY+r1jd_!8`qwTBhv+cX>!|luM)9u^s zbM0gkIt3KJL`7$X=f7&90<7(?vIv5hH=EsQaYHNYIk9w`iBEV3C)@*&v7 z808(XiZRRUV3$`YOMqp@7X{eH7^e@cW6WdhV+`~pSf~w5WNc)NbT3%RnCXt<07Dr| z8B<+Z6<{o5En_ZYFJmxcF=MimW&@07tafBafZdGYjOC2!jO}&{DTVcn`M!~n!hptt zDNJZ=_yHJk27LGyn95qN z`)x3|vA8k0vH3`yQdr%X{RQ%$EmsQ58`B%x8{-@68}qLO`T-e7H2QwEl zC)<8r=4j?>(a+#)AHv0)p@b3Ajsm*9NQ!Tp}hP>KuQ4=234NhywaBl+fx zms56eZonnYDa|cUf;S#l5^zp)&jWG-E{YvaaZ@;|xoV2Dn!B3A{v9s+H#lt^ZflNf zu4~R~?)wBB_~GV&6Pp{~0!O|+J>bljz@3-Fq08aYr}qTh`q<8ZYnyZL2lqAy|8^wc zF_~!cN{13wYo6rF6Knt)YaP`dBag2;WGq84W za#o-vSW~dJV2xp~je+K1?IEo`rA4GP32PG@&?sI(tFUHa?ZO(yb7&duXc`ZpZCK-I zK{MdOOXd6)mJR zqv6f7Xh_5?tR=nD8E8xWXiVKp(VX%F?dgwbP`^iux*1K%+En#)pjDNlS)GQCWew{` zm4T*}M<3R>zBPm9g?{#bnSlnj2`y{`@d;~V*2t`ty?|zB?aUgQwKQvL*4D0=M{6q! zG&gH+Cy`%N_x1NMsGn05Xmx0Iga5~K$eYpdHlgKhC_>w-2sFMvw7%zC0`2cXG{C#j z0&hhVv^IEIN1zp+-GFvj8)%3}pd}LjAKaV1tTCcBhQC6m{EXPdY+9g2zD%rQv>{&=4tKou!2Af-D`26ji#fUE)buXBWCdq z+Nm|v3G(_^2HNUVXsp&+?}-H3Yi(KZ9-HOVTegKY82Nk$gLa3a&+gZufjwdx*l|t+ zTc@l4-;31$&&BG0t5E%~l&F7{SVrGc_5X$K$4U2-|2twFH&X9P;vE;wsQ(<|7^ilq zUn|xB!)59}kTn`PtmDY)<{eV3w^%6F+7B1B_HE)2uU2X8*dnd%BNp+O6 z@D1-Uaf|K}g`R3v=pnZ6E>!5|l?v5_6uNkeLg%r6TCGBAS7^yPh4P7C?3J(3PGt&h zMciS&L2KS8j`3y>@rxMoi*>{=HWI%eJvdGLVmKp4(Umt5So+B>tCt?(TXjb2C#p=73?O(I~bK)JR zN7Z*c@ror|)OP?ej6GSKk;yuW|K+Op6XF&#{JusUVmwQ|Jm=B-9I=VEW$OJyk9uz- zhH-7JdM}w$Zw2uU{%3D7ag4*W)w^H0dUtP8@AkwVwq&d4BVrTNwS2p~Q$3L>^?*Bj zp3YUz!^P^kt6V)d5x2OaPQkY?h+UjSY~x3ygGk>YeXB)1TM>u&Y^7GeSEbdjFV*Vt zC0f0PI7KIMibrF-JF1O$N3G!9QN_GFiggxelXo(0jxN>egL!w_c!+PoCe7!W?sFz-5O~r7bUg~aQ?FHKp5X)#_eZ_SVy>uyY3Vh{Dr>D@#LMuc7^-8Y*bh z(9yLTIKPi0yx% zMZ=Hb3p#T(yo$0gK4Jo2v5vZLFV^q}Wg7mhM#EpWu=cM@k)5N8;8!F2%_+h$B1aKl zFG*MARJOsmkqgMbl$gcU_^X?G6uA@sbzd#-PApgCDPr?4P`8h=2r-F7l_Kj4i1W`X z@?pCopHt_5nHtFmX=IlwjpSr$WWO$r98#u{LVWA-wXB20cmE7OeSR+MBil7{CBMII z<^7MeyStwELE^g~tI)_ZF^zOD*2vlhjf`&8$W)<5-eCLPDUHn0=5y+N)uz$yrZu{A zwnp)Hqu&FIEGF-V;E`iz`Mw00ey|$8Y9*=c0btcQE=5$85(;Y9JZRa;bM(N zDSJ7M=TXXeE(QGc!4{2uTBfltz>8ah6~CFS=(l;sWv_bH=yof5(5#|Mz?nsbik5&i z%T_45tWi<0f3&hw(dv+**zV}csG{}!HjQLnr6vlhHE|@r z$HE_eOe%wuoLQ;~?*GIExth3`cE6d?#ML1wWS*qTl%l&wrI_aw~XMMhVY_!OeDs*X;$T z+7~{RU#ZC-Q1-(-O&(pRNx=b27isb|c-gWpO`f}1lNaP`68=1S8EvkD*Il;&ugiho z6~psr|A!iQUlaVV6&~0PAMAk_a-3EChDbvlnq=%uCK@%lj=FD_EA@WkP1!s@NB^Hh zHTj=rO@6stv2F4c%YfJJ1TW35P;5^)?A~F;_N!6sAh>e@y!D7>iXA=AnKr_8;m&1n z+@BV}fxF?tg>d3d#j5D%vR1{eg4@=@p>J5H*sYW`G%0r1tYY^S@%$j|A0ht<`sm=; z;PcpD*DKbOrPx}^hbk2tqkgPQv6o{!qsVz}*aEMod{ZtwpR_q&vCsMag5&)UZ6F;T zBV$4F9j6uF1-)Q*^oAU?iharz-@icd@1ryDoIrdDTE&l+Dt>H+;>U*+KM8%IEKl*J zF|-MEh;y?Q|3x>NMJd_^eN~fpMVjJQ)7SO1xv^exY;61v+Wme;@up=w^GQDsat@EI zRQ$2^=p~EMO{n*mDFv|y&S6cf;sfkQvJ{UNDITN!REy&4!iuk_pN$KO^Z(-SBa7;$Idiu@$<|wrF14XDE@0W|TFhM0S@FiyD=Hhb3|gl=$8}&&fuV zI0!xI5HzC0)6kWub5x}g$D%I@{prUQN}No7Su@Y!(&o$%8WlR!x$DrZ%F(W-l(=ZA z5>+uJs_EnM&fxjpYigCa4(;g%+JGGrw{BGGz45Alz6UAi7t-&BIT>P(C4~|F^~pGLmX#>{Ad&Jf2l{?t48Ca-MV}w z;E{6N)7v#L&qwn`4JV(uW{fl$@cp5E{^ZI-V+9KOu)S)*nQsRFdN^V_* z4jEH&yJbATjdqz?j#i0Ixih-xx5Ma|1?ZX0JPVFy`W?#l?nd*>Rx)on8YtWOXqyKW z6GNy+C#C+O3uvc{(NHUtEF|wJw9%p{v4c6Ko^L-POUWPSD|ylqbXn3*3Y9#S-_!X$ zgZwkIm4q)O&zeQ!rQNyhN>;2y1LoMv>Gy(pB`*x271wY=S15T$OvyVr4)-qk z`}IoRm8<04)k@yer6d|n@(+~Vw_M5Q7A5}}QS$ydC0iCL`5^r~MBfirD%r}pJ<_1$ zqs>bGsa44~(qn8t-lXIc^-8vLoIi8EPnIb8RKAiO>7dzFWzzawWkO$rsv`{44p`hvbX&*PTXcQt~C*p`j#KWs}%nP5$ay(u|Tl zWh9Q#n@Or9g_P`LyN^EmILAKftjQ&DZfiK-HC(SXo0SYLA{CQp6RIIm7HT7PljtK9 z{yKTvc`kFE8j{<%Eq%DJRG*Zkj*%N2Gj(jAgU^dTQs>6D&(-Jbb5H$;wBSGazxhA< zzxqG>zxx{aTKJmy+V~pzTKSs!+W8v#TKby$+WH#%TKk&&+WQ{(UihB)-uNE*UiqH+ z-uWK-UizN;-ufQ&oR$6&pFRM&q2>c&q>cs&r#1+r3G`>bJ%m)bJ}y;bKG;?bKY~`Ho&&PHo>;R zHo~^THp8~VHpI5XHpRBZHpaHbHpjNdHV9i}o0PImwo$fKwpq4awqdqqwrRF)wsE#~ zwt2RFwt=>Vwu!clwvo1#QEaAdr)}s!cVJU{`L&I;t+ma4t}d{_w#Bx|w#~ND4^jR= zjZ(JTHr%${HvNvpfsMDVx6QZhw-2}~EAR=`oR@useZ>V+f$unXUEoXXQ|w#pV@~Ec z_Br-F_CfYV_DS|l_E87b2R_Tb%RbD$%s$P&%|6b)&OXn+4nDU9A;~VWG?JMmw z?K|y5?Mv6s1ip2uCGfRT>e%<%2iq5~E)RUOeYAb`lk{ufZ69u5Zl7-7ZXa)7Z=Y}9 ze?1t$SiqRT*uWUUSizX#-0T2D7)uyaoJ70h>Bm^Zn8VnEag@R$`xhvMO^i{DRg77T zU5sJCGR8D1Z1efoSjU*h*oXfY+W;0aCNefMMlx11W?Bn&GKRuFq%f7Sl`)pFmNA#H zmoZoa^^M7HAis8nQdsS>)&RTxvNOPP#&ka|PzvLnL?6a{N6!Zs&{)uzaKD-WBkq~4 z6lUD1Ex?dqNn^?swlv245UjZg%sE{cU{GUGV^U+&Auy`3sxhmvt1+yxtTAm1*wz@g z5v*&>YwT+bY%FX{d~sEPk&Ts?bp_bDbWwn%jj4^Tjj@fjjk))tjWM{fxG_1{+!#HD z)jtBW8@s;)hBuaf6-;kzKT4jlzA?YCzd694;R26R$K2rFw16v^GpuY5IE1-`ImHF^ zbuRs$$uY{x1MXoCawK&NssnDaFa4UUWEUvKUEnb0GAT}DZexz~E?mc)$K1yp$XsYG zoM;u?$Q;RB$(+gD>E7vpOPN#smb@C;nrmG|8*?vnFmo~PX^NX2xjEo!=4|F}=5Xe6 zJ7j+y%YYM_>wO64Gxys72jo0boG=16G)FX7?1VF#JGN5487|ov3b^G;IHtK~HK`Kr zX%1>GYEEiyYL05IYR>w-?tsIZ%bL@|ZOw60T=xSw?>lhcH{if8!-W|;DQ>)aW5AW4 zf-{>tn?v74nK`w&bqyTbT>F=B?sF>w4sI@PPJSe94=D_|x;guRF$y$`ly+ea<6X3jH_$Y!ZH%IEtYzDphqaH#&^N4w+>Iu3TXvw4SSzt+az4L5 zLqoBaa{NlPmDxaJvDT84h4xa71`|e$$wrf@M4RbAqe0iPR)ZgjMA+|JA80vGq3Jw= zZexwdT8}lKtI>BZMFX-HboLfSPD=|kB5Os5ECkw-H6&|E+Yxv85)JCJTC}F}Ky$M8 zG{G~m)}mGsljtN~@ffj)2iU(0&Fa>4G_3AG%es)d*0xSB4z#YL(7ZUt@P3Vf7M4Zd zXk*sMQd-$2G&5^wFQcK2qNRm$(AHK48e3}tnj0~S+ou8z?uzATa_DsDp}(C$A19Uu znq2{T`%%YQUKaV=E=J=kMe8d^^Fs@K<7*9YnBN|>z~}gVocssS0~^r{Z!Qcp#9yN& zp1(fO7Ef*rv_@-=2h5{EhLqAG(ImrP%%M@X2U_KNG|M>J;0WRu->3d}yEL#%j|RRGR{s~o zB0h?!|6R6U>rwwim-_ozgYnk}zT>@u?|2iRxSJTpEm^!niWtQu)cbkU*YA+ZQGemI z`WLsUKes~t*{r|VjyT2t5tI0sbrkfu_BCP_6Rg2l+o82D5Z7p5r?oB1wf6UOd;^2B zE9bTLSNxtctF@<+UrhOuQms9pQ)~Ak?y*xX-|!~>@HyYMn5|c6!$yUYc?u0xEA$ev zil=!eRBN$9_tYqK3+pbfo>u6REQKnFbDUnT(D8K&{jinzMGx_d81W0rx0)k}hH56Z_tM_B#5${Z^cU`G^iQV<~W~lesO7%VxR&NtAid&Yex299Q zzgkf5IYsLINu7F+390vx&FbAZk8gKZ@a=A55({1G`6n@pjq~bBW~pazv3g!4Zt>@3 zeEVWK-@d3;&u@ueTt+&dbVikWP9Xn>lpj!{o;|3K&0f7_3-69v$Gf9Cd3RI;?~W?x z-BGz({nWfxKN#ZOQFXjKirB?(*k8_?jGtC&bqVc$7}n|ohy0_iHEq zSE+%2EhZi~!`g{fg|nEOIcW+X(82d-n2X0UXHRKX_$=n%1!09RV_w$IDqO!p;d|y4 zet4zAPxAX$_Se$KIPr~jQH7@q6#lS@HCPK8+`3$YJ9cPrj|>gwEz{s3Z5ljkMuR67 zXz&bT4HcAKJgvd2vNd>PsRr+?)gZiU@K4y0XUKnvJ_a*27|Ye*>$G{dNQ3_>*5E?1 zhPEN?RHUIj3N%CvZRn6}4gF|~hDxS2bb44r=eBC-B5dRe;<>-g)zEF&&!#R7J&Y}V zk~UrQ3f}2dfn6n5yrGBK1lymaVRy03*s$TO81@)jzTajI7Z6W4I!(hruETC)zt6?? zSI%nq^5sf>>!_akcVo9(<~97pQVl=Xq2blo^r2D>$C@?#+C~k(ldItmD>VFhtA_uF zzu6u?12&3$w?dKpm5LnJsz^~-!TX$M6gd~aa}oaM@=Qgp%TnZ4eA`|8K0rL8ZC(+s zQKUPjNPo8?(H7pFSfj|sGDX1Bk&h|=ZW(w7f^77tAX4jjfu+ z8oi-RqqkRS^q!R(ZE4o%W38+O?c`aC4xXv#0+XQ8t{RF?i?LtMs#xK4+#i_OCLHecs9QC*Z52MXy_@=*^oI{T+4xkgMoJOB8*) zNYQ7C6-D!i_K}826X3>I7AyKDIPu*KMLz&P!Zo7*1ru*o$Fn#~!Lhj-Uo^)vI^f@Y za5Miqh)HySf0t?e^mOnr+rOahMVmE#S)<0U&eQnHZjINMX#Dpvjo(MTht_HQaq2wH z{tHDK@9ETd82mg|ukpmJ#$PMb_}|+#KC__lIWYKVEgJs<4zM-&e|xZgR;wm<->8Yb zvox`PktPl%ZwdV2=nhRB4~IB;RufAXG*O0GKgUhf#3(xqN^3OTWmRe120}tCC zew9_K$=zl&`5p4|(lz;g_}HP89m(&pnXHXismW93HF*Zy>Fk&$f3ZZ9jE~7*!?ms` z)8w`Avm53#SzoBh-@&z-!kWB)izXi_(&Q7>ntZxVlh4D`UZRiCB6uF{qs!oZaJF^y z{bmDvunAsBpK~pm{FL7X_P?rEEWKK>9m*BUf}`#R_uOkiv3=GlhTV-FT&vjOC5rth zQ?X-Xij_1dc1p2gr&H%=l%H3w*ahi|f$w6~(EcyW6?>6Bd#Tf3uh{UkVmvz*OR)Va`EM*z>@E8LM~h;!Q%b#0;j==; z7HIcngW_9vC=RBHXQCPG43FPEOYt1EgniHg_9q>buXsVO;y=t$@D3*GoIu@D+ML?1 z_)qHXY&UUISGuTtlA+H5RVe42X9;rIuf zwC~tR+ned~M=U`Uj6E{!II)iO1w#(ztjJ8vl9R4 zQesntXObzO+lW3li%v)Sd`5|dX`W>!|0}ew zW>GEr;1VUjGao!h{kY^f zGg>A6orDH?3OZw1rIM#jp=)xSGh28*oPNt`i%(Bh%<(*Vk&+kGp^wu4MO)BJmnivb zj#*9J-*CPwW|h1uljqs<(OgT=UYGIwJAK};0$rA4-%Pr-2F;fJJIKG2^50YMuEprQ zv}>aMA0mMUd_VoR6rvGxUJsM^2*>>s$8X!LJ3*b86kgUUdb`~ zj8Zn{FirA9jyJcQ#PR;MjKuLiDj?C%NA&SAc^`KxxjBPWPU2iP&nfxI zVp0u>V|+p%pDrR5ld4D@>r?VSjgdH~PdWZ)X(amiEc5H+ZSq`}s^hwDpcbM(2U&Y5%ffAD|tfAW9xfAoL#fA)X(HSo3YHSx9aHS)Fc zHS@LeHT1RgHTAXiHTJdkHTSjmJ@CEoJ@LKqJ@UQsJ@dWuJ@mcwJ@viyJ@&o!J@>u$ z81Pu|nDE%}81Y!~nDN;081h*1nDW^281q>3nDf~481z{5nDp5681-27nDyB881`89 znD*HA82|G^Fy=k>JqJ7&JSRLiJV!iNJZGBe$8)G@Hkea)Ee__G=bGo7=bq=_&9%Xt z^xX6u^<4Fw_1yIwUO{`$Y0quXanE(ndCz^@fb%)NZNj;nn{9+`g>8myhi!;$iEWB) zi*1Z;jctx?k8O}`k!_M~Q$bf?t8BAuyKKX3%W^vd+h!YQTemCwwtcpNJ93;I$^+YI z8;PwBz$)Hc<&m2s4^wYIsoy|%%&#kR?|&9>3D)wbCa*mc`*+j84<+jiS{ z+xizN>q-lJfPH~|f_=lI_z3$7`;6xLz=znE*r(XH*vH(I7x*0eo|=Nd7uhG-H`zzo zSJ`LTcb!EW`!f49`!@SH`#SqP`@SP7w=c9$v~RSJw6CHUc9VE2S`#vC~KC0hThRdaFFZSjJk$T;pIbV=!YeV=`kiV>DwmW41@Z zZpLu;Kjk(VNd!Ird#^T20M>Z;j(T&yf!0g8EyUi$t<+txnVS8iz z6xRP3%x~;(4qz^@4o+ZhV2)s}V9sFfU=Cp}VNP*B+`=5gT*I8h+@pqME@Dn{{-S`R zn5&qxn7bU^5pWrE8gm}k%%#k!%&p9^ z9*1k0bKM8`G6$TER1Sf?Hn-izFu{kn0JH?q_g*#8eq0OcH8Ut=^j@>pF zaPE8H-sa#pQf5wWZqE2farHCd?59xI9KMi#_}?jR|6Mq~xxP6++}|2NN(=Y^O<)>r zz#2gUtzZPrAcS^c4dEHKttnVrxCf2FT7xx*n#F+zalve$Nm!e(Mq#bu@Qs0XVGYAt z#!hG&+pb7y94W2CnuoQI*U&($g;*1@Hq!OAR`LkH&1fgTi=w3zqp5TR+KM%nb1Adt za#AxI%xs{=e7`QxX0Ruz_gidJpVDqp8qOxPoQ-HYub}Nj(RljNdb-h$o_8*B9IfagG$U(AOJ@Tu$(qt(ZD>q&f!35=pvVqrS^pzm@i|)5hiF{ypiR9t z9cWcU=vS-JuAb`*w5%31Eo)o#Xk6FvobBby(Z0~c&aDl!upie2+L$#mYh`=UU)Fjw zw4#)j7Q`?r1C8xvVh`Bzl=jxOfEKp|O%9!|32p8+G`ioS)mgK%c6V-DpyicJ2io2t zXnfZC_MnZmziq4qq8nNhd>3ud8et5ra4<_L?eLjtw8Su)Vr!r+T4TIw5t<_!q_xL0 zRs>q)QL{>ElX+;Adt{(lcBiz<@PE)lKWstMToP!T;}wC{Y0dLV^igY|_n?8+vu|zm zGBnT&&_K^Z13jfR&`=LwhNjwuwwjK{%657yn(J(!y}rZq;eYGUz&J6Ae&QB?E&lpD z7|j~EJEVbI&}y&Qtbt3jG;n^d2F@(dz)6J~IJ!gwhmgKU+Pz2v-&~^p{}rhJGuBqT zpRWElXZemd@rK>XsXYOVb=O>5s{O~o7ed;_D0Z(#5~s23xA1B2g(_`SPGYi}kW`?a>J ziEnss(c051`Gz-TN3ag#K;jlTF|Ey_d>djF|L#(VcxUMEMG8%^u40&dVnU&37Af>d zi9$^)6uPxZp=-K{Ul6CLploTHLg0=N{e=$9S14z(Lf;~7&Dx32bBJFMkJymG`U~O~ zVb)!6erulWAbvs2qOpSb#bUnSO`M~Wc*fZ+T60P{@rzuo;s37Lm;GH!v}QZjPkhm- zKJb1Y_`L5GVizL?>RU~`;+ZP-wa%;W9%37}5U;2qUU4z;igQ?lQI@H`V;j|1Ksi{k z5Bt=YQK;T8n$(zS~YcFmhesKl$E@b`1S?PSc zyMS+Zlef4*z2EInZ&s9VU(Bk97){UnnSA>qk8fWT@a>BweEXtEJ&zH?xUWz>w-KMX zmac`n{ zUar-*Q+_S6ic2Uvk61-nw^kp!Osfx}&OUqtft+nIBE=_}S+d|boY zi;!M=CGG3A7uBr2=-|5->sWh1-gU$-eob6rheG0i%(s0)tes%a{fPNk!hBoWtZ+q% z!WXlBRg=OuG4HUs;ro{>{MdrR&owCAo2_t!xj2=p@P;;p-(RHgr!B0pVxDhbqrvQr z8q6(W4OXWHk0kH-at)r!{6B~7$_fo$j_tU9PJ_4SYw+Gm4YsoX)OroRNO}Jf4Pp}p z*VSq8?KTbmvqyuUuhUT4jE25BuOT$|p?$G;2V=Jii7}i&I*q(??B0dgwBN)ubRBVr z`W6k{Q>&qemTKtF*uUp#+ZW-xK~21SkGu`Ee{YKQYz^4cVhwMLjm^Ry^K8ZNe#9UO z8nC~)#3n)-URs7--l*Y=smpZb)S6a0Q$tC5Ax8r^z1-@cA&bXV}gcfkk;ENJx5Wg0yS zY;eLBjh?zxqi2B+&Y#ifCHVguGZrnNtUSkdH-&?A&2NyKpdzLA>Zx7F%fbR~kSM+G|O2B+&tvsJXy`O{qE~-=XGO*k=V8t83 zg}1L%^zKeY@1IumPvkuX{(F9zqO0k9pi$AWHbs-&ioQ;pw_}RFKc(o$r2l{c{}O%1E95}dH z`@4nm7*peqyO6PNm+mkS2%=OoIg(hn2 zG;vdlCT{Q6#9eTPX4*U))5PQJHSzS6Cb~$gBbw-^?MR0vV)XlJttQ@F1{W!SlVob* zqiHxwmx6disU~-T%Vb3~xjQ`PyKtX8IL$%us>2F3Sval9$XwO+Ex4YYS`gsa#DySEI=nLz?W% z&}5jlqb-_@(eBkOO})m% zD;3LMq1X@DUqb#-rHU296Hm>kB_-lW)t6^dQ5Sh3%1R;-4+>uVIN zgV)|RtJv>b6uTGRdH)>mRi^IafyebaA3;D7r}|)w(BzB$Q=Kz z&5F$|Q|fz_AH!4U$@}j*o*RUxr`IUHJ-q!}aPRD;ithoBXO6|cw?**-3zYgsCSyKc z*s1t2)H{A!@slV!mHI!0v!4x*uYkv20Ehn-zn7A?B3tpR(E+ZT&+}f>pLxbXX zRV)67BE=ubQ2gO3#oJmG|8s@n&*Ugr|3ICdC5neQ#$Y2F#p18y4^?Ov=mP7>`#XKV z)28?)j`zK!QDPVLl-=0g6U}6A^1s)h)S8FI z=}Ht7DfPY7!Zsy}VoDTmR^rFmJcGJKiBrpzI0NnFXH`m^Q-g-H5-o=Wj!s;{aV{hO zigG2cE>Yq-w5A)-W$I{;jZ5&nL86g1O&qV8<2*p#!wpLOiT%gtm3XpKiDzj07s_7f zP~s)}=$TO>MBm{|G^$p#sys9+$`bs(%=!H-R|)=qVgvus3dZdQrS zMM`{Duf)ICv6f+x5?@v*nT9sEO-#w{($Un=+IC#7WLBe+-|kRyH}tqYrj^W@M~_3d z`ySfc{@F?%n4{$4MM}aAQt!=QvY_OV=y^xc&oMn{eN8+AUCn!Aij_Q>IzOQg^sM9= zu1oIX}hrq-I?vT>(Qhap-p$8RdZfojO6?|vteIN$x# zNT-oF&i)IUI-rPDM+%X)Xeyuc&0kJxB&{O7MWWt;*(Cngfya_gA#n}*U;C}*VNb6*VxzE*WB0M_rUkU_r&+c_sI9k_ssXs_t5v!_tf{+ z_t^K^_uTj1W58paz1ltDN2-^zV4BHOd z5Ze;l6x$Zt7~2}#9NQk-pxgw?`o_Ge3^Y(9lp&z&c4n*&%Vz-(7w<<(Z10>(!SC@)4tO_)V|a{wV3wy zvG%n~ssrC^AACS0@X32~Jo{+-YWr;aZu@Zia(uddd&nBv8T0Amn=^_~Os8T&n6pcED~CNwrQMl@EeBj4E37}8kMn9|tN z7}HqOnA6zP7__)Bz@$s)(-_rQ)tEJ>Ho&l1@y#5By**2Qa8Op=1?gv zWlm*o^)?*qb-0!}SCn6KuwL^2O8GNzE^{<~jc_+}ICD93I&(X7ykBGloX_0P z9MD|QoY36R9MN3Sobg+5M>u5a+u#3zQ<_`8Pcqju=QQ^;2OWTmnvbbBXEt|+Lz_#dIJLR8IkvgB zIk&lY2o7#8ZchGqd%)4n)y>(>-Ob^DL;Z``H@7#(H`h1kH}|&&U@gF!fVBZ@1lyob zeDSq*Fw5`TXbG>ODa6qhB4`ZO8mu`yT^nc+)*|kr9yTteQCx{uVa?)vvMxk!y1RR4r?B}7NUW~&_d8#&_q(&h&7UpXeF=mJC1fTK)$t`*>EuGRC)!$rvKEzF5olB2r0!P4FVL)p z=e7hI)_S(BX$_-o_2vg!S38>5gJ@s(R0Uet^(z8x%o^FbXk|+&|8XeL&fkyW{THT-X0`2Y|G`xDWyz6?<_NaRy8ejRAK=V7n z8X)Ol(!N=NHfW8IF*o=*dg4EcQ@o9aXf4s2Vt-MfF+N3EEBV$QZ%2c)7I``QmDD*0 z4f0g9%Ht_xPNg(VYnj>8DQz?SRc4@dzMm6lpHpa{5wy@=G|}f81C8|lDm2rnKs&t( zUG(D3XsV@wwpuc)l-7DMn(IE{K!e?Z_`_G#8u+xGZv#*9o|`njgOSHKgNa3~uGGM@ zwHo*n@r(O9HE>6l27VjSz~!V1qZ&Ay^%_4Ru5ldshqr6sfF=#>wUY06SMVM0629Y| z!#kuFc!yMscSyDJ4yh{MAw_KCIr1J0sUPgre3gyjf#$ zZ3f@)Cf@P$u-2ZDt+ger!8o!@Yrntn|F}B;_&lfk|7Qf56&Xbl)Wj4;RS;yB7a2iD zW?@AUlx0+91Vu(xK~MzMg=H3GbaVvK{7&=ZPI4#r&rm*LF+og65Y}0c@qNDT%U|C= z&f~uBbDeXpU+28vuh;8+&UL8^+uilUN_C|ZllbqnIzO7P&VQ7sbF@;Op$2ufgo$5F zsq-=34fO!~su!vA2I3o66shxqV&WH+J&ANQ+XoTj$YS44j$h0pev!ueqlj6&Nz5Wf zETX4^_yzBUdWx7v-K0A1A&zlNIq?f(7?-l<;+#fxoVr9EdBi&oYgNa7#5d;B#7$d=PgkC zVbyA%zgg|O@P4Unh)Mi+v)Vo+jxk=Tws=Twy~HP8Dp1>BSYz>rCbiu)!MD3J`F3}n z+DchxaaOh33hLE%bSvM!Af5r2Xq&TzZ(pSG?F-U-q}ND8#3x#3)cOo@jruXQ-WOJD z)q1sFvqr7USbK3!iCPO6@a`yL9*2Z@cNFi9qJOl0FIz32PpV~-SOqq@B}}XWJl^sw zv5G&jeP63us>uH>Ycwuhqn5K5sO4nxa*O$H_ZZ*pUdng7$N6seQf>H@xWqfeB3=n; zLuipUyi82u>2z(VTc!x{j3$@`f4JjdSjka{y2%X@`cTgCsy#3xS3R`02+S$nlby`?4U zy^?st4a7QEEm!Zolz(K7dK;Ih_xXDDwr^JN;C%ItVkgFH)%yYMd_Jw-Z?e?){e1P! zU8cT$tJH@*?K`STefeSaoxVwZ=ZvZE;&JsYCoxB;Zxwb7oY7a;tiC53)b|`Qh_>bG z3l*u4ao_i5n))WG_bL0n!k%ukhJKede|(er-zel;N3H7rBuo7>)e3EeFWI3$q1_u5 z+7ExRknN-KHTmNTojzZovr84iKZSlXrcebwBnHtzPO#=%iHGpjy$ivT_T%&B zXIS7kJDj#&sPGRu6`otDaMqZ@2b3y&=(xg1^LqmS zPbpUTOt9J#%9YkCTsF%*l%>o^0q?Dvqww97d$5W5Dl-Z{nXmA(RSLgEdmWSuEl@bh z{!QSyamv#V!&B1=|A%`21DkFG#@wM$!@Dv^X73UWA3!>k?W2k`{F8hQ|7@X#e?Cvc zC9@h{I;r7hVA|yk8m<77R_Pd zu5?BEDK|W>NRs1X<09kJio8b~|Eg8w(@90XSg6P>JZ`J?O1-0LCpg}&Ma)ry)9u&D zoV6%(*QONBfnOeH{s&h)8SYqAtms)KiY_5tNLognEAkaBXaDtCidLp6`g^$Logqbu znM5CgCq6>GKW8e6oryj}UQ?-}8|Et7kyNy|TG1iuMmH(?N~NM>9P6!4Mc*rC4&S7r zpOh1apv^BS_iZ|y7~Z@+e0!&IxH6o5&tk>)g^M2mXFj-scOfJdJ1SeT}f@coe5_zfn%RLqu50Y6L z6TRc7or)KruM{p8LgzdA@SkRy17fdLA$$Z5xr`#3Qim#xZYbIImfPQi_?cZ9Y z_-)G+zY{Iyo*KpPU!(X#9QzNXKT;q37=N->@uw>lf0lC3Q~sqK#arew2XE}{%5V3f3iP17`QTVP!q9+S>_2-_sAM0jt--t zr77_fbg+E(pHz#+MjNM&D^WzbV(R@e8$FKw=hFUC>RnW##3jW_Tvn(=S)LMCE=1GI zLfcD6K!tx*jJ7AcIP@Ju!X7j&)h%8-0Ib6iMeQs<7kUDO8klA z{J96+aj6oG)98@Ye}*>KQ|@`r>SM_}Zd%Da z^y{CXXP;1{(7Thf1E_ zr(_Z5^Ydj&p24-8F{9*}?MfC??-#WDi_J=&^`Mf!%qRU*$&$O31h*#7rrtRm?;P6r z)ior}cL~QkH;u&c&Ye>7yi(GLlIPRT`D;nEz4R;+_hIQ5N?uSvsv#wnyfBk=8i_V8 ze33M+YO+?pJVD=IcJ}{uff-nx+bp8*XV2YHT&9q4}33tPke8Dk9@Cu&wTHE4}C9vPknEF zkA1Iw&wcOx4E!woO#E#8jQp(p%>3;94E-$qO#N*AjQyC@$2izCjC)_vON8DH3 zXWVz(huoLkr`)&P$K2Q4=iK+)2i+IlC*3#QN8MN5XWe)ATpjdf_i6WS_i^`i_j&hy zj{%Pbj|qZMki_ZTpYeZ(ENaPuYI^0Q&;_1p5a2 z2>S~A419-uNXnPkr`WgH$Jp1{=S<>z?1Su!?33)9?4#_f?6d5<`f3AT)=r*%n|+*p zoqe8t-;?aOFSJjzZ?uoJue8s!@4St2_NDfz_O14@_O; z_TBd3_T~2Ji&qCe-oD;G-@e}%z*xYTz}R5>Im98e^M3EL8CA4KR$3kTKDi?rZcuP#xvG4<}>yi z0|UMS7BnUtS`=W!j%0utn@R%=**G3xN@Gi7Ok+)BPGe7FP-9VJQe#tNRAbd8Bx6@& zSYz3I_8Z$8;~omuJun(z-@Q1tvG7jp%iz4g$i~Vk%xvsz3~eku4yHD?HpU(XYa4SL zdmDori~qGDz~;v2#_GoG#_p>qck@Jm>5c7w4aUE?IKceBoC!F9xqvyrv9xE7V6Ko& zS#t+-2y+Q@iml)wU&BMTz%@RGbNmzTVGd$0VoovwH!(*sS21UK4({@FX2504Y0PcR zam;nhc`D#O=0N5`7j6!?Q866JT*;j2ILetrnM>_I8*nRgEORY5m$_GpgPDtA_fp)< z9BmY?X3l2r)((d=mwN_IXKrVXXRc??R}FtN2dt=2iW8a}nj@MknlqX^{)GKU6)D9j z4}klbW14I30Oy2znuDges5z;*sX3~-syVBhdy*X;MC^UbK%%OpglNu>fKW2;QxY)o0FTHC)gf@tDCdG zoEvcXCtCwfZ*G4V9N%2uoPPz}-x|OL)UzgFZNM7Av4w$VaA0wuA?!+?H3hTsZ6FmO~#;X*-K4w}5*4vX8zr@B=iO@1YB!357l;y@x(zttf$J#J5IL8j`i7r_hw@ z(Uu;p4Ya0P(44G2S%b0`bvByR>D0-eM60R`G^_n)0u5`2qLij(ZL9wiw6gcmy52zZ zilcpnzSF{fe*Lc0!Az zFZO*^jz-rQXmxL**(DYR8eUrkn%?R_+p9z4v(~o?-Ot+J@(Hv+k~KkVgVqR-Y7I2Q zeX9Zu5iPOz8+69c(Il-gj-xe>qB#yO2sFs&8v{-95wyvB(I{7;Ro;MRc_rFqDdntX zTGKoMZF6yRpmpv=JDCM34YcP==672YeHZQY_4+_7-B=!Ir##=3mRgIZdV3BUYjvQt zUcwyjbI@K-TN7xpN2aj`18sG$QuXYF_PRCih5GL%ZTx6j8z(Zfann3)3@^~ePS#R1 zu?C~DKpX3dv~e}tRpeE$uZ%KF=kOixEquqDy2sGQA;d5CEz`zbh>2`Bhj&Pg^A0KA z0rf61h}Y+dYl&c4Jlz?EIw5WkoiBYrWf&bNtEyppTV0b&xZ%hdVoa^e@P z$9RBLT}}LgbVUX63-XKEpNd})w>XG;S!v(BKZ^LnmNmpLmJz?mQAdh-L0sbQW_A3oOdaLKGcF0K<7~cm;i^ zJwd#J`4H{R#4DaES9=|?iF=CG{<~JSmuIQ{l9g&NA=YuqBDEh&?BbB5+V`2K_RKQ1 zZ$*4!%e2})SfI8yh)qO@XLNn*spmcKNp*DfXgw}3d{ zqUoYa)Rt7wz)JOuvTr=f_h=Y*TQb%2?K<`TAXmM+CDr?*GW8z1S-r=W ztG8fEy~X^V&+o5G)q71;y|-W=?!pEDxAoZ|oGSFN-+Cf!LbGJ>U?TA_=v_gCTrt{YdVYJoy`mnpP{eUDM@uhb#N7V28A&=6%t zI~Dr-m_qO4E2cA*`UZA7zHi5T4a_6`s89n7`8}GHPu$|PYz>sm)4+x48u;~u27cS4 zft%N9;PwjEOcWE(pRa)@rikx15$7kbXORYmr-}bJYT&IB4SbNHf$8-c_^L#MTW`_e z4*2HX7in-lzWiYFj>Lce1mAr!zPh+fgXd0a@X}HZE>CLk`aBKZN<8DPIU0PhLW6&d zYVa@k?dK{r*ur)%{(gkA9BXi#djFiS!B3ZHaHdv6-vfhezga_b=kR@r1-!GeNJED& zW9=tcMAfVXUBg_8dgfF#fJsOz*uH_hsv0m$IdO@l8v0{_h8l@){4E1)Gp?cTCJhZ$ zX($O!d1Ef`*Cg*l>ixS!L$kyO(!ohPELV89c?$2-s_=nesl#U#&aG2eV6#)nJF7t9 z^P&o0TCDJu{8sS468v?0uEO^=D7-dP;m2s>DfT_Tka;Op3U37G;Y-5Fa)rleFjDHT)=e^NEGvSK51l@@@6O9G&4s%-NYHCb1seyG+BI z=Mj?#gNLc}Z5H?#yuIU6ViMr&y}{-O5TjVQUL!eR>f@U=auS&Pv?h(5HD4p=f~PN@ zqmi<5ja)shksER~atnF4gTwF5V~qsce}s{lYnAitY@T+heYx`z}}X0Oq1C zgwri9R5Z7W_4S>K7R)Hh9G~bfmMZ$IYDF(t&)l`JqQBV;@1yMXneagP+bTF-4e7od zMc0rXU8raS`A^aIddj^pt!PV9(XJ-u&(Y2>bJ!A;CuSaflXI9@t>`~F?o_&>pYm(*PFJjoGPkc*>~8ke!j~Ujq!<`9_UCHF8mar& zuwu_mD)tf_y=@Nsem*=u8@^Au80nP-iv1sHob!AqU9tDKDE8sFVxM*@wxwRNFFChw ziWJ{^p5oh~UCcpy_~CNJcSC>JbFO-+Qg~3 z%-clMIIB?cbCQampRf2u{9i^JWoQ;xr8D=lj5(lD#cx8#xP`jl+xVSq-&2mB(u%G! zqtv%i>-pcnaT+%%{?|0c|CX)z3mo%h>bFs^t5&J+uMRXTK1{tB$H7L&U*rE9O^Ux& zr}#Uo6@R~iIj<#(6HktRmZ|ub&5F;kf3^zU2)!vCjcL10=t--U*eM@<37u)TMkV$v zQR*E^`=L3Z!6X($(WaI%FLxTPsvOOVa>voePxw`Z5(Se=6fRNX^d2S7M58*3_Rc2n z+&uKKHA-B>_N5D%CrsJjv?{S2ovWO7uU&?wRpViaU13CC{W@qj<a@QM|Unqht5QgZbF~t7{#xnTch*-atb}0 zedlm4ORhls{wJFFIq2kJv~$ksqP0qva{iZ`rR1egDS26%l9zMLU)L&G_NJ0o%v18p z5+#@4r{qz+~a`VC6n(4*wasFF8wPB-y;Gyf~O zXTPKETUIN1>q;f7&QtRDxk|3unY3BS+o)H)O3B+#R`QN?QlFA{Rw-GtnDnlacRi@& z-A9u+pL@t(y&GwrlK0XE94~oa5owc>_n$%He=X-$%YAqti$wVcCY5~fMA8$a86_X$ zI1jBQ{X@w$`;aanJxZdDhqoee+=tI0Rg%bmcms*+d3b`v^*{VM>6`D8x1HyGF5|MU zKr&1pHu4GP7Ka9b7TjPq=TmkGQY6&$#cn54kV7Pq}ZokGZe8&$;ip54tb9Pr7fqkGikA z&${or54$hBPrGlskGrqC&%5t?40tSfOn7W~jCibg%y{g040$YhOnGd1jCrhi%z5m2 z40mKtS`?dkt0^5X?ZLp27t+35V#YC_n zwk5VHwk@_Xwl%gnwmr5%wner{woSHCwpF%Swq3Siwq>?ywr#d?wsp37wtcpNwuQEd zwvD!tww1P-ww<=2wxzbIwyn0YwzamocN7FR*tXa<*|ym>+P2y@+qU~RoS$vEZMto{ zZM<#0ZN6>4eZc9QuYH1jgMEa3g?)y7hkb~BiG7NFi+zlJ&E8uA-(w$SU$j$x;G68D z?5oo7f%vYJ53?_`PqS~ckF&3{&$I8d5410|Pqc3w#z)#$_Tp3RJMBa5OYKwbTkT`* zYwdIGd+TxoUwl9H?%}_Ew0*UGwtcsKxP7^OdRblIbM0gMHV35*Sl5ss&B zD!#!njUkLBj46yQj4_Ngj5&-wj6uL6#w00hVvJ&}V$AYB*u@yeSjL#f*v1$qOfu#% z_Av%B7BVI>HZn#sRx)NXb~1)CmNKR?wz@ezz*@#!#$Lu?#$v`~XH(Z0%~}?GGK3LqC+}PY0-B^79%-+2z!0^WM#`MPae*)th z>({bv?0*{^z+Awbz}&zb!Cc`yID@%E5gfu?!kog~!W_d~!<@t1!yIHsezz--xrsSS zimRBjn7fR_Va#RBX@=l7J#d^>^38e7eV(MexsW;0y?IJ;q+8fFXEJv(hccHkr!u!P z$2ut|;9TZj=3wSx=49q(J1$LeHFLHUcl#6$XD&AZr!%)p!tsXTdcE+vHn^WTpt+zq z;iGWF2gx^AG-ot-ye6s?mo%p|w>*pTrzHcZ|m3u_oHXc^Wt{(`pg zN4BkX+%+fAKCaJ03)vKCBGyKVmj_yjH4|$m)==h$0!?LyIy4sMkA8*jGTj_#FK-cx zcon^77)_=JZDzwtv>Nt3!GCKv)^KjG3^bkJhS7M?axO&cDM9l&jq>@qfflr|Akc>9 z6$V-nno;m=Z}g@2XVH|}Xlz%r|FSuO_I74npv4`R8EA9aXmootq1jEK-Jt<89{WB)1AGq+a13oPhQ?>D z&zj$QYk*CG7I+W;s~Q50&|2X|lrN#)sSD8(Gtd+#0&US6;}4glG{@erCea|#DJRh) ztw~y&?5hm4%4g9m8_+eaVcxYa&@`{vq?E>a{#2lO77)ic7ERMy=#R<*ZS)6)DXp}} zn(5|>KtmlR4lz&_Xsgy(t+lRQhxR%ZXt35|!D&6`htzZCw0a8WswX#3J^1XN`Dn3o zSF0z3wG*?%AErCCagwzaW2~!)hPAPW-)8dGvG$_AK^to;wXvG`#7bfr%NJ^6X*%za z8s{BStiQl72Jeu{)W+SC+PFROh;N8fd^W4@_nXxH1~H6Srn-CBZZ1*x)6?qy<7#!^ zpQY|qt?H()cmF2DH!zCTT^v<+L4mrD;r}75$=GMTx_6$Z?rn%U%n-krDpJ?m#3x?m zT~dR^>S|l3uJyzw8uHZjP_?@5V9mu%>FT&8`SZ}65=HoJEU&URO|IgwU(_`EBbe95%G+lbgK1m_U|`etvgdbo%q8S znQHl9y;|NVQp?D=S~{1j<+(Jq{JBOg56tDe-F1Aoo8QZ7`EK_dwG>vV<=82;9I{j` zSv_jmk#|FVTcZuro3-KHd~JA@_(f<^8#d&y_M(!t7oFO0cRFh?3cg!=(ad)**q=X- zwHK8C(Q<9rrCJ-(*Q@90EyVv?)w8jZcwm8g{*UYdXiUBTU8KIPuq8VX2i+r!_t0TC4#)N!H^w)1=JKtb0`)CjslHz~st-Tg_q#3X zyDL+DYp^+gX5TZE2mAK*P$oip9sBhTX^QRtE?56n*u@>Nm-BMfk6rCQEKU8n*wT|) z)qh5f`p;$mvW4njK|J@yX4bJ)Vn=h-|Hx+SY6JGQm~|5~*ju&}wDSgQC?>1a|LHvJ zGB({lV8@U`dtkr!k1BK+zq$Mee}sPCpwN;z3SF{Nq2<{88}b$UJ$C<|EefrrT*DgP zH%R`=3l!Q|rqJ+ugy;_M%rqk(tE zSTj*iyq`MX&L#d&yn+5bn3bc!1MsVdS8MQC{PBtS)}o~vJbMf4A=&>M%3gz?1+xy` z-l#!rSL!{GPo(pGtCbpTp3q=-u?B~lG&qV6f3r-3?{#YM<4g^HK|9~B(@+MOVCRJz z+N(@M+2DsmlNvfET|@cgoeB;)Yo&(H2U9F-*3eZU4P8IRT2SiLY~ejlq(@0lPBGVl zJY$t^>JL#iNn3A#L*6ab&_{V1!bc3vPAI(1dWCmfsqpT(3eQhdctNYeN0cdiT(-g| zg0twu;gb0ZFAXVt*)oM!Q0Dqtg>Pm5olOeY<|tg(qwwS4w7*jK#YGCYH!9peSK$bC zd2ZqH0);0VSStbM`cIL<{{tUxGp*qr7ioC6l^Wg$EcoLs8a^BxdCXD;-?;`qp1DcG zznal->0Aw$QMSB5!z+t5Tm>GjA=Q%V$~4?is^N9u$0qi-Qm!Y9^}}fzPE4?#n6}@p zWlj#|KFtBwlK)KzoD1gNu>jmV3;t~e2QSmep~MMt*v50*##4s*y_y zHS(KH8u{%KjjSBkNEN?#hBb13zDCwk=dpPj`Ad~X)=z4rnY@lVjr0+-7-4&KHS>~^ z8kwNX2gMrsWQ~GvNX#gbo~_6aiWJ$oQjtAs6`2obI1nyz*tjA`!5i`@!?{FGPgCR< zY%hVQTr{P~!yCg3L@R4DSv97VP?DDpKN3``o$C{UE~8{L)Pz2H_qf|D&+r|98x6g{d^ z(c>o+Juy$w!a7CIm{Rm?%ALPj(Tm3vEz44LMJem+>se>NNzva^=T7S1OWs4(iatU+ ze-1197utAsM$s2Dl={|U_dG>I)E(hB3FmvAdVil)^qomXKS(P2ag(C|Uc;J*a@It^ zRkwk|f}vxXa8deEEGt*B{ottw!etL#s@Re6)7)moPGGwLE_`aeVrS+thcB!c94&Sc zbuU}4*cDs8i$hGpkttin{yQjlPlI9)WGJ>4KK#cX#U7u_{J}ECo~6yES;aOKDArM} z7&s_4KpT+-iX}@Fd#y^bH`gilc381V+WasLUQgXE^Wpi6;QI^V{W*$fkao!CeGc;# z-+d1AAJGctPbhvsSn-3=4i2wWJf~RkW7+?cEsCGmtoSM973C>jJfk?AJ$~L&;tv^$ zUyA-vwgjz$Hhx>H`1Ks)rXIy_rT%T3cuz&C;;Y9Le}FO%^Z(HT#UG>2leGPGwc^jB zO*G{z-dxYSmZs59*xy@&o|062WUk`z62)KP|LY;e-(>&WS&F|$nGcpJ{!z8!VA=S; zn-%|WNU85Qd^@hh)}-ww(00&uc1rRt5457)*DJACtrGiIDzX0(B@WC{;*dE?97egt z=s`!9qaWpd7jtM;;%EFnHJiD$)Gww^3ERJ-o%5F}aUt5#B{gVN^U$iOd(}d;E81uN zOyUOq-#k}|TdI}#{j?Idmnc!wiT;&^4n~^~hLl)Ky+=xws9(o?;VC7aq#n;V@z;7K z)`!v7W|eq}@*DD%Xro+bg%TUrFejO|!GVczrxFp)CC=|C=emh@{;xraH*3)LR-*AO zQR2NkCH~2AHg9HLH03^7O>BX4*^-4$IHtsx^-9bZDe=uLTH<;%MfB5czu{drkMV9B z^u--tL4Pbmhn(TPI5oU0CxdtA+>KV5j%Ila+T|AJe4~{fIPzUA0Uh+P>0qw-k&S4e z=$N_3F}ED;H*b(R=UXYMJhamlXs8_HlqmDnX`^V*U_N_saiG1HRG`OFX9>qTkF=C@ zVKw?Ky5}X-T~-okyJbH@?@dGZ9Vb43R(dsce#^dV_dzTE2fFd2XvpWGC+{5S%D=k; zy%~MDsv8aZLiA{^`*zyClXhyTZw>pN9`tRl{l0ee??q_h4QS!Y$Y3L|8dGc!TB_z4gZC*Pm%ZZY9-e_r{rJ5N_j`Z#{yS4#GC4k7LXm?=5159u)_2RZH_ z`Gf2qqTJA!lHpv^qomK394;hrt-~KGIdT~38d4K!O3BENN#~Kc=aFYf94kVd=(Z&8 zMRYNV>yMuC|0U;npUY6zby9WRhTC$RwC!W0j&%*c9NXuRIv38#=axE0&eiAabN4l* zu7zvzwfP!-t-fYoyYGSTh3|>)jqj1~mG7DFo$sOVrSGZlt?#k#wePv_y`O=fg`bI^ zjh~U9m7kfPou8qfrJt#vt)H=GM@3C(i zU|V3DVB6sJB(@a`*95lXz>&a~*rwRF*v8n_*yh;w*aq1a*(TXG*+y;qO<=RIUAAE< zTV|VP+h!a08Me+g&$iDt(6-Pv(YDbx(zenz)3(z#)V9<%HO#MVtZl7ru5GVvux+tz zvTd_%^k3@(n{C@|8*W=}n{L}~8*f{0n_trz_yGF?`vm(2`w06A`waUI`w;sQ`xN^Y z`xyHg`yBfo`yl%w`y_F$saQvD;Ir(z?8EHK?9=Sq?Bnd~?DOpV>;vr!?Gy2h@o!0A zZwh>-eW!h>eW`t_eXD(}eXV`2eXo76eX)JAeY1VEeYJgd6TaI%+`imC-M-yE{$bj; z&$sV41~3*dCNMTIMle>mDlNbcmr}=A!kEI?!Wg3{FTflpHU}8QSj3pb*u)scSjCvd z*u@yeSjL#f*anPatdqh#TfjcXK*mDGL~nzQjFF6$jG2s`jG>IBjH!&RjIo|=4ltLo zmob>Jm@%2LnK7EN+RaOeYtW|S8pd+QbjEhZcqQyN<~y}Mz z_g=8?ojCy(HYTp94=}Q^vN5x<^VtOfmM*Ldu=Vk@VXSS;y?}O%!HvatrJS+3F*;b? zm_3Evjp2>uC&2V?<^&kuSbvahV}El1bAe~!1m*_j2<8gr4CW5z5I4gMD%J(u!W_d~ zV+m!n1hoH^Z&1u2ea zu9xC`pThmj0pErTniHBEhT(!ea7A-QbH{aXNOMVZN^{E^QWeRZvz+~9lrtAKCp9-c zrB%Uqzn2Ew)g0Db)|_@n%EEEYbyJ+z+;@`gak#KKaRhE`j@%AcHfR1T-1+f5rMR>? zwYl}J*#XyHk*4rv%ar2aCG!JLZf z7(xrMCh+ojpbY~>qz;Yb{_ixBN`9|t2(*)nYS2<< z0!_u*%CQRqt;L#47TSw77_=B`GO6!-f7GCqRx_4~c0&vzR1s)7P3S^Tp$q*9jpw2H zf#!27y3lp#IagW}Dh;%upR;YP$eNM0qXW>8_R0t}rESA0jVbhbRiHWjV}77P4HJLZ zxEyV2(Rbg(K&x6?h;~K1!Wz~M1%ak@3EI|@xoBPM(7bX3?aLaNwXi+-W&DQHbJhRl zxrtkXDSO`zH3js+Ut z{__J(Z-){zzEZTlax}m1v_ETr)&i{wzPvop2(1;aK{LDy?eKRqXo<-{Q(Q{fUl7AM z8LjcS@j!dbroBC~0!nlcC zUonuOjjfZ~2#(m;(5Q`Th+ou{Xk#V&ub$=|Qp7fhGjBXSk9SC=Y2)HnZTxYWHttE? z9YWgpJ>C!XpSkMZOnhRTm_=fGJm*^l?@m#*T9 z8e$oDj;rhD40WwosIH5NQy#RG9oInoqM7&wb#_S-zZfTeF+u!--?xcHypkk- z(MkM*bs5jr6TcvC@c=Q7YGM{Q5VQEr4Bx+?%$dYGPAnsSu}GZ^p=1%I4eMC49S^*vIZoYXAP2+PqgYe%8F7jCN_ltGBDD_kPN>!p?~bbC z-BGNwxGzJkx3;SFw-su=bb(q+h+~}0b}q4wgL>4u*AlhPnO4g;e7jqp(W4E2 zPuGTMA!{#)XFM0;yBD*py&#T(zus_dE#JK$mT~q3YcD95yMVY~4e`HX^=u)Q_w6`q zC)TKUcgETNjJv~H)q7kS-=WFmdo<1Jy`WgVWz*`dSgqcw`Rcugyg%ft_ldB2pDR>v z8@~h8O)|E}b69iLpx%GaVGUM=`ndOfyXLBIey#cr*{r@}^3-?YYW4k`{ByAr%di($ z*Q*b_(|1RP`W~QcJ@)A7HR^k@Q+-|I>I-jC-zIF&+u7>-Fh_li(f%}S;17r|?1l~7 zk8R@1{l{ScPRv&S&*!Qi|J#4bxcaYZR{zR1>c4Hd`tQrfPR>#Pli0!M>ad?nv7_@8 zywhp5`X}><6=O%gT#en$!TxT-4woyGolb0mI5WN`bW*EAXXIhGI~BT=->djtxlo~1 zjS9YlU8&IH#3k0}E3~0rp`I-YMX=?st>JxxlL~!E*)6pBO|u3vh?VbxKiQku!-3Tr zID)+6i8uUgmNkI$G;kjNW*NR`1^ZVn)4-~74bV>q){+|7erCA_n(No$L_F5gS*Xsw`QVRgE@JunZSphvR;F~Sfau6@wfQ#!4>%6 z8;Dcm!pIe4#{x?VEW2BjsaJzH3#$_pPY&VU`B}P5Ier4Q*Sb zp`Do1ut%DPegqac1pl7{b~pk5e@cahikmcaF4*DHc^XtXqyS7p**6NoC~SX}2WDBIq5ownye)WU#|bb^2yC-P;e(i;u{dAh z`#GZe`#P~?!6iY$f~91AB9 zd4&rVIkQlabKndYPD~lEVZL^{)*>?+ds>6!jlcgxWFZzcj zMgO!#(MH;QhVo7P-vICGnx|+#dBbIjCaM*EZ5_O?2mZGS9!Q%X&nWeNrmr}sZ)f3) zGm8BH9{Ixw#dhDM*xs#*?GNWX2u^u;iDF0PD0Y0BV);qMPOej|s8q4D<}3Cq%3rWX zu}cdTE2Eti@X?AQz70Wpx7I2Kc8J~G$y`429v+7SFIB7ouKN@`_*v?{umr9gR;-iu zdm9wPXT_qWilL>%{x4Ipamu~Ztk^%vpDIx7vr5Ij=v3@$xc&DQD84P)zz=E^&s?uK zekV0Yaeg|yelGl;{ovwwPA)MBbbu2`C&B9r$^ZF0=09dI4{}QJ^EWA8+N}6xv~$H$ z#jmE$wX=$^4Dn8E@>Ufwx015=v?^X(uK2@Qia!!YqbNhG$W;94b&6wC<4v^la;f5N zwAsB}@xCpJ4^cj{S@9(8ZlcZ`oXcBv%mJnSf0Fkh$Ne;e*uxTZlm_&aDJ8%uiF7oY z40M+{<4XK+juQB(#2%$e>|L$IerPoZbn*@kw3tIDl{jKriKAwTJEWoM@GH{K$U7CC z=I3ZOzo6{d4a|M5QsM%1or}?TE=yOUjD5@N(2tg(BW0r}(bmmt6ud`ihB>y4yl13X ziF;}50W_tD=QAI-S&8}rCH_p^Cu!>`&f%F7=J4|W1@fEO-O-(~Rbr84Bh(@Ae>CUj=iZ&kEifv)?T9quF<$^+9yYL(wu1 ze?52?%~9xo$I#Ak=%qhl`=|d1-fio=nxH=rdiL09JZFSVmLUxN0`cIz|f(OhfCKhUX{qghkG zry5Or$3WkPPb3F9_n|H5BJ1l2f~pW|aK+btOOHx<9Q^a++)TESt1h$GW8(zKGZ9Cw!U|KB;JJ|(~5yuac4zM;&w)cN)? z(zHg?a!IR5A&q`7gOpFYiL{RNx< z*w5O}+|S;9zjjuVVhywVH;vw zVw+;yVjE*yW1C~!V;f{!WSivoZrMiJR@r9RcG-q4rcK*4+cw)c+dA7k+rE8&9N0qJ zMBBz)ngUyCn`zr=8){pMO|@-J*;w0J+g#h;PqD$a#kR?|&9>3D)wbET-L~Pj<+kaz z?Y8l@^|tx8{q_O&1@;N{4S%DqeZ^Co0^jjx{@a(>r`WgH$Jp1{=h*kGqP<&b%RcFb z$-qb1SJ`LTciD$soD=vo`!@SH`?{aA&%Vz-(7w<<(Z10>@(A+mGwnO=L+wlLQ}-wh ze5`$~eXf14eK5Y*J~`!^?W66h?X&H>-@%95m)ob?x7)|t*W2gY_ZtHk3m6j^8>|N- z7%MypW-xXzhA@^erZBcJ#xT|}<}miSmiCQBj7f}5j8V>~9b=ZWC~FL3EMrV#Y-5aL ztaCVZ4krJAxr!SL?YTL?M#f0SO2$mz1DAoJQdsIUFqN^DF_y8GF_*EIF_^KKF`2Ph zCm79G&6v&D%^1#D&X~^F&KS>F&zR5H??&1;7F@9|z=oF=23XOU(b&-#(pb`%(%8}% z(^%7()7W$G)&PqdlNy_XQH@ojUx8V-kc?rCWsPZ#ZC?Z98tWSK8v7ap8w(o~8yh#0 zXRK_@`~cY57`i$fU}|IQ-!=zW+nC$f+Zg;8H323!HaA8$RySrhb~lDMmN%x)@&xB7_M<&Q@}mULCi(WNz6@_ zvEN+9oWZlEbxTd>Apt-+eZEocwdp+Q)SuqI(`!WzXXXcZ?c477_w>H{rf500}F z<SQD`}(mobwB~PQ3JVw4X6l*EgRIIJ6C<(Nd^XH(wEDbam zYcVz zPeh-zHhIW&pjE;J`@T=y;+qPzOmxr>Scfr=wwVkCTBkM7=g>Z%;MZEHHBoD$)=10H zN-tO%Xs0Jr_qgSOrn-Ly8fyt!Yh6lnO=+*2(O}2XVw21r4^1oh4n~f89$%`ShgYiS zu3Gi{u2DU|W&dR%^_)u_1K*1!R?ktSgUH*vM?F7Wubyq{wed^VP<*^h8{aA5JKkBm zLuw1}kc#pSDfTrIm#AB$jrWjuD{D2bE!W0hXKUkmi(!)-Tz=6#w)}w1~b&%R?Rmsh*30DsQaPK>b|o?-8c29dj)Zfi);9X zH+hAu$2gw2#bHzG{t?@|Qa2;2uGu;2`m{)0@3PL~HQosoo>Et5mb#uVP}dX7)%Ead zb%6o9ZYD;tVnSUPv%h3UU4_IrumfF(%~jWa{LalFelbh@V(Pp2Mfkh*7psY15VLr; zi1e zW4Kfu9ppV%t&Ybt)$vfBI__ZY1)6flmHfYe{4=+x`)kE&AF5Y-+oampFHk$!q`kJ0Z+8>pxQ=+nuk-nKH|sG@U!(S)5XU&2 z{QZb!%w_$>wpnVMS;)69^7!@zF^M>-hZx3-q{e);tzD$HnryY*l&QAmX==M*Qf+63 z)uslu9a*Wi16ZT6`+VLVHO;%Dh)qmY^X{m8-W`>u)?Q)~FD+N=U$WJ@c1*1`RcgJ7 z?JHShaRK>frmHo-UagCXb?na?j9rWQZucg>+f6*;Ut?TKkhK@YB);9G4WDJQ_JZ$TyjsQDi&nmS zG0EBs^6K*V?nS9K+*GLzSJo5vV{BzjeYbX^T)p{=hz~N(ezi`$molcVW^CQm#P?{} zeqctueOTtml}~KqbkaEs)PFH~%QMw~{jBi^qH^|uzPzaRUE-|io4<{f;M*iUTZmubAy3A?)^F@!y`i4|jC535q>*!kGs9%2(q zvBxvmU)V>)a5Gl1pog=y_R}~`sOGU#YVqAp%Age&`0F`XNv~X ziZrl&g9heKX<*+)8aSv@14p)M0E|CS$p5qOOH0>i;BsON<(paim%$pqg{%cE(7+@3 zl_$$J@V6=rwBToYYcvq4)xc|Pzg?|?f6*3vc;J8d%I)%acjA1$Gm*}_6el!z7``&M zhBXuHD=)vO7njal%@ zHVt4C((ZL&6|fEWFT5Dsa(s!x1dvn$9}QkR0UUS=81Bqk=B0oGON*JOQpbE1aN&)MH2nKY4c`S0e1QLtF3|83rL5Vd z{)?>|ZlBO_{~QfRX#3SX4gbAF!|&1NM_|8yS84cbugVB)*9yKECbhqi+?jqOoF@{D+AnHvjrU7ppm*laPc(wn6l63 zX=KADjll6n2GcYWTdk4T=4oWSQ6rPI{Sm+a=KoiQ%zYZQBi z^3T)W%TcxH^bU@^`&_W7dkz6k+G-wHmxT`Thj z;q^Pi^>=S#J|X!(g6|(#t@uK?{1N;=W})JFOBKhr$4`cfpB`1bcnUtBq4@dp;P>pm zEEm2{y{iipuOQvX_AT)H+vYR>k@i<_VLs%z;%j>p|6`rv4a*h(OP=D-%u)QgG2V$y zUh5L(R?cINWmNG&jvt}j1a&r5p;ge%Tb+u(Ta1Q*F7VMx#i!F0-@-Y4$$9(_4Q4B} zl5H!L*dCo^r(z}0jS}{iI+LowgpObqUrRkR3en6#89&mkz(fK za@<$Qdu>*UH)@o?{w5~)olH~W1L}RaS&2_5_gO2t*F0hkl>K^4iEkJ3-gvY-e}~uK z<(*T)J5 z6Zo(ByhopXr=&B_nX*OHITKCptowLx3~epRLl0!%1xfV5wY+ntgn87N%%_erw|W&? z;$ditXor4(&GodkvX7VoWq$V|F$MI&+c>Y=IiH$2Xq7e0@7{{{?r;qcOrmE}Zfz&? z!n2qkPTrqhLHp#~o><9T@?`#x3AJD-v$`nVU9+3JW-=N-J;RV z=QR35%I=b>(Yc3dbk`D%?sknvcdyatyn2o9@svjQd{Lu&b!#+>w0EmU_i5MYzRzfM z{-YY*Z)6rzFwn;p!pxd{)MYG zdgwyZD;hnF;~l;|=?RT4I-10BjyRb#qS3{Lq&|%vNi5<>j+wI?i8w*dyBa;}L{c5; z3ymI4xua`IV;Vgsi&RSD+K-`)V`ep)%dv9LAyGGXEvcClCecprhomk4Uvi%Jxs1!Y zj_bM&+H#wzwtWmAi(~rOsdM05d`_uz;~agisdL^fxCUQ~ugTZuYxK4HntkoQ2fi1+ zC%!knN4{6SXTEp7hrXA-r@pto$G+FT=f3xT27VTPCVn=4Mt)X)W`1^lhJKcQrhc}5 z#(vg*=6?3>1MUm%6Yd-CBkn8iGwwU?L+(rNQ|?>tWA1D2bM!s;!Bk&#pLE}J490!c zeb#-~eb{~3ecFB7ecXNBecpZFW58p{W5#30W5{F4W6ER8W6WdCW6opG zW6)#KW71>OW7K2SW7cEWW7uQaW7=ceW87oiW8P!mHo&&PHo>;RHo~^THp8~VHpI5X zHpRBZHpaHbHpjNdHpsTfHp#ZhHp;fjHp{llHq5rnHqExpHqN%rHqW-tHqf@vHqo}x zHqy4zHq*A#Hq^G%Hr2M(HrBS*HrKY-Huy)>wN18dwvD!}w#~Nfwhgx}w@t^k+s3DC zy=}g2zkPsxfqjB~gMEa3g?+{w{Mv`um)NJ+x7f$n*VyOS_t*#77uhG-H`zzoSJ`LT zciD&8mpxP!_%{2vJI9r{jeS*=w-2;0v`@5ew2!o}w9mYVGWMbNrN3+pe5-w|eXV`2 zeXo76eX)JAeY1V^0qnQWw(p)dFYx8|>GtjR@%VcC{FLuE1~3*dCNMTIMle<|W-xXz zhA@^erZBcJ#xT|}<}mg!1~C>fCNVZy+Y?|FV-{lHBY6QgLN8UUt?g-H-(8Oz{bYN#>&Rb#?Hpj?OEBbqCkGnzZ@QW0=T zIHkE|ies8O~`V3svoYma*fvJGYn$zA0x1|rIxUM;` zxvx2}x$p_efE$}5A3%L`=D7_4hlWd=Q>VD~hj46jZF6pO?;$vNH(cDD{BM*qM>khD zXE%3W1&6HVxesllhOO(jl!v&0(yjfU_cS^~Z) zr7a}T7zWWAumdUW!5YNlXc2X264oYep9!>zYbyfnq7)6|SF~YGqX2Eg8i%zGYaZ4< ztbxp_MiU9Nk-_P?fmVY39elMi&`^4oqN#+?Ru%*r%OhwrwRvbSXeyOxFxR5F{APKe z&73`jR>Oa5Hr8&e;aJPrD;;fz`e;1XdItVo5okYeqXAh9vL@8aZwtv<(Ua_d1nubl z!az&98BNLB(&cDO)|#w26{0;^gF3P^(4_Xw4KyliRcS>j?JA{Vy+cgm^+~j?N;EFw z71p}Ame3PTfd+OjTG(xi0&Q#sTG*v%WlLt!&PoFf?O4iMQ`;YH%^KT|Xk=S=rnI+| z2KQbynjBi4wYh%uyw;*Xv$J;h2lB1u{hoa{q@(f8L+eAwI|t40bjs%Qdlczl^7kd) zVT~{YUGab6Ks&UChz;!l4QngDoiy&x^z_p2}AbwyEd7 z|Hsw&$5lG!|9>!;wV@@%&JuVq0cM2(h8H zF{d9>r#jWCPNz;{V}_k2YY^HM!m!`x>)ihO{&79dx$pbBuKU+@z2C3b>wR63`w9Hz>SIo5DXKM)5r{iT~uP1yF1P#W^v+Nb>uXvBa8BT5y#lMNFCcu@C}R- zwST%%?f)uL`xxs&MRV2O+Nk!I)71V{Ip6T!z&E^!Pm~j@xSISI=cxT$Vi>28_Zafb zFI4-!%+=VH?d^y`d_Q07rWR`*?O@%Tbz0X?oT6<)>l!)sbh_3(I#28FCQtc7t-G4^ zyG2@e4s$i~NJnQ8zaZAJH+gp^uCdJ+@eAfoOja@ff|$i`S zbVMy1)73Ic{324N7VJVx{ft^3&*0nL?7Ov0E!Whj<>HW9&LQu~#5a!QcouPsJ=4^Z zn$EW`h+j;T{>?m!abgs-ljc_DQ@prA%})@gc(75;cT}nQ_sqSxY@wRZC*P^WFcz}^ z;4;>ZBF6Ev&1&9?SjCsjrTCC}6|{}ez$%5>vK6A9&=chfJ;)r3+Zz;Gk*CllV+x%| z{335!p`*$a0>6fS!S4=i&swUr|JkUu?-yw8SV(JoGqiRs^DOH5p2cIiT6=FTb1#Tr zEaU%gE17#ioa6X(=3W#t_o9Zm7sNldox|LV9Ohn>@ZF1QzI(yiP;h|PUYud>MVdNK zY$EVR0FAuqM=XU7osb;n+RwdxW?|ZAe`W{9lVr z=&EBqI`V)myCzE1^*Lp~jj4O<&FaQBchALc{c?r67gVbIm_~KyMb&-wxVo2YQupQ9 zuNBy)TS@nj9+_5m?S#7j%5mC2cPIH{*gIk>-R~8u`_n9S&&*O}%MnGkuVoFs66|Ld zYw}GpudNQdx)}S4EiP)q-g0ai|H~S&!v(}9Ca}xc+o#$0B6&mD->wElhA96J^1PR! z$YhNoU#F;N3;e`S$JMhdv4#xn{(&><`Bi~>j;mMCDbwmH#NRBz$CO6ZbKM;E+>DR- z<8t*pNZ!ZC)bkAa8|N|yko}P&<^)pTIQieJQO_iKzN%C2tOoUNSFc|1K<}P4>dnOO zEhtm(Lj2-Mi&#Suzjgus?c#CuUe&1H8&<0Kwmjne_|j_jt*KIPeZG2MAx~?YdV7l1 zJDjRsd`9nkMe6-zO1)o_=YR9mH>Xj3J1t_KWK@0o<*Dyr{Cf`DYE$2@@$ZFTf+e%n zciD3FEgwULq_`kl5Y3loEq5A$? zqQ37KS1}u$v3;AOyN)Qj*My?*xhP|`qDO&YP9Wv;AAcTQvH|Q81;aFgWxyx5gH`SY zw^Ywp^e=N1eP&$Im+BNtqd@J(RU*4qtJ5$wv-%`e>MAW~A ze9soE|D{ItzqVQZ9og#dD^ve4`Tns<{oHH+rd;(;k$(n^I;&9wbD|nZ1!ty%O*6ot z2Y@#ZP1V4WB+b#lDd5-w#?KUO(m?Tu21>&kSiYM1#4DAYSG)?$`T*trL_JSbfpfvA zFN1+!BX4^JI5-D9O#N@vXy6_GPmF6|68t(%{l@cKWovLdusH8`aQCPN_XclgvOPaX zgGX@uxVaiUsa%7nZPeg799vwg!HZ`Yhm_BFB=Gqk*nb=2k5+-nAE;vkAd%$gv6bf7GD(XG@v?ki{6wjjWqprQt2|G`#gDG>U2s@0hLOozXAS zOVKdUU-m{%*&m(cz*-F-fYfLN{52enMRr zqj{8OqN^-LUui&RVfzo1y&28qHuRA@sq^j#V^BHwL$j4!gX6I*v>WPrY7tsa-j8ty z{=c$N!>@6yWuAuHQ#9Pkd3s_R?r+fW&?*fl7NZw&Y=0t z*$z6L^-s{d7R*5pD@GSfmjq01F*Vl9>} zS;u}JW0BF_E-65lTZ}&UJ9N5I)^y=_MG0e>*>@x7xS8@Rv(Wfb(E2#Rr!uyijPRKgD=;^u5of7|*^Pn(O{(uiWF;r=!PSfG*2(`kwT^5;WT-=(p#hUcXZ{SAB&d!Uv%XLH0HC=nm406 zqbKi&R=huYa3(tSfpgHT(TM+VAN1@x^zBTvZt@(qCwe&h52wB((5Z7qlsF1a`RMzV zSooe2$8sIVRVb0m{^NI2;)Eh47S$+mVqA%nw^SmJYdK}H5~tpx#ILD0A6@#ix0N`Z z>nY&)ncFCF78>^1yDCx0?>Xq{=dz#gLnqFAQ;8z(>-;rJTyVP*7ha&m;w%!^|C^{1 zOSp#Lo~uML<%$QCxQINz<9xr{pv1+ilqflnM7~RwkY<#)^e!bX<2;v9U+G-ZY9%h; zgH%cSL5VB4zbi(Sxbkok_i*Ku5=)OK@qg(iC9p4vt5%SBj#qJP**>JRNZkLjmq^_6 zvd@&bn(MiGPtpPs<*&{o75td|-}`*bdC2QB$+E7)b-7OJ_BoR0@;T=R*O0sx%KO@q z*T}W{ntkoQhvdC*Prf(bqwm%C?0fe!@U!qU@w4$W^0V?Y^Rx3a^t1Fc^|SRe_Otdg z_p|pq@VoFk@w@Rm^1Jdo^Skpq^t<#s^}F>u_Ph2w_q%r+a9ePjaNBSjaa(blaocel za$9nna@%qnb6azpbK7$pbX#toyF}u=}$6wEMRE zxcj>My!*awfNgzZL)2%ZM1E*ZMJQA;ZSxP}9QFbB1@;N{4fYZC74{kS9rhvi zCH5)!7Wgw@3IfGFSAdxZ?liHud~my@3RklY0-~y z4t$t>qVV?JZQ55R!Ng2senU_)a> zV?|>|V@G31V@YF5V@qR9V@+dDV^3pHV^L#LV^d>PW7SJy0d_TpHI_A|HMUi0fOU;| zjeRr60xUe2{l><|$Uos+Ta_fSa}q;Ofu)V9jji7SV;gH5a}R*MyTRbb;>P60=Emqx zgVl}MjoppmjpdE$Z>k9}zOlYBzp?)X)L|}=UmtJ-a|Ck*a|Uw(9c_uI5Hs(0yI_5m)K7U}_T*#cr+{hfs zT*;it+{qluT*{ov+-h%*?H&y{m$?@l%v>zV$tK}u6L2(hwKr-4?$*nHb2)Rmm(!Kx zc;G|5fP zQO#BV0cSOLHHS5qeGN`)ZflNfuKNg__dd9+IWTQL$%)O4&5&y9gD1JTIr;l3N^*2_b#r!ecXRlc*fys(w>QVX7aniUZ|=XmG0*~* zpb1zT$e&VDE3js8DF3Y?SWB>`uzg!nW3bkc)EquSd*EJ^T7)$TYZI+#6xJ&0BY}4D zC>lm3TE-oev9_@sjl)`pH4keazs?S{kQ~Y#j5e|#@epez)=bb&tf3^eluu>?ZN(bP zFgi@n(m;E8We!>lV?)=}1lo)>n%mH7Za}lSstFBeBU%o53Niz2XCYe6g0evKvG$Wz z7idAe!=A6um?rB2t;m{D9PP*&QcG^2DLpe2XiN{1&zh69Cu>mFqLz@qa8aO9S*!XL zr zKwGoMb^=-(zAUM|S%br7CpEb($C4Ud_owp%&Cc3gtXfGe?=>{NzitRLK5Ko}{H*<1 z1H7DVYl3H^{pFz%9)tF0&CuH6+`>RhL{segmRN-~Mr)04p*aqrJ$BWhMHZn+rUu&N zBWRTOpika{W_cYN^gn_C3U0i@P{}W23^$$p5i;E>iBDlI@S=Mc&JbvcTTF~4;AXT zDxGg&)T^U#KHtD-Q^(PZ)Nyb`9s3Z&*oC}5$x{2~)oP!bqxScg@(u4XwGYha8{U;_ zZ)A=|9dV045v#aoT^-z`G?f|$b(%$xWR zF^UOd6aOgCx`D-7x2}}<#d6{otRM9#>F%Y(FW7f=5%G(B;uqP(FNj+lJ&X0Dn3Ita zB7RXt{Gx>TMRxH0?subVOH`^Yl1Kc47{&9kI7c+Av|j>KxKeAM9MRf`=W1<533D&%n0v92xfkR+g)&D~ z@!g9s-@TY*?nN4NFNj@C73sBqmFcxn`pAui#Q%uvJxD+K3;pT&73zF7i#Q?MvDNAv zr*FMaAN#yco!{m0eVR#i?X*%|dlHj8piy1FTBI(xP**;E{k#q8x|n{xY@E4P^zl2x ztm~A`9IVyqs;BS&y;NQ85p@k@sOya~b#08PYcfq;Ul+3;T^-+?nB*HfnS2kM?L(^7 zeH6CoMB))=5=ULUP~Df6sQcP7b>GafRSoKXxJ}(_$n$(m-A!Za?if+`0Cwt)u(~&f z)cpx{epRIiv4coTks`ZgE3ywZGi#F~M_@mXU!}-t`Pk1CzBe((yf*Td(O_=MZ1`E%ECs3e~fM{I_EN?`HqQm8?TZY@$9@J+I`er){NrdYjZUvQa(jsRR7f z1CQwW2A{T7g?hKgCt(|V_r}-pPJ4e9QSWj5ouQMSt0(=zQ?VkzkAy(!d<^ zz#a>UOXPw@^1&p9U=z~q1z?ptFiQ^DC6n)brGjNPgK5Aj;U-1yP!Bi)N)Q^4YF9b*ZX1V$=ou&R|;I|u6 z)qiUhYktmAf7MF$*G#GZ$zt_CA7va$7UNOK)5X3)#;CjwE?W=AgY)%IG8Sbztp3g5 z!mYuh+b`9?E;SnXMN9+xO>5v_aOdIdKekWE#+Yw^9R3$a86}29|+4ujluc zY7N}Q{`)!pD0QqU(7>~ldnt{1#giIX7tufuWrj+@xAPgZGY#Ht`Y|Rk4;;J!JY1>4 zpMuGE;{P7t=Y7D>^xwfl!Phxp>fBNdo;=CiXE6D>;PfTSHFyd8uPW5wiiifwvov@| zl?Lyb*5E@08vHZZyLPh%>-qokYQ`r`Xb@gE*t1xJG4j10)8N}2e=k#mACYH@dcUsL z;13OoZ5>rCWn8hHCluR**uvg3ieoj6z(ww!50{yO)4)^y0tc!~Wj=bFVt*@D@ZAXZ_v9%y2&Wk(|68*Z`{ydg zfst>rO0h3e6#J%vabhzX+6Jzb0`J;6M?<@>*3e$;+rLOdS@5m}aIzzFHFR8+hE9aX z<)>-rtb7d>t<=zxCJmLq5wA>v=go)jvHxc3x`ShP*T4ts;f18f8{vth=SVNI{ddmM zyh=kIWg6nS4GqF66B!zMbDHsVF%7-P@lB-~`iwfiVBa?l8v5T-#kYnVZwH4>U8(r4 z@Y&QIq0JD1ULa;-zroWpL!{ zLW~=PGv5NYzJud;&sO~YT*V)*P`oCr_?lUYKTVzWC5pdDxmUq!ZAu)&o#Xlf@%)ZZam;*tcZ_*eKIiuk%$2GidlZI2!7JgQ&;a%rzcn>s& zz1lQ9ubi=!xs0)#Qu3P*M--w>piku1qgCXgS)gT{zEZBB zo21LKHGD1mZ%EhhP1Jwuq=qY~V^t%uhdCO4u$1*Mn$S_Ga}9dPQ&q$sq8k3|Y}O5@ zj=wM0@Y>ZHZe?EwWg{sXj%Kn32ggRJXRH_vXDM0^X(M?j_}#?5NsdiX-*mQyXXa}7 zyUmP&9n;8G=t&I;LL=Eb zXyk~O(5BF*j%DBR=vybEWu0OT3tg(%(>eRW_|uS8tHtOb^Kppd^FcEa4KV_cSR?p z{?Uh6i-zrS&iyve;T`VzpPO0BhWb9>IzQwdK3>6C?OCjgL*4&PW8ECi{q;_0mX*vu z*pe~dD_E}wopY-^^i6cpIomK+{C8-duQ7%k?ek~or8}d`?y^CN^mN9bqkqm_g@)RW zmO6>9nuflLZu-kY#xlme{(%oQ14RCb5%>Au`lP`*KjY_9i_yIdzAP+|8JP5MADplfc?&Y2m zCGIU&qLOpn$MO5Qwg<@n0OhJE|KLkXJaoGf4;Ly?P5ssXR^k!%JxcjM?L&G~i5i~k zpR-B)e~i43&n3O0#A@z&H9GcRt{`#mYq-BP+}{&i^Ap@(E!R@Zb9j<-KKT%dJWu73 z){y?I#M3#X`$;28)XgSwtnL=lvn1~SnQcg1(=(@%xQ}P9CGp&!d5l!||0VD9G3Rk! zmvLF@a9zndUANCMJ2+?Z++2gNC3#I8^ED=~m2393`yPBRz9-+C@6q?_d-lEi8TeWF znfTfG8TncHnfclI8TwiJnflrK8T(oLnfuxM9r#`No%r4O9r<1Po%!AQ9r|7Ro%-GS z9s6DTo%`Lp4Y)11O}K5ijkvA2&A9Ej4Y@73O}TBkjk&EkuH&}nHt4qKHtDwMHtM$O zHtV+QHte?SHtn|UHtx3WHt)9YKH$FKKH6Y>`>gw}`>^}6`?UME`?&kM`@H+UZGdfoZGvrsZG>%wZH8?JHpI3h zX;W-lY-4O|Y;$aTY=dlzY?Ex8Y@=+eY_n{;Y{P8JY}0JpY~yU}Z1ZgUYy)izZ4+%9 zZ6j?fZ8L2^tm3>`UxZ>|5+(>}%|E?0f8k?2GJ^?3?VP?5pgv zep(p#FnpPPTGF@K$Jy7}=S|}K>;vr!?Gx=A?IZ0g?KAB=?L+NL?NjYr*RBqHt$nV2 zuYK@SRe?{oZ?=!NueQ&&@3s%OFSk#(Z?}(MmL2$f`+j49#aRI+Fg7qoFjg>TFm^D8 zFqT+Q7hnrx3}X#r4r32v5MvQz5@QoEim^%(vlzP=!x+mL(-_+r;~480^BDUW0~re$ z6B!%55(==AF_W>AF;q=Bz*NRo##nbW1(?g&%NWd9%$UsB%oxpB&6v&D%^1#D?ikKz zY`1_qjrENAjQxxOjRlPfjSaV+m&A%m%xLUr3~4NBOlfRsjA^Vn2<9~QGzK*m{X55v zO^s2FRgGDVU5#OlW$ysf8r$9g#x>S8<~8;;1~wKpCN?%UMmAP9W;S*hrZ=`X#;+saUy1|lZw_$x{D2eO zR2y&va|Lq-a|d$>bBSNWDak_b*}rS2bsayPCr$xvV*@x$Sy5?(1+} zb6#^_bKqCt!sf*0#^%UXaOG8Vl;qCl(96hUPW_wZ0mnAiJ^{{c?!5pGZZ2+4p3eUr z;pp3@BsqJMyHCR5&E?m_>CNrU@xyR^bAEIGXHx?$z?#6lc}i*oe?Ti(PO^5e1P#Gj zf;EK`(gKaaTEj1?V^8X`29c7T)FiA;BsB_a71k`q&@QZDbfIO0&@`-VJd4I*t>aJHJkUa}LK7*W%mu3gt>k1hlVhgPP!^-5Q19MZXe*V0#)8(;`z?CS6xz#&XfW@r zM3X_&u{IOtn6(;fHcyQQ8qNb~Id|p-+Rim-Jl1-C!|}7*0uAW6XrKulgs!tU`*!1) zH6yg6o^Oate2$j%0kMhoDS^h+i`LY(D$t(l(V+e^7HCp;qeWSxTG4=JMZBUIEy^0! zuhFb>CjxED8rR-vUDmu($cF}IEi9>ty@xh7jz%_A7HDRnML)(cW(S(ueNDtLW(8WC zHMif5p~0o0#T5pc+!3KbquYml*6dQy;kH2c`wHF9n%+jVy*JSP2GILDNKI&e&y%bL zK8z-4ZSZFDUb`~T3>W7H8sdp)iASLiT3eh~g4UQ7XpURfCpE~f$#gVHw9GfqCI`?c z+tDhmS=OUnu13Rr5H0ggG)-%p);O(oo`>d{UxWrq48&UK0U@+e%I;K=)Ji+Q%m49P z-psQYFF;eR4Ybu)=LcHrUm64L^)58n8_{BytyJgl>eYE}RGp_z^Bs&-bsmw4;5&MWOSBZJi7l*`OmFY$EoB!rcxdAr`5r8?bt1>j-O`o4etuR;m!Pu z53)Vk64$t-P3=XS)SjQB_T#eDz93KS z`<19Yy-e*ttt5U?L;Qm8SWMItzi1$S(MbG)?Z#^27aV`IjQB+{@eAS>S7#BwAl^~9 ziSJ)D@%@Wc#4m_xWDv{PiG5o$r(&j*_(g`=-WgNdNQK(EbJX@K^DpXGsqOJBwcQs} z+sb0KT|1+;OE_MXuC`N|b8!sE4rx%^KK!QDscl>072goA_=Ncr?-r|dgm^`FmRetB z-*XdceJoe4mDOs!WrJF;VgJQT)p|~oT2CgHaTIZlEMgpc63u*d3RZ8sV8o3G|W ziDT@`|6Ov`yv;_1W>zTlF|mmCwF(Wf9#ngqLXEk6w>!#ryP0!QK|JI7g$iBPsL=Tt z3Y}W1(9tsr@w`KOl9#z4p)Htu@!yEnPNZq=8^kGk8<~4CqqWcHF!y2wb1xd0d%--6 zW$Dbl$Yt&Y`BkH}3qo4E@3_{cO=;~mDe8P_6Y;+=b0;c@2j;2sUHZpIBg~yxrLHY< zi5pIUz40xmL58 zb5*Xco=N6l(VsVve^Z9Kz98=p8`ZsCwz_wzQg;S+CTkwwoG4ZI@r~*}ZBpIm6SF8; ztnRBhej~AmJK6VOin>?NSNC)I>ZZMSufrBaIgZclez#uTA2q0Zn)JUqMYgL|gt{Xc zD-}6tsUnAC@06oR{#-@Qol@kYs3J>OD{=$JZeK`j0^9kgHbtH)!>(c<*G^(*IX*<} z;vdsL#wIBL^*DC9R6W~c%Xh0%54fTS%-C~8rg~Jb9(-ZXx$M6PTYlvV_56OLdRFGD z=bk$CJi@*wO4Rdwi1h~f-(H}eDE5AIgL>Y{P|t@&>iJK#dcMQgY(1^s)C~3Rfp3~u z%3Q!o_2%Grjt{9fKce1qN7P%454&QcdRLHcAu;x(_aXe%YW&o*_3CY8UPQ|Z^+pQR z8^_O$&r^=*kNvsqWggp4goXd6ey}yik=!$^lWg)Z@?#)&R6tm@I_gPqPJBr7qnW@ zhijP|TCeCc4PcW-FbXML2WFwJQOd7h4wflW^i%N2R~cZO8L$r6C$(1nyMvAP0bd=+ zzQe#q3(M4h;(Ya=2HrZiTK)8m{>%8k94z&RO!eOePP(T^{SQs5|8a0r9r&uDLjAAK zR{uK6_JG^s)cNMP`u|DU4;vVdlFGOgaNR60BeBEO}v>1}?7Cz?ESQT(?mJH_d3EB1Hq0l&{Xxz-m$*sR2yd#BUq>BVf@O zb-q5Kf%P#BykD<@PbxI#x>ZyNQ2nC z!RTxa4lmZ=n+=Rd;`j#z8k{8W7u5e`GS&IE%p<>y~ z6+4<^$5$(MN}XZ_4T_xyhgd@1OZZ*N_I2=$8>CV4T0@UTG*mkU&r5^vWyAaO;eVw+#v&*`NS*{d?H{!odWYZlYc%u` z`#&$&(3jLfEI7VJ7USpOt|=3W?*u1JuTp%^#fs0%Q2YS)9a5+GuZk5vI!*ChIP6Jf zisy6e%!uOWvHv%-m=95-cq#Q<6@mjVQoJk*C#LQ@$bUE6_rrm!Df4IQMt6(X6)FBa z`(CEbrc%Y5`QJf3k$lDbD;1AZer&dq>qfu3RB^65zA3EuryH3E!G17K{Cl|mmih2} z^nesN|IbR`|D>NU)^J9VhWA4=I50=aagf<5jEmf);bWp2KE9SUA(m@6AMM~w_A~Es z_<}0NR#Fd`f4CGa;i^)!3G|8U`F|62-a3PJv6^w3ob$ejh9An)@S~J_obx;}rs28* z4cAjQ?_l_E)c+dwVh4xY$D`^kXo^0=f-n0L;8aZ$YV_wmO z7N7wgK1(A~>OexHI?#W~6|(5~t&MevnI=G)@ONzG0DA*O&(+I`wi%H zT*EsPtdY;}`)@O*nPVSup2-(}j4z<{R(q@`Oj=AQblX5TgMq32Tm_aQXiHR!#!C~*_lbMt)k;B5k3 z_;&VJl%N}DpdX`^-d&BZd_4N{3|ceCs&dhwxhCR$iAT7{N6}1cIR57-dN$X*I)>iO zeLN9C6F(e{{8_Z~R5bN#m3WrxtIt;A`AQ{T*r){dKk?#?O1u=4qFgt}BjoELe=p_w zC?DnCqVFov->AeunG%CsSB&$;+LajM{PBZH6G{wMDlx*bkvEk{uzz$1Qk@d7=a71p z7(123yL=;`#P*w9FMcBNk3C3tk*H&Q5s5P6KPd4Q`QGCBy~T5UdmgET#4~)G`qy(! z=o5+ci%GYTo+7o8;-m@ESES8KY?u`!|4;7oG3Rk!mvPy}B-cfqt~+@SpKD%l?&LLa zExxAYwJi#+HF?ckyYIpG;(PMF`5t|*zGvUNpMjr+pNXH1pOK%HpP8SXpP`?npQ)d% zpRu2{pShpC-+|wS--+Li-;v*y-KiN-?87d-?`tt+ko4G+l1SO+lbqW z+lB+pOEJ+pycR+qB!Z+qm1h+q~Pp z`+)m``-J<3`^d_%pwGDPxDUB6xlg%oxsSQ8xzD-pxevN8x=*@qx{tcAy3e}rx(~ZA zyH9)0iTk+wy8FEQzHNYQfo+0qgKdOug>8myhi!;$iEWB)i*1Z;jctx?k8O}`k!_M~ zlWml3m2H-7*AB-Aw#+uow$1ZGuywY1N!w=|Xj}MSY@*{Iwvo1#wwbn_wxPDAwyCzQ zwz0Ogwz;;ww!yZ=w#l~5t?vf5+BVy^`(=EIZMki_ZM$u}ZM|*2ZGUxP;0x>%>>KPO z>?`au>^sWvA@(KqDfTV)G4?g~IrcsFLH0$3_$2$LFR^a38 z>+JLF`|Jbl3+)r_8|@?QEA2D!o%W$gUuvIf-)bLgUu&Oh-)kRiUu>Uj-)tXkUu~al z-)$dmUv8gn-)~L**fF+D6j4g~Yj5Uloj6IA& zPF@~h5@Qo%6l0Y`xE5mBZ2}`1D;YDrl@nkn zV<}^*2>Fe%jJ2A;T*h9;V8&u=W&&(xjApE6%x3In3}-B7OlNFojQ6{m0P`99of!$R zpfO=C`Hc~c6^$8<9gQK4CDXxu#+E7UGu8xi8ha)&sIjOqDc7CEsK%`%rt zN&{@mJ4j;PS}?D%uQ9N(urcv%U}IxsW96&aHg-0KUd*|Ssg13Tv5#*IF!!%w0R}e~ zHzqeW-vx}m!$^SH!S2TJNi1(n{~_4k7~fdmm_G*gHwS3ntRyEeH!w#qS6I!yN8t|U z5atr*6y_G!aok+PoWtDXJbukZ%t_2mj)tR{tNdSKz+KE?%w^1J%x&N}<~m8vWA5_- z+vY;}i6l2NM+(E0n&C|5PUcYNQsz|VR_0iD@_RF!>w37CIheVaIhnbcIa(fE&795L z%^c2L&YaHNZYMaNx!zXrLAYO%1DXq(6Rw9Fnj@MknlrY+9h-jSlIE1FN!28C&5D$O zdtSFO;G*WF=BDPTzn&LxR&!T#SaVr(T60@-TytGGueooM1Ahn?HYYYWj>D17mCc#W zog3iL=F;ZW55cYPDGIo@Ik&mDIrwiWdoG;Z-26m1y1BYJySckLyt%wNJ>1?LKgsn! zh4W9q{nw)bjG_ft6R7}he9nufIv@I+GUu;$T$_F)aAVG){0G|)yKKqI*;7tMsc)=n-* zLn)pWXe#*}v&Ler<&Y7y7yi$sZfh}f(0=}x9cVP4pw(Ejv36q($65|{Fsbc4+Yo3y z)_kn}SOc;abj1YP5Wm)l@?(K!WbMcr(mb>zYf7mZNsX!Jy9k;S8q|kqPw${zjZUCR ztwx(F3^XchRZpNmO=}a{R#~8NolDt#%31q5A~Vp!_F>!F*!Hu~%9bZJ zv&g4tXYW;`rOgDIS{E9fHMSSg-kw6Ev-Wl$8rd2ozS4r*88lJVh zj0&_p>e{Xrt?x(8&)T0gz}MNw`$%eo)(D@=4>ZFE(*h0ATB0?@l43N*tUznT1|_w} zEHucB4S^=PU5>hbmb#>~ztWBL4qRE~yq0SQ~)p-Q@vSz7suNigjOxkvn!o)MfpEJkeec}_??eHM; zC_0E^{5@A;G>PzIlREl|X{?*2j>Zag)Xh=HpZL9JvpQ~Bu8wOa)Nu*(F^Y(B@Xk7p zBkzI@e8W3i9lKYlgBWnfEMgE}@coN_ht&SojM|5oa}j3y@64%qmiZWrQ)s`Bc*e?n zwO_YX?Uz=n{ro!O7vw)KM*L!&_{9YA3t}KYW$pwz^1A8<#cVZS(x~S13e=p({-dhYoRz8OUl7~akyys8jSBsDDc|j$MN3-bJ)*v6#`wf4Lst<58*aU}hO=g|2%bCtfE_v74&M&g5I z>N+rscp-gO^tsdW)m1dZcWG+WwLDi{y#KC0u29#*Y3f>2tF9O5o3FO1t20ksv8cMn z$@6}QIaloewocvKq%s$a_~_h?>dwqn_hGf_K6Xakr(#deS*`BkO{_;p{Njcc>b|{6 z-S$!yI-0w?N(Bmnbr4g(5qbE3zl)0Bl({ z_V2hdMNY-Gol~qxaiJntW-D@ih9b9N6Ym{Yc8q=AZ56SJEbMUmqX9AzNRT1(0EcNZWS$!GABo4%PI^KUQe)QxGj031q--Y?=y99r_48L1epuXFt z)yKW`J(8`yHTcx}67~IUl6jKU*F(PeO7-DKgKtc*pT5xdJwATh97Vwt(LH7mF`^F@EBg3KMeErAVp!4FsIP#Be*KAb(jjPpvN1^&F!BN$X>isYT)o(#;MdWUS&!HXXI$$Jn-XhV;U$0 z6Vh*j^_vbDndV`%J#B0uA|b@idB~@29}LIwUoKw>~G}wS~x~K`FkigNcmCfd8-Dl zLYWV#|1-`r9f8Zhg|^It+stTa#|91Ux|p?g=dk83eCWUe4b6u?9Z{j7W9MpUQJsc< zoui>M+ZYeFP(w@jy@c&6DR(W$%c$#?DGl9OsG)l)|4^!iYDzVi!36)1k_LdMO_Rea%Q#g89nJYA#Wr5Bh$gW{LeD1K#;;@7}M zuMe~40OfDz*dN;zzYlKu5M^t~vnEsVr$cbyT*Y64<2F%8a|DjO2(HZjfq9A#SHYoY z6n~p?|AZHRkjnVMQpG=~t}kaR{w-zxSE=FI@cHfF?K{Hf(^hDB_c{&l37_A0TEhph z|KLmwAI7m9c>6K%_v1@{j6sn1%%vJWm+cGT`NfMgd`Y&3uRu3gHlyL|#x-2lsNtJe zY4~=^{&Aj$@14*veslN{$~>0ASj#aDKV5+~F<-+C{Qp}Sng!)sni!{=$py3ZA8lKE#3}~f>r^)k8zJ|ZAWj%~(bd=-KQ_z)m7-h|j zoiwr=S`#|e$X+3h?EA4sGSPgp4rdJNxr{|cbICz_S@@VnavL?Ws7WJvU1&OCw4F9I z9=40nrxvf)NHKa*$uh>io~7isUzcyA5xC&U4d^{Lp)cKfEIJbV?xOrXH?o$90p=hXiUT^+0?(M&O2HM)pa*ce)J^j$cnk?vW zv(e?YMKAp64(M}xGF}<&F^#kv|Mx)0qfaIF+RPgJo6z_M(E89YJqG&V62?X6pa-HO z9*&N9B)Z{3bim`%7*CBBxTq2>G5g230{_oAn6cO~#${j1`0N>UNcJt>gYnz7N?gSG zE?&dKpoTzvOy>t@o zbRRU-0>-G9GhQ9t@U)-b)^T5;d;@tI zSCn{p4BfW@4S1yzucA?|rF`gPwBpy$jaQ%{&qqsUyOZncrmhI*>8(M7=6UpU&4bI) zsDD7S=3Yk9(6hO>*QtMu{BQ0WX!zF1-{u*v?^WWREG6EpQsSQ*l=v5QzjwP5@5hz+ zfU^JQ{2x{;u_>y=$Fr39amyND8-Dfj&)Gi*(y^q| zNd=^$ACv!kpN}~Yd0i%1)^)fp*Gb(zNAg@gXHIYp$!p=7d~L~Vptto_WB&))CA@51lI@5b-Q z@5=AY@6PYg@6zwo@7C|w@7nL&@7`^|ZNY8AZNqKEZN+WIZO3iMZOLuQZOd)UZOv`Y zZO?7cu^G2Xw@tTEw^g@Uw_Uekw`I3!w{5p^w{^F9w|(~k_XYO}_YL+alW}+a}v6Fn$mRp$^+H+cMiU+cw)c z+dA7k+dkVs+d|t!+eX_++e+I^+fLh1+fv(9+g96H+gjUPY_Dx_(iYn$e~B-$jkc|} z&G!0Nw&Awrw&}L*w(++0w)wXG1NR5Mz&^pg!9K#i!al>k!#>2m#6HEo#XiQq#y-cs z$3Dou$Ue!w$v(=y%0A1!%RbD$%-=J&Z?liHud~my@3RlIFSJjzZ?uoJue8s!@3arK zFSSp#Z?%uLueHy$@3jxMFSbv%Z^lR4S0{b8efJbT+`imC-M;-@e7t?VeZGCaF@Ujv zF@do`OH+Uqj2Vm_j3JCAj46yQj4_Ngj5&-wj6sY=j7f}5mdylM<&x|GyBNcqOBrLD z{1rdOHrO`iG4?SAG8Qr>+IKp@NXAOWOvX;eP+%!zswB2D#xmA2=K28aWejF4W=v*m zHULI5Rx@TZb~A=EmNTX^wll^v)_VxdXY6MTXe?+9~f_X>4zQ(}D!p6jF z*=LOWSFo}%v$3-=w6U}?wXwA^wz0M`x3RY|xUqQA!T_5aqZ_Lmvm3h`!yC&R(;M3x z;~VQ6^Mn1(0g_z6oWR_`9Kl?{oWb0|9HKol;1rE;3v-O8;2P!}<{suCcTvWiq>SU{ zDCR2WEaooeFy=BRk;mM|9LHS8oX6bf=Wrl%A#9L-$q`tpFgnZx}SE@w`626dX_nd_PJnfsXonhTl} znj4xU!WGRKlibl9(p>T_IHkFzIi|U$Ij6a&Ip~vcQFBsrQ*+dnaL%%o0e3ZrHJ3dP zPHS#!j(aR+&3XTCGT^|!$P75KxiK8sTsg^^KZZO13l9AjT-u!4+`0pfy%w%*&ixGB z+Z^0n+?@OlxcQCjH&-`jH+Mg;HsJE+^yc>2)M2h~&TsCYI+D}^f;a}+z^7;g6F+JO z)()&8bf6_zQ+N?=!5V|LhKGs+?O`Q(u1AZoCSh&D8pWC9&#O~XyRe3l)f8wN);6qh zpmkXDNNOJ+qnG@vJkUhqXd^vnBdu)z4Q-?zZDb7^$|DQWR92&{Y(`@#478R@$ai5r z8Vvib#T<(!V{PU@G?@&_?8fo!=O;BBYdJ|x$J&lHp2Vg=^I3-$)byhkWKHN%k~N~+ zh+W)3K5Iu8qaj&KI%9F5EghW_XifXq2ilW0D72KG@9WW~N&=1Q-AJHWS-a|J3bd>j z(6p>=;g@^3m!#%(J=)jOd4U#YO$@);b3E~iBd8}U8fa(M(5$7QsYSl73N*I&vI5O5 zhW6Hp2DcXNtpQE$$yI1{O=xxKaMtXuXaALyvzB*GG1}gWK;z3s!#ki7?T`FB^B*nH znqX2J#0Dg_!bA$%;Y6SzzD%s5j^oxA??Yp}9bNGTbV+NE#b}V$B2Uc^w8_Kj0_3{+Ik=HI7{nu*GSpe0qt4aLtEehbXT=J2mX)b< zDY`Abx3h4iI*Dm^E@c0F;v4hGm!7N6lq_}5nxpU+#2r2)ezBgp6vHJ7cQg0mwap5@ zK%8O?af*kCW!%a39~uP z>X;yQF}_qCgTyF07OUfx5p_JfNF9G}RL6aD)v>aSZ+MgUvK+qQO|0TH{wqrz+2!i^ zWmFwMpRJA^$iGE}+NYUQ@!_P}*E7!|&U}ha;ulTTYJaYY_{9kE3t|&1iEXTyqxQ>E z)qX*m+D}VYdoJm)xoY1(MeVy2=lJO)@eAS-|A`R4s3m?;@#FeY*~BkW_`QhTD88+e2b@2)mpuXZ+9=}+uiKDBBIvCQ+&Idd_1$(!}8U-Uy)jO zE9Ki4#3R02$+s^m)bh_twIul8O)TTp-irDQYe!hOsP9 z&ESXTGncFR1oCDRzu2FAyVa=qCv*93cMad|PF3igl?ug)PlQVp`ddt)r{^nFU8T^S zlM3BXpipTYb1$Y9`gN{C3s*7sVoagE(wKWeJY$Q>VD82HGt9lnWbOs=iM8c?_k!5P zpWC$do^h?cX;N#in#0@);uQPN5C@DB4bicBx^A0R*M0Q0 zKhrnsHmR$T{cZHQzOcGpFH{%UvFqdI>iUux#1`f1PT@BlJFxF0bFUUN2Wv##C*?3F z3mfp8S?Vq=QTKHr)}6za+(R7nkw(^|qf7&_jJ4Q?F6_t<+i%TR_e8O}KVPNpZ?Run zk13Kmt;pO|MSh8`Tac^BG5Lz*Emq`g?AQ{1FE3SO1;4izC~^7K4$eT-*{1)~k`@b4vO+IWdezs?~H1+H~f}LH7y`4{NVjTO+|MP5*BiLo) z3~-?y;`BY0y+_-n0tUgI~+_8_+W4Q%>9vCkhDs%N@MJ^xEn?@yMicW3;^ zp6Tk%ELHD{ytTG+s;-W&!}$?{OG(0Ybn;MFQ;66#}_eYB2#_mY-Y{` z`!BB~?vKyBnKF09)K|4qeUIT6pPo?PiyQ+>^o7|!u$=jjDe8M?wfa7!{8W?rzRhLc zWJJ;J`Q5cm(Tr?G52#o4urx&%Rw#NRe*TO+MT_w17j0JbN@5i&_#$S+Fq$>U!$TU;0a={QSfkdGE31f^A)V)v=~eRZrQyQj6ymHyn^o$k25FPQ_aruJ@brz$D-LUL z31f9GEdkf&fN$q$@MiGqo$R}ha*wdRnmlzYG}w>~sN0&sc} zc>T9v{7Wko10Tg!%qUhqUoo&}?4DJMJ=maFO-!*THYxTTd0(RJtC@TA-2*v_ zf#qZ4Z2yyT|ISeCQ@Fwx)bs5K^CRFqbE@Dd@Sa`aGRb%Z`@wZXhZbolCk<}H_K9$- z{4D0Bk1P2d!QxB}T^iBQRphyDN<%kt{I*mL{gHC_X9VNI9%uVWIMH)68hWWjLrv7v znysNuI9DHK;&~bxBmdi*G=$zW^kJEXK80I-F|DC*;f=FO6yK(rF>7$nohIOWvx!Ay zD8Ap1Jn&HX(l0&nZZzSDSlN7>m5%km=jT}IQS%f zPp;xs?5F+4AFoy%P7!~O`Wr%uze1g%G{x8Pzk9Lb(HX_#Rf>QT@KJF0+%zTE zRXBB}hR7%f4|n9=YLXdzpne{5UD8r`WHNo9K%w4B}1M}9HE*w1{%fTEim#IZxrSPnx^ z$=MYh<(%yh|3cws18aI<6^Mn8Jcfp|=61gQx>zI6Quhng_ab`G->Bc?Wt$hE zCw+#lgx(Y>VQg(0V{Cg_XXFZvyiVRX$@ezb{mwWV71unm8O>@HV|F>_vAbVvqmi$; zu5YFo-&@Q$-w3)_7W!8e>!5s$9+rzPR*gQkfpNkIGG2H&hvB8=xi_jTQrtVXJ$2$3FaRo)J`Eoj%9@^ab^U(NG(fX+G zqL>mTFDP-@ElON*D!L%`T+Ml}tz#TDWy{d&ZX7{N;PT=uNa>?&;HVG-B$S`ay~R_M#u(g_g`Ud_9T2{1|#O z*Sq;+G-$N(S!ml^M9`_vMYGj5-Y4m_1jUGsugO+PFi}U{9QjH#5qR~Sx(CGYAHG1e#8eM>Xe%O{I%4fH0^jB!? zhok2okwxPAa#}QcuyrbZW0cF`IVc}}FviLYq%q*)}c_oN!qxJFOr9>AcZCvz<) zbN-VjHJY~*>2T7yB(5iq`tu$m)svb?p&yg~d!LUvkMp{W%TkByO4jMReU7x?oXK7spw>`H(w?(%}w@tTEw^g@Uw_Uekw`JP2+jg>zyREy; zyY0IVxG%U*xNo?RxUaa+xbL_Rxi7g-xo^3Txv#m;x$n6Tx-YsyKlRXyRW;?yYJfu*cR9(*f!Wk*jCtP*ml^4*p}F)*tXcl*w$2&$F|2d$hOEf z$+qcE>c5?HCF3Bs1UAgJ%r?!o%{I=q&Nk1s&oBz$)Hc<& zRc8gZ);8C+*EaajapE40KgK;SP|{Z0X4`h#hNqL)Hr=+}Hr}@0Hs7`%ACUb1hkb&5 zgMCDDo(Dd|zQaDmzQjJozQsPqzQ#VszQ;buz9@`OvTw4FvahnwvhT7FvoEtxvv0GH zv#+zyv+uJHv@f(zv~RSJw6C;r zV-8~vW01es1enCwlpJG`xpZm3mFrgPo2g{#!ALa zC*}nh%2>*n%Gk;n%UH{p%h<~p%vj8r%-9T!W~`RPY{qWJaK>`RbZ>#}jPZ>1jQNcH zi~)@WjR}nnpPL}U*WENM)6OKpHLubmDsr?ICosIjOqsj;ass_K2pV_IWdW89r40?cdd3kEh8PGaIu!N$hO#>&Rb#?Hpj#?r>rtzc_oY-4R>Zewp_ z@M?}3lN*~GqZ_Lmvm3h`!yC(=PhMktV|-)%W2wj3-yFbPU|+U>F(=>%JC+8V0q$T9 zk>nDe!YRxx%rVS0%sI?Gy7@I1c@<7#Zeot|1YE_O<-vx4!_)fR9aa~^Y_ z0?uJBWKLvmWR8?IC*VxxPUcYNQgAABtHH0~SW|E^b1rkQx8PvrVgqn8bF)_Rn5#8p z1l-LW&Rou%&fLx%&s=XAoX_0P9MD|QobV*L;c>(+%oPuTGyXC^;E=mf)|_(Nf+WXG za!qs258-mNU)(%Y0Ixpk6bn`@hMuZMe^gPV(+lZW8u=IGBA1f2a5xI5=fa(Q!l zb9-}qbA5CEGbndbPM`%?6RApt-+duwFheu z4QLThp-EVqs45Gzikl|`?ZO(yMVpn>G|oWVI1!CwAzH^_XdVaD2O7w(5i}8wZN(f5 zG?Kolwm>s^2MuNn4P^i=B|H{rD=$_BTFYZOXfG9k24gM8n#|?ZXf$Xq)@o?yN$n;F zedgf0K-00dlRAyogAVk4YM}jml!+EZvL-Z$9@L4(WUZ(H&B)qOOt_P3?4n4ZojHb~4S}YXMftr)(Av_`+|bj|-mJkzCR5Pl76sbe zAR1jb5@>eT?rPES7&{xehvT=R?OEfy9IbCjNud2%1GE;n0L{ zxjo0Ws7Pv_N$vApG*D}yeQ2WAMjO#m>m~!u^gguH+t5(2M@zM)x&&?Y?DRluJ(_%n zpuO%(Oe1|Bb1(|jHH(QLesVAlU2w~o2ZcNuFWQ0N7Fh&SrjL^6lnHx8R5JHy_ zGBWycnof19Q|ENf=@ddncZ7_Lj6s+1eZJ22*Y}V0IA^cD_S(PJdcR+<*L$tCDR%_( zH0HB^PvRf5h*SJXoMK{>_ysYFAhC%~=3Bg*W}E7WWjvE*n;y=!O>1+AUy!cNCVr7Y z{34b31@VsKiD?`@#r%t5;uj6XFPJ~^eID_PG~yRge}2Ea(%Rn2CVoK-qta__4-nT_ zlVWWJW7c*B<x8oA_-YZt>D0 zb3M+SjItPWttl{9f!|zLkau2%xy~Fl*Wy%jB^R4(9{GC^pZFukTEDNb*3UzHyL-l3 zy{UY=n|MS$dCxUkYsIj&-ZyQnwmkg~*qicy$+MQ9 zS6Iu}S=RDNy0r`>TT6SKwKVLomKTSurLxgl)-f+*HDy*2tGIOf&$Xk-JCKJ|PyX`Zcq^ZZz6-BH-n-Sez_-xR(hG{g6Vu&Im5vut7$(bkRM z@4m9gi5cIHU0u5xJ3JqIO#Tbl?AND=O_W%7M;vy$*1A7PvhFc{ze%?4A8V{93STk1 z)OzBltmk0j*+;moCl&jDT8Z^!4qML!i&%FMzj3YCdTznz6z5pax^nBO@L10?QP%UH zRO@*w&w6;*Jze;Z0gm~Qy0%bul6rnhvEHb3>z$2HjL)>*gGk94)_WYjFP(i$=UZ=f zwDn#*X}$lz@7+*my?5Xf*RCMGpKQIA)5QBd#Qn>xcLT?{q8Ja*WWB+a*8361ZgE@h zWP$bm6l=YI)SGuVeDGX+`}{)lE{HPkF=ggGDcZbe@XPb^URZ42%klFo^UQm**SvRu z2}-NX`w)3ga%>H;idXsnPP%zr`1o%8dN9VkBN^uXFUM}>96yX&D&-;FashyQP7+1Z7Go`w&>>366c9I_khN4I|dq z%(0!+f?u%mU+m>p5KaE@8A7HZ>a9P|0^IS`<|Ij4ny@J_}&$0efMy)>s z+?F+E{TG6}E(3R615Uef-1_gxwf=iOj6+GW{tD`-+QIk~@Z76lwznzQ!v9Y4`-iQ6 z2we9Gbzm@(aA?D{3-dakcqMOUzfk!hDsf#3W*wZ#>FaoqBL?0eBZ2+qnb$+W-y*&we}~ zTUjx6tP2J7&&F?0!x4`^? z3iH2D-J@Rfe?DgZt?d7P#{B<_wt=W98`zcKJ!Wixe&ejyG;9M$z#arM&_~Bty z7C2(W0x1a=SPa)oFJa7DEo0Xxf9@2#k8+oC+?7j+MdVoE29CKE&UYv2UO3_X{Qisl zM=~w&WU2+QiGi1*El@XZfj2!CXr%rYc%nPa0zKsW{1zB2v%rUx|CH@5^%j`Ox4^bI z)@5q6!2fbB@cRzN@KwNh6B*Oj1P5Ni+RPp}Fvx zH{%B3%*%_I3qkoS<1Khio&~S3wP4{G>nX7Bu3QV2lv(h8j$aR-uYhkqPCZX&Sn&B3 z7OaK0zgi66=lt(bcN3|N)LCwkbshZdAEev|=@$GX$%12?dz`wzuD9T}5)1y2&37L- z@60e`E1$5?E;$z3J%;tOH(4kiO=W*Hiv!<8vp65^ViFCbgz=ha8mTX{PQ^mjt3VSu zBh^An(KVJOS?K?$BWE^aKhZW;ge`RCVhdf3u5sN~^b_=wTgn|h<*tJ)bngnrlU7^k zfe$QH&N(YiW_^vTEL63QF{sqX`t_mOQP$#^M!P{Ld3!Ee&O9_7QX9v2u-zSt=JPw> zcAc1O?Py)MH~4hE;`lmXjdoBvcZ!JSQFlh z4t6Md82Z`Y0yek|eeK+97%xm6IooY87rpCJbThc^;FUdSY~-zsXWa2(i+qRpW;C&) zQr2ui16%VPF@~4X>*`n|pWk)pk`HpO^83*CZbailZ+kqAG0<}v3yr4sEa$G_xR>(K z2N$3derbc)qQN(>LPMO#7;6ujViD`ta80e9jK!v$`*-w5j_FB6kK{Z)u4BN3R!P0# z4XnG9%$RS^`SC93n%v*!4=gf1{PXpU86UI33CeHfUMKzNqj8Km=bCXOG zKIj}Y;00*GGw8x03m;yER*YtR#60xl573gS`)KszV|Ji5*PuI}fd;(=JsNE|Z9Y0R zb)AAn489AWhAw=17`^*kwC^4?@x|!lb!g{tXzFV$d^Vc#vUCe)Rap4%T+jcZNoS)g zpU1Hm>|)`Zr548DhL<;5_#)16@mvdELfw3`IlN-Eg)ifpFMrX(S8T9wp4-A#a=lme zTNoS=&S(4TjTXMhMj}ebcao3%S3W zGf3RuEj1Rtm3zIFvPI zW$toWxOfhU^B32Xwp;k_lSykx+#6%%!}sv)?%{s#xrM}i-LrudAWe{{=iX>i{QsB8 z)4oUQvp4bLd>0Q|FFcgRX^Z(zWRtb*+(WKF+xZ-HYx?_ojQ)z3QHI z?|KG$7J4RnHhM;SR(fW7c6x?-mU^aowtB{T)_Uf8_Id|;7kVdpH+n~US9)i9cY23< zmwKmqw|d8V*LvrA_i6)b3u+T;8)_qJD{3=pJ8DB}OKMYUTWVu!Yie_9duoGfi)xc< zn`)zKt7@}qyK2K~%WBhV+iK%#>uU3A`|1Pg3+fZ<8|owKE9x`qJL*H~OX^eVTl;#P zzNS8>zNbE@zNkK_zNtQ{zN$V;-&G%u^kwyF^=y`m^?lg@*#g-F*#_AN^wNmU zknNBSku8x;k!_KUk*$%cSSuCdoF*M#)ymX32KRhRK%6rpdO+#>v*n=E?TS z2Fe!7CdxL-M#@&oX3BQThRT-8rpmU;#>&>p=IR^pvca;&vdOZ|veB~Dve~lTvf;Aj zvgxwzvhlL@viY+8@&WP%@(J<{@)7bC@)`0S@*(mi@+tBy@-gx?@;Ue(`Jjj|l24Lv zl8=(FlFyRwk`I$FlTVXxlaG_Hlh2dylMj?Hluwjzl#i6Jl+Tp!ln<3Jl~0v#m5-IL zmCu#$l@FFLj>I@PhkUerwS2aGw|uyKxqP~OyL`NSy?nlWzc7HXfG~lufiQxwf-r-y zgD`}!gfNA$1sFqEBZ4`EJ+^^Cghhl&giVA|gjIxDgk5@z9V{bEBWxp#BdjCLQ_Fr~ zAYq}$!9>DF!brkO!c4+W!cf9e!c@Xm!dSvu!d${$!eGK;!eqi`!f3*3!fe8B!f?WJ z`>-u+CyWQy6XuIxKVd*&L198+Lt#W=MPWu^M`1`|$(8~KTMA5y6?PSd6_ypIy)fRvxWc-^yu!Z1z{0}9#KOkH$oo-Fm|55v3@t1j!PLUm zTfx}E+QQtz-ooJh;B8@YVRK=0VRd14VRvD8VR>PCVS8cxyRse3FYGT4ATDrm-JkJ{ zIEO2oMja=XIvhe=;?QA-TZm)qG2?KKD0mATB*I0+NyJUWQN&fmS;Sq$VZ>#`X;&Ad;e00d&gu%xOm@B6Am~3qQoLxeK^M9?&9#x zaQ%i+hue$ei|aoG=NI>v25<}er3vK0@h|eD6_BJEq?0ENVIgG>DR;C5X$-rhp*h5& zJ&dD4G@wPGO^l#d1j+06qg9ld_njqSc&FwIeGZoh!!GE zbfdjUgOL_fhbHqJ+Ke=sf1%|_ zvypalL!+bRTs-AyJ4-oMTF-Gy9PQ_zEVLlfY|ax!?BS<0M=RPA=V(XLkfbF^Q}XWds-#(^COI0`!5kMqjkZ-5(YT~_MKrImdNeS! zFCQ9M2Qi3^Xlc^Oq?JA4akR6w=w-JLqp4BuO0>1*qpCLU*u?e()gbHQ}Zk1R~q0g=zY=zr444I5%TUmC)cALmZ2f$ zIa;DL#aZ5n#u(8Wr8&Nj_Sj#H7P-jLB5D^wa0bf2_pOSf#ZVrlGwSI2vpg zTI`Z3>pX#2#u2I3d0>up?p0`=e_;;A|0=EH+iL6hZ>@D;mpb}LZ8g^MHt9bV*6|c& z=nEZa`5m{VTE}(q*0ExTb!7XkBZJsS8nKH-DSxgZ)x;W7t64*eb)dFQntMx)xj)D< zci*(R+lWuRy~NzLW9F`+OgVEd?j`TmMdrSy$@vDx-}$vPbEoo~oNVp`s?5D7Ch?0Gh-W-j zXKm{;t!*{=*Au(AJjvR!8?Eh3@=h4Gw&WGoHjghvYYq;K8Jzi_wIAN`?MqBGM@z(lKlC|DTOylMyeETB9S}!F1jdT)mj3d*m^}u9n z-ILhHPSMt~ebQRErj`%tti@YmEiJj$QctYnS>{#z+hZ+jD_J`#o3*3jtz~)0S~5$m zsXv=&3~=8=KbTXd3KRC{YuQ@+Z1csOdKLOYE2!BtZ4)JFEugug1H#$ zm}_wd+bbKHdojV>i+JW<5Z5?rCEvXuwy|fuHAS&r)c1VXVheLG@CBV~iBmj8pLr^d zcpx!|2KtpN-#Wd-`i4fW^Rr6p{3g>nf1=OMsvIo$j`-O!6)m zwXQ1)tZNnfZYTb*c8WPyE3K=F{95+EMgMLkHqleYx=xh&G|Rds=;J?7?howE>`K<9 z8!^uz*qcSfN>8lf+dCoioQr+A7#o$JW1gFe&9kP)Joh)5r-HIPJI^cZ-wE8oOynqw(i{~tUI2#!@&*Ky|B`{PbjqRB^lPu^Xa~jeR++}ntZpX zS$7HL|HbwbE3vo4C|<{|ZY;tM^WR7Q2iV{-j`^n4x_^qtZr51P?#b4(PrdadV%PDB zJ=mL`(<`m#Y;1c@f%RODPq;3{dTuSZp1a4br!0+cP4K@uWIeA?_U(M@X|1)MZuW)P z{y58e#_vHkDZ=xy=NC#?*)a{d)Z3sy_US2 ziO=6fnfp_%_uui>3zq18am0G-8?1L@Iq`kU^e?jBp(*A};3vOYZN1;8F&<#jywNr0 zos(nU1pMqFwdOq%|C~BuUVN4}Gr_#);nOcA=8*3-?~Uxc1E0P&!@TR=e}4O#V_w16 zH{iRQi9d8v)?aDf_o?HvCFY$dGw=5!=KVFn`l55KZ;xv0+c#u=iC~f=7$0$5zV)2~ z_Q(KR{2i>Ze8T#!m-?9H^Vi8dd*1sG0X)c&*-jwwpR%`vVv;Gretp8N@Ed^tpOB~^%YU|HS zv;LJ{>%W=(cY?d_18=S8n8)~~&GpyjTYvqC^}m~G{cRlE6UVp|_I(ht{?8U!|5qi< zmEiw0I5P?i8B=S%xC!&^pJ={Az@tZ$neSNeQZoc)TM@!B3bdmX91h>AHZN3d)))wmTm|=b~^$gZCMyD8jn?+0_!F*FA z;9jsW_}3p33myhj?^kI4gA<5J@c-z1^DmAz|EZKYE7km2CDHb+*cg4 zje35HG5>Fjhax^RuzR5m%q_Qp{cCL?vC#(p3Wqq_YXc{c()~7YrpE@(aoYfGZ6KHU z!xd$Wt>Ty)GT|mEa1?mT+7UQQ9o%KL4Lp%<1J4rcc!^_PgXg@>dER6HCOAqr<$dsz zF!>+F+Q4Vj_hqULOmdDNvTWdg@U5t9=0?D&_JD8g?XtkUL<=OATVTPI1&)HF9p7ky zlVdEfB;Nw?<3QH5MZSS}aj6Baa5LYY?SfPb+>*zbwF<_rHCdo^)B@{g;D2%Oz(w%E zG{@7{ShRw@ z&?@##u+Y36tO?O?p`@oRbi_3lI(iXnW1};ih{lj!U?EGi&{^md=iGxvfgW*z*U>C4 zLvy(DMaF9$$GA<8g>K5S(5;kPjXqHvve4Rd`Ce%gV?NO|9vD4NNW=poObYt+P| zpKz|a4d^LR=qgLlS4z=YxQ;f;bslY@o;=pqD0kKt4}Z*9RPsOJxX;j2K2Jr%;h1lh zqUn%+ILAUiXRwwB_w)za($4$Z;4U+aeMJwNgHALT-D%%jY;c}*qd7Ks$Ot+Tdespt zSVJ9sD#dSuscRTxdmQr{KCr>lSKHv3Xi7^PZSb5-#^r87t3vzAq0U_PU5cJ|#qBnD z6<xSJ~io)OW*aHdu)6RJ02^7@F3a78|^0Hs9gp9Hrzxu$47bQdn;uUF#9HD>~DuQV_w8LjLMuA^ZLoo)iXj$@liF1Fi0W=(zcv2L!-OL-rf zSs;&fW2k4C^L@Z^AHD3v7Cs~Izg**Z9b>C0vvn?-BG-iuHn=^OvDj#5KQCc?Hg(Q$ zUHIwnPH3Ug=&QTjgJ#M0?&zC)e2Jck{yDeF!uupLJ{)~@{}Hs$7jCiei|EcTC6G9GZM}v0 zwm}#>AAXs#uT)#OF3ZBNMv>|){2FCn77K5PCl!(?_s$&Ba?;Zz?zNG9jXcZ7V$#c`Fo|Q|MV_Ij7FeHRxI**Tg(b+5W--MgNFo`s%?o{gT7o|T@No}Hedo~53to~@p-p0%F2p1t0I-i6+Y z-i_Xo-j&{&-ksi|-lg8D-mTuT-nHKOBaKcQP+L%&P}@)&QCm@)QQJ`)Qd?4+Qrl7+ zQ(IG;Q`=J;R9jS=RNGV=Ra;e?Rohh?R$Eq^R@+t^S6f$`SKC(~P+w4=P~T7=QD0G? zQQuJ?QeRS^Qr}V^Q(se`Q{Ph`R9{q|RNqt|RbN$~Ro_(~R$o@1KALByKCZs5KCiwn z8z5UCn;_dD8zEaEn<3jF8zNgGnrGwq7=0wqHI#zCb=fzCk`hzCu1jzC%7lzC=DnzU9mW z$JfZ`$oHhKaeR?{l6;eVlzf$ZmVB3dn0%RhntYpl9KKFIFXH>;1LX_l6XhG_BjqdQ zGvzzwL&Nw|eXm2lwFe(7Un`$0-zy(1Uo4+2-z*<3UoD?4-z^_5UoM|6-!307UoW38 z-!BXxEO5k*h_Hw-iLi+3)n>% zCW2*zX@qTrafEe*d4zq0frN#GiG+>ZQw~-VW)gO)2SW)<2~!DMJqgBoWZ1!6!d}8) z!eYW?!e#}OzsBWYHeokmIAOVEOB`$`j3=xo%qQ$8444$-U_xQTxibz{6lMfF3PVP) zq%fthr7)(jrZA_lr!c6ns4%IpsW7UrsxYgtt1zsvtT3&xtuU^zt}w5#Z$YYqg@uWQ zjV}OC3o8pV3p)!#3rh=A3tJ0g3u_B=3w!V4jbL$M@(4B;M*kA5F3c|ME)4G*b}+rL zy)eG8zA*nwV1IFdC*T6&1mXtb2;vIj4B`&r5aJTz6yg@=aE!QyIET2$(WJk^MZ`(O zP3D$6Tm{Y|?h@fJ;xghiV{n^~;5gzs;ymI$E;vvlTu7VpVz(GE^D;Twm^RNpVVXOL0tbO>xfi(;W^fE-FqcZh8de#8t&v#a&}+BV1OT zHo|Siam97Tc>{1?abR&_{BDFBizACGi!-n1R~%Yg`erz_xOE;J`y#luIJdaBIJmgD zIQe094o9C$ncX-psv*MR`@Tar5U2liH5|VZu1_37oL}7k?N~<(kS6d1+Q363X$5z& zE$u)W!WA=)rXX!0gF2-(9GCBC4~a3>w=Z=`lZeiXXcQ5xVxk=F0*yl&hO~?>G!1DR z4aI03%&mB~&e1;Bdn}@b+<_)?W0s?lT#DvFTZ?EXr=fkMPNJ!>Puhw!7HKWfbrJ0) zqQQJs>}WDWXfx7i+~_;fY^2@1IO=FQ|DNw?JJNV=&PDUdL;ERoG@!HjJslnB1T>x_ zvmLESn$aF{5e=zl8og;-oue&DW0Ka?J>h6iZ?8m)N<@l&@|vSI5ykJ!c1SnE7F!8(slw$4LSt#e<>%}%$D z-$~nvKYY2!IzFCn9f4Tu=$N*ScSo(`RgZN%ON`=?BI~$6%R25%WDTiF>&PQckyFeX zQXG5AjCCB-XdQ!h`BCWbM{ z91D-z+M9?;yw3J>Mb`dEowb*aSo>=7u1~i1%bBxr9x;s!Vi+gZ5Wi?3e&Hd0;U#|I zCw{@)iLXP%FZdty62BlO@ow#(@rz>O7b|Sj+H~R<@qGVclJ8%*`Thm54dy>@I*wRI z5^;?EYHZVNVh_K?TibUYYumEg+TNdUZM|M=W1d6X8_cVCzRubnO}DmEzqQ>#+3O~( zZAGEAojYZ1R$y(%6Vq6bXKe{1*0wwIE`Dn;*HnVJwh)&X9x+!hF^pzn60bAA;yL0K z6|2p4AMuIXn1gXG(*gw9VKQFjI!1a z=48AxpKo6fmw1xc#e@7}*II8N?}~V9Juk*u&zQ2-)DdgNZ?q;v}!Kx;EA^=ZftC%6=5bT&!Z& zb(*lQU$e}!OM`jl5=%HR-#p3Kl~n$pPK@H58uMVYJXaE*C}7_m*p>URFArm9o=P!K zEisF?R-31V{O$(xguLeYWXwEYPMYWYY4iL>d^RS=y1{DQ2a=Mpna7V?_i29XJ{xB6@B%VFxww_;zE&PSp!Jhc3{VT2aFt_y{HNkpL#2wDSe`RG_ z?{a)pUV-(lDzV<8GV8sk!g_i3y^m79nmGK+_`C-8x8Sopln-#+NR0LVcieivX|moQ zDgQ^Vd3Q@T?_9-@^SO#m6~@I`M0{wdpACO9safg zKmBx!d0(R38*%2{SY}=~+kGYG4NsVNbcK1p@S69#Wb;m!THj7%*0+1Q^~G0M-+?36 z_g8R4N}=_g1YS5}()zN%6Bn+qzRST5*Sf5)aNPRVfF1rxx$+Fwamr;*Xo2;;Qq0&2 z>ca-~b&`CPA7=Yg^1o#Nca)oEEv223z&K!_xue#9K%@01m9f5Nw)HQb&w88SrN4oN zvYD%Taf0<<>9u~IWB;wm)_>Qq^_PN;9*Sq)YrXY9mtp-cllK;7-ve*C+2`eVkTD`3 zQO}kf>)*=pKcrg!uhr(;dB%LPE0`!m%y)RM`HreH-w9y7(-xU;DPvd8W#95K z^I>Cr*Df*NO$Fw=qtbl$Hk$8&koo>SZoVg}r-tL}z@rUd#U_$_zWKbQ5XX%~neVeH z^L;gJzVF=T`?<<|e}Id31^4cmX#Rb7FlMI_yjue9&0s9g3^cA1O8eQ|znZw=UZJZ&CNgN%L=7Vg7Ef`3Dx6AFSyA zWWxNPFERhtD#j)8|9@%Bk$^kw3J;hwV*~r9GB3Np29m05;7EAEaZ@&Ma=Z-~!2J(gtp+uz@>CCFDI&Yy%H-Jnw&?Is;BJA8rCCX&8a4)De&1 z*zQal@W;YsLU0<68>8G;l=-&N26n8lfnVWBJJnhs1`al7!UFr0SYSSU>(Cm;fhAes z7`W78I9Pf;bKluMyV3&L9J9R80xO0s@DKRgbyXI)34V1ubrkdezA+0t$T9yeu)yPW z7IoRxQf{Zx%6jjk;SrjAbLgHwqp&A3m7O9R4)s@>9nbl>3I??=vj$GkL$I zSa9e07TgV?@gWP&gKs9nGZ*Aqa8a@akDapMiE!6bi!FF2d^L00g6G0}bJ%|g z9Q6vyU!85iRg^2Nu;A@**}EFyz?l|Y=e1yYmcxyoZVB$~t-@c+M^Z=o!70{m>~!p#=C1Wn=! z>c~g$xGv5@Hw2vd4|kkqp}V$OsD#+W{p@=Xec|D93q6MZ@FdzqHF`qL7~dbg(L%4H zGw}RF8@5~My_L@QO5Ggeq3*teEHqGTp}|pf6Lg4=sbllU=qU@(RXA2_gnvifJGiIm zTno*tXMK$(8;nL*+AYck_c+J~_gZX&`()eT{^&up>%l{c&~#R#?eKe4p$#6p$_7u! zvB8s;GUgS1$>JCTyUhkO12&lTB5Q(BHs=iVBXqD!htZQNSwkJ|=vwMuwUIHl`K&X7 zHgsD9>#t|qVDY34mXxAV%}1+x4b3VQ?TT`bp3E3texE{Pszy_)naeoeV#fP&t*=r4 z8@1?RXh~?YgYP}fdMW5*n~r7;l|mcz)Y)L~muPD9(AK!lL9S_-G9S7bf4rIX?6;%G zp?{6D|Eq=QbSbRq!m(3}7|XoC27lr?{Eut=^%pcgG{l`eXnyFByQ2Ng&PESJPu+6| z`rt-%Lh=&QEIjWg)}^UOOI(4b7=yNmwwk=$!V9M?d=#2#3VPykls{o9YvlB^ZqB)k z=^jS2_#=nQrJkdqZr~VwuUBvMh=cA7{pp$a$yhUiIS2-H$ zH9d@1-;TaIhw<#xbJO`~uavzN9rLzz=(3#u&PvA8ql4aE#W?--7G6ue|13n?y~M%~ zoPpkZ5W4U07B2q)Ew~O{_;xhnCFsT6%j4AlWGPxQ+fPrSFTaf5j6VDPPiW9jpht7g zTJGuP%h9YS(XTo7^(eG$&i@8=zQuL}`t1hJ^^ON^eW8Unat-fMwmHMX;GA$P|JxQ@ zcvG>3+dD1np0RLes)f4>EbL)FHaXn0&BER|3-_g1xS!|d<9z-bEzCEb!+{bD2f5b} z*U7h_!(q-j#Pb=Z-20UOAc6FYg+Jt3j&Oe;t+w#Tocj~%`-HsFhb{bRriC|0lNv1i z8Fi2Gf2`KR|6NK#Gu}eIpU)z3J>&C9?EB(SQniJ@oJ+cgw8O$*<&xYMo}kRcEu@fz zzdnexiquG&w(wT&bt~7nm226$*}~tdH)fuT<=ippAXXM)!PY z^epsD^lbEu^sMyE^z8Hu^(^&F^=$Qw^{n;G_3ZTy^e*&H^ltQy^se;I^zQTy^)B^J z^=|c!^{(~K_3qUM)E3kx)Hc*c)K=7H)OOT{)Rxqy)V9>d)YjDI)b`W{)fUwz)i%{e z)mGJJ)ppf})t1$!)wb2f)z;PK)%Mi~)ECqz)Hl>e)K}DJ)OXZ}9z4kDQ|eplW9n<_ zbLxBQgX)Xwlj@u5qw1^bv+BF*!|KcG)A^KFA6H*jpI6_P4UjF6O^|JnjgYO7&5-So z4UsL8O_6PpjghU9&5`Yq4U#RAO_FU&Uhdc`*(}*E*)Z8M*)-WU**Muc*}U1=jt!J8 z#3sr%Mr@>PrEI2br);Qfscfojt8A=nt!%DruWaxzwpcbwpun@wp%t_wp=z{ zwp}(}wq7=0wqHI#zCb=fzCk`hzCu1jzC%7lzC=DnzU9tr$JfZ`$oI$x$rs5d$v0g( z?D#7AEcq_^F!?h1H2F69IQcsHJo!HPz$1zrpD5obA1PlcpDEudA1YrepNemlkB#_R z`CR#4`C$2C`DFQK`DpoS`E2>_K76=*xf`F}y4msZ^7Zoh^8La9HDCc@0%3#4zzD($ z!VJO=!Vtm|cYrBwAzv6nSVNdY*h3gZSVWja*hCmbSVfpc*hLsdSVove*hUyfSVx#g z*hd&hSO`ocY!tyr!b-wS!cM|a!cxLi!dAjq!dk*y!d}8)!eYW?!e+u~!fL{7j|@8) zPFQYjri1N-@r3n+`Goz10fhyH355-X5zhoG3Ns2j3PTD@3R4PO3S$au3Udm3f!QD;?}D3@$7#OfGDGD0R<|b1?gy2?xXPR2#wc5p4f082>A^zog635A?}f1?r;%t5^*Gyqq^CP%o_MmUtXR2`hE=1-0# zuC*S{bsyYI9PHLihm(n$iKB_DiL;5jiNl>RV-Zd#ZYPen?+%Ca!TrPmBV16NP~1=) zQCv}+QQT1+Qe3iuByK5=DX#f%IOhX!&wCp!!bNX_lU}>T;i${U9L_54Dh_)BTvnV` z-1b0@7uStl5#he#z!5GiPAqQx5gb`uS)ADocNT{hmlmgf4sI=u{ZPEax$h$H)?}SDG=kNOt{oqS91!)1p=mI{phE9^S zf(Euq2`V_HkjJ_0jM9&X}@@HX)5dTE&6mj&>mp11-Zl zU2YL=;|ny7QM8Wml%svPV;n8y4PqECp^ZEpYu*Ypk#+2sc5*B6j8%)!RLUG}np7{^lr*Xh?5pFy zw5!KP94+hK7)RTZ#wD#wnwPY%C1_wLFuqorn6xozWYWs8^-c_<5G~D%rk3DnYwiZL zwkbz*llJy_t)s=2#5&sCsuhk_mxE?^w#(7*q~#@ZjI=#zd@(T*&CjzPeelaEw7_vR z!4TSDlcN#7Rf=Yqi*}gmXo&X>qbZ^>78E!dpYI%1%B(C=d#W@_0~D7+B$yXn-*L1tz$DWig1#3bQ6=_(hCuT1K4XG`5c=u5lPKjeV8i1MqAr${H|m^#wElrvc|3Lbjlq|`J_Z^+n0SYW9Irb-CW;Rn(MQ1bA^dl zc!*bQberqdXmdTY#9R-Rm9sV(TFVQ}yLhb7TJBGTmP25lZ z?H+5ocEp-;DSu9kHJuV`om*+^wDYc6#3JSpU)Z1id3Y0ZCste6sj0*b+0Lo6uDo39 zDxgo^K_C4mWgp&QT~Ak7*UO8n>utAnwb8e|3Gu55IZ# zPiIb6gLzW01*fbw&r)p1`6=c>uk@@W##xkXo_h;fpRUn7kI$Irg*5ZjvkmU>bh1AX zZJv)7nP)u5Jl}ErFBR7P7vc(W*uw)t*1dqZ!?9!5ojzgRf8+Q3QR}{J*t%C@BX6Y) zv77D(ORc+drFB1_Y2B|SS$E^Kb+@y>zskDbUx7VMwC=6g#2;(0v$@z?Z1X-Y>~B7H znExj-}G$d3Pp;zelQh_e(Hu z;-q;O;x|+Ar>Eg-Gn0t_kDK?>I`dx5_Dz&o?KSUxMdmF}H1Fe-eSW2R>0{mv)y$8? z-*&sr8_Y8ANXWcfsQa5?bG|X*wZ51%>)Wf=`VNS*z9jtpQN$-sByS0r;cVg-7o=F< ziW2L)+GBk;#aQ2Ju)|umdFOqPOjT&HHI-UlN453!vv0`H+|W_$`(lE* zqB|ICG0m6@@Xv0O#3jHk2Y`2yTwt17uuZx3pI%`7;LZN?z&RIF_9~C{Ur&Bfj`iOS zM!G*_{SUMK{W2F&+gBcv6Y^419M^I^$I;8Mng!f25f2dT=E19$#@3b9K|qR}LPm ztYHo>$Nz`CH2A1uxfeVst;1W2(m2d-W>A+1RHgNki-`Y#yn|q6F zpnQo9JO(d#nmV|afx0{!c#HBI*>20Q0Z$rZs;DCzV*?{ojJ5JJ=8AKD%Q<#%+^=vX zFk~PGZUg=b>b+gycW!^wcv7i=87B(Ud8@vcUbU-Mho6rV!=B(_8#gjt+U|zJag7%V&5~_ z7OWxv<#NXIajv(U;J}#{bPdCabKu4yIC2(TnfgaKH?}DFd8`E|R$K7f5exo6z0(zp zBZTwse6)pP&qLQh|1 zq36*nUYf8_9s0r>@c#|57TV}SqsT|An2TmXJ$+ed7~_oBTw|dRcVYbIde*C8-*}~k zzD}?Z-<}O^kG9ZH=ncP&G6ob4W@ogW81$i7G?chE(NUt&Q_xZltS0s_VuK43Y;fVZ ztRG%xgJ@TSi_wZsnavnfw4XD{{~MalvJxBozj7PA0L|zk^c(Ev;1$&S4>Y1{$-ACo zZ(3u6w{ng;Ar!aC?5+u*u-8(e=6`q2_}By^j{`_YxC@7XkTrmc*zrH(qT z^YuyAU*{a}B-`LdHyTwg>$Gz%k$A&TjNQEjEo;gKg9SDi-op4^uI-~Hbgx5gaE$ss zA43mIM;EI=9~-s7?-w#&n6f`}&%bO(OIu*!ozM&Q4d7kT+-9S(&Y5N5z2;ju9xZM^ zw6}R^f8h3T;_bv3R$F+%jf`b3WK1*p$K;~zrK9mJv~b#NG`|VPLVH-p|2f7-Q*P+Vqhni9r-qaWVOHLu-d;nF0=iWf6xoMZpRH9W-e|8_IRdy6iam>?GspN$+qU?~?Z(I%cy6ZMOl9 z_fhm-^v({hyK518a1^?57=5@3t#}2xaU%LL#{}6P%tT-2-iB+@otL6PZ%2>joS*Q# zM>n8ZXP{pP(6e*Uwz>B4<>=nj^%Z3&8qv<@qo-4TlIz$;UEkGMcxu|h+qtJ79<}g} z5exIpuo6NWgFV5#fD}DY-s0?Z3umE z=r7|ovCCJ(kd#$vgx#Z2= zYD0V1*--pdHnh)N5_Ru;jScM=Ln1%nG}4F-?N9mrf3cx?#Wr+66zM(^+w-Y^KIcF1 zd=keVbP$O$2aVd$!AFxQcd*}v5)(*yq$*O_h7O4(QTHJQq)HOU(ocsD*QG&(le%?|$hmY*%Ig{;*TOaF+9KD;wd$I6?Yalui|#3M zZ``BqRrjoW*E7(w&@<7q(KFJs(lgVu(=*hw)HBty)ic(!)-%_$*E`U=&^yt)(L2(+ z(mT_;(>v6=)H~I?)jQ^0>zzm5z1o1EomfD!wn%bP& zp4y<=qS~a|rrN05s@kmDuG+BLvf8xTw%WMby4t+jzWRXrg8GE|hWd#5iu#QDj{1=L zlKPbTmin0bn);mjp8BBrqWYxzruwM*s`{+@uKKY0vih|8w)(jGy868OzHESOfoy_o zgKUIsg=~gwhir&!iEN5&i)@T+jckr=k8F@^k!+G|lh)gkt&+`>?UD_XEt5@?ZIg|Y zt&`1@?UN0ZEtE}^ZIq3at(47_?UW6bEtO4`ZIz9ct(DD{?UfCdEtXA|ZN^5+R!3~M zY`1KSb%B3~k(@(#X5K1RMqK1aSs zK1jYuK1sewK1#kyK1;q!K1{ysF0M(wO+HS(PCie*Pd-q-P(D$Qa)3@Q$AF_ zR6bR{RX$d}Rz6q0S3X$2SUy?4Sw336T0R@!Egv57T8s z27O=zVFfq)gdKz-ge8P2ge`e$4?9@s#9Rj(2_p$B9SUX=b`pl#JI29O!d74`VXX+}67~`Xn^^5&GGVh3 zFq*L10GLhKO&CsCPMA*EP8jdyatHGX`#s+1V8QiZLSaK;L}5i?Mq$TmcQ{y5m{QnM z7*kkNm{ZvEq&NqQ3X=+(9>TH0s=}4oiu z@rCt;`Gx((0pJ4S1QBi^jv%fe&LHj(hC}$^65Wl!-~s_(~8@QQ@`o$n z=p%6T`Ed3XaCf$^@;Y4pJUG3$y*U2jN;rSL^(Rr^0mF_KAWa}@DxwiYv;t`c(hi1( z(Gr@_6l%~G%F!5BI$FcC36A!#o_%Z4B35TQ+Qijp6w)d#NOiOeX&BNnj-sxlA~cS8 zN9%|o?(jRB$&cCQ{bm$RBo}RD#JoYayU|KqXfGSkPNbnoOL?5KO6~RFtm4XX0oHrq*hu)t4W-4v>RzSI}^h|)9L-L!qIp>K_imp)BC3eBrWJwwx6#< zBdT(=B56iA6w5gu$d5%`~DY}-l zD`{BLvKom;NZWco1+5D`Oq$odl(~)VRcK*(Xkr(Zp^;6YmE|~^StN#$?`Ubeqluxd zMb?s%)+WvEeY7`eaMI$W$w`}gKFiVS9xOwHx0YDvOT;yvTw$I6Vot`rE3Na^ z0_(h%|Ce&y{}J0bGu=8*r0kLL)_KshbIK z>3sg5MPAwx;upj^4x;Q{#58s$Ht~~(_(d)83+7q`bBJHew@r=2BwlIc``zR{lxCam z-C>(-c-wRi@r+aXkN&#p&^p_+Plj#UbZk+q#eSv2UjW74hdkP2(toBz8IubAfh6`Zxnh|^ z@jGdXw1qgv@VK@15Q}JHPQ`0=)>>U|tq&JiYe}}X-jZys`NTLbWdGl4SUZaRBZ*(k zC%!R2K};vtu{+{@gH!ZB;Pin2M;)^b+4 zwVW7dEy?`Oi?Np7%dB~3+M2hmwC2x7ta)&WHFwrnb7PD(|A*Mb6K-oRBTjKA|5r6I z_kvi)-zjq%#~s7IL!y~`k;dGM0_I*+@!bpJ6CX@5_ac_L7sM`J%d)0t@~o-6$eQjZ zhH+yhaX;b=Mi_?bIF-X$xn>l*TIrZ3)IWnB-__a2Y7t{VDSJ$<%`KG;QH zM7Qqxc+9%KTy0(3$@{I!Ji8^DC%)D^iP#C^ik=hMKBMx_b)AZsn}sbXOgGP6)#h0@ zW1dHI%=0XE1YhlG#4faBGx~{dd=P7qZ|_W&nCItu>z;*8niFH)`=?lUQjT>WQ)t~M zmsh(Oi@Kj>UtNuLzf*4An>ePAV~5kM`?FZ<{u+C^ zqml0km0AyDq%ED%_nkG?`%l01J~Yi7K=NKBhEZQ&&U#M7Bl@Xl*lWF;N33^Z%zC$v zTkp)6d3V8I#t}!D=QnRsqj`_UPo|Zb_l$h=W@QrB$4_3d!@SoKhq$%EiTgj0#5e$s zeY(oLwZtLbm@)5r4d(4wVP0R9d57!GyLpLuzoN|dMdtlAhWU~e);9-#y&t~+kVNZS zNL(S6?NjmTOTh}+E3EHgxApxa+4>5IJKTnkzcgskrwFhVV5-l(#^jU(3Q zX1kBrMYzEFKB3NWu)?)vUx0c+?WN^*mruY z1^*ZY=nEZ)LGEY`Ff%)=xeaxV6Fu}Ab+&df?KHfD;FF%%YxfOaANlTHVj8z zP8=d&p*c(8&hY5{53xB#3!Ri7}C=RONH<+B#V$*c*1p3p-%Uz3GGB^G*rsf9j{VvJ=cV=Y%(Xd=ZzlhpA& zdc;qQ&@8zAKMEM9ISVZVePfRl#&0&VUIkjqycQcg_%s_ljP2xP*71IVv7hK2C#+&D zXut*;BR0tQatFbqgUc48r`(RNvI%|VcN<*6ad~Jf`5SBy>^gWuoDJSgp1!YoXR!_5 z&AHaTW`p;;ZSX-ZRLhx8QN&yrqX-+ws%RnGgy8XJ835*uVqn!)B+i>wLa z-iUrgU0$x$&-DjgHaNss)DO{sKBm6Sr8daB8^mu9e$9C&XBd}TiB>hwi8uU8UB5S4 zcvgyq|59sVd~G-u{cq1{3-5h7Yr?zG!4lBJ(8Lna`;yrI*OiPHuD9?}+bn!6$EC7; zVh!3F`%j%=yz!xoJI-WH7WA`o)-Wdd3C1Q%pF=lW-h^J)!W#M1d&LIEG{4E3`sjEo z*P-#D!QF_Kck^P_jX^`aeaymlQogtpeK3nPXXc_8h8Rzc_O?EQG1fcK6gieQ9j=_T z@RQ^}oy_>`PIO4>c>%4imTP;N>wFb$?{&(sUVOM=1U-}E->tWBQ=ElcIIpW8?K24t zl>6-RqKlI6%|R!vLN5)VoyMY}q9qPd-}@Elt7w)VQU52?Xs`P+_MPXlh5h5CFR6Qi zv~>ykE!*3UM$?^#wi|=S%eDN-xqf~P4Y<_8zjBV>7N8GLp%uH(jaQ=~pNy8g9bFkc zdN(xZ-J{W+UqpjG2Q7L6ZTcRx>N#lEB*!e`d`EHKqd8y70vkFu*M^QOv!UbBw^PyE zPoUh1oIfqWhE7Vhp_7lZp;OMVq4Xs-bSl56ajmCw4NKZ7Aaj z8#)V(eJRH*{oRKC)^9_Z6*hEswhf&VOWI^Z%Wkxxtb<6MHuQJ?pBqDZ*@pfvjYRqE z<)qCvbY4Db+=kA-oHS}f7c3=hu%VoJq0uuG)t|wLfe@S`Tr!p$5V>niIMCwwVs$1vaTsmju++2gM zC2~z%o31f(tsJXs*FET7bWgfB-J|YR_pE!@GtjfpGtslrGt#rtGt;xvGt{%xGu5-z zGuE@#GuN}%JJ7q(JJGw*JJP$-JJY+JJq|@JJ!3_JJ-8c8&F$Nn^4c%**>>4@*?QT0*?#!|`2zU_`38K1e8r%A zMkL;W50NjCPx%_(A|E4PBcCJRBOfGRB%dVTBp)STC77(-Y?n8OG55C-W4 ziwKhln+T%_s|d3Qy9mPw%REjwVH;r_VI5%}VIN_jTgn|wBy1#%B&;ONB(C9EaPCF~^(CM+gQCTu2*76aA+vkALJFr2X5w_rM9JKkLc>k0D>g71U@g$0EP zg$;!f-vTSX3XT+Z6owR*q)kV#r7)(jrZ8vGw1Yu~MTJQ(TjXF=Vbx`93%d%#3d;)9 zF7h}SS6Ek=SJ+n=cvn{h6AK$hFtV_+Ftf0;Fto6=FtxC?Ft)I^Ft@O`Fu1U|FuAb# z!{k5UbuhcIyD+@4yfD46y)eG8zA(SAzc_%nfH=Xz`3^@AR}g0qcMykwONdiMxP>@| zxP~}~xQ951xQIB3xQRH5xQaN7xQjSU1!cr(#BJ6TJ6uPcN8CpoNL)yqNZd#qNnA;s z=?J)!IMlpxhg0oN{ctRCtqA85_xchJCNB0NoJ`zI98FwJoK4(K98O&Bsd9(gt%u`@ z>xuK-0{0UK6c-dHyZ~+}j(Fx0hck*hibIM^ic^YP?#}t(n&O-h?zt5Xx&}D{d=}doSh1d2fn!IIy^|II*~~II_61IJ3C3IP|-t9d6zVN8eEHaCXic;qc<}>)`a__C+Ib{TVoaJlsFU`lSJ6 za?I(ujy7=A3bX?LbH5SoU^mJ{%|}y+cC-a)44u5F06CLg5i~>i?Il9Io+Ri*b zTF-Dq^Xa7>MKmC3LDGbV(T1cExrt|NEO)e{7qc8K>ES4}rI4dBNo%^AIL0MYjs~?f z4^1ioZHnWiQAw+kW`%Yo4J)E$eVvH5g$^c-OInvSuSS=nfk_LKCbqr|jf{B3ZD?iE z%<^K<(1=%@L-{5AKY@InSI@ztj^;KSP3{l$xgF?rU!%!=7UyVmedKQ{amy0?yOiUy>m4odWHiBJ(D{-$cK=jIGn_SshSo8@SW{b-Es zXpN0%j&*2{&!Ry-iWa#JP4cc9N2AOiWgRIrOE8`147AM?6CF)c+Gcz`S|_mvG|#T> zv5p4%@r0v^b~T`pl2?ybD$Vq9{y&I@dUuVZsa})pXsqXQ>=_}8Xs^;>4@QgKyU4nB zt+LKv8?Ey@=2Lt=YMmcWTBmQuI@?K&QPx>UdWN{fzsdjSxOLt^jN^KjbzV_tofni^ zCwQXslyvJnhWN#y?BAEVW)r9QofyP+;t^lCt>Yu&4+AOI;U2Y)M&?x1G1uam5$pIj z<^Gvq9k&z9Se0oVmknFTc`Nt^#w6dsD720v`8}B5xntI`3vr465v%x?SjAYSxrd2k z^fD)-#bxd{nSb#D^DZiz%w0zQT^Z)SvE1BOQvO1=Gt`xg7^h_1tr8U3jT~=5Z6dcBz_Ua z`cb2-A5~BMqJ;QGHt`GQS$r~Xn*z1AsUwg01=}x=*run-Y*RV$i+jdw)6J!}DL=_J zE%)1|vng}RjBPr)!Zsa3ykl=-7`qUQm~ODPZ&Ix7(|T(QC0QGDxZ2*0vbI+WtnH~0 zYoo5Vdn&E1aLU^LA6MrepXYS{|BRx#jv#z8%8DW^qbj4b==!pvEF&tqvZ%12$SNbF zsOl(!s*JF%x~?p%j36o_&9COio#f7)JGplZWBe)&qZUqHO#Z1VFLs{NQ|wP){9`@YPj_)n$U{z<&z?X=pW z#4tK0@$K#+wLLdaZM93(hJS8bK+NJMViu)QzI{Qt>CCs7LYl<(5#)^}PO-aDt;E7x z!EvqeO0{+uskMo?#Gjdy@w<(z9mT%8$^Yea){e?jYjKFRqZYGvRH0f=Aa?P8E7f{n zv08TTQVaF8p!K%Ih+%XsP|It~y{M~G%hUO4d5HMMoiq4ucStRz{Qk64EkBy7mdUIm zbzH7m4qKv@FT%EyG=d2$`l%trO?6jiN;xX#IB454b#p=Cfo_fIzy$`UzW~F+c zsZ;M7?AEJ|>TScm^;1uh{P(KWyLEwjKQCAB-WlpU5SyDrEaDjK<;e}|DB|J@5%kC3u8W7NN5hWhK+ z_i~f^oADt%x#}O9uKvH_JN~|uxq*%9|4&H$UlEr$khptJmcqx7^2RGX1>aXlI-mcS z>{9p&{NFD&DE!N2h3_D4@tXw-KUz$DKTqLR#3btRhii#hw9QetZvx{1@O^JDRrnv% z6&|JTo%r3XnHo5dxWQrg>7((*CpK$fa*+mpfM5S{sRoMJH+!B2uEv+&$p2f@8dx}8 z0}nK4;BkET@+u8Hi_d=%AKz51fp(7T&(lD>LIZC%Y2brh8W=6mz<>C^ceNr1#QFBt zE=7)~b$oCgACzNp%7t}LHw23hm&5X4G-`otYsRFm$(+GyCBQCKVOhegc zIpzg$%WHXxv}J*PHYhU0{kUv*e@1q5KcEZZnFjtW30%8>3n-^EAw5e zHF)ME=D&i=E-2C9B_kR{`xq?SrokJlH25p<7Z`EyKFU5iM}tp>7>`0db>O@P_WiY( z@hWwUTiK(*;UW#bMLrl}@ShVj_-X0a>o3lj5Lo_xmD2x{_AE9-NW%q!10fQ#h+w*MJ3~u$jA2&H5O{IL}8bHMFfsL;vO2y&E;WA3SCJ4h8h*TxdFj-#vPQ#yV*4*6 z8g5*u;WsGLUa#R0c@gSOP}ko$W>c|-H%B%6FY-TY)bK9K?X6Y}`y2b_48;zqSM2}x zD0WP-Vw09Db_)5Y!{yGZRO}q~{dgl|)p8iKHl49+70ipMRqWa(#pb~;=Z`9e9f{qY z&0PKod?y0__gJ1{PxAZwiHiM^{6Dk*g&m4DrWJb~&ez5{x~V5zq}VXWq&VN(jf%a$ zK(P-eEB5g=#kO(You!KH8L#*^npkJ~O4eK6sraGr-`rCf)7J(EE>b*i1DqKCeZ~kJ zxeTrh&qf=GU)ZAfC3%XMz~!$PW!&H_#mnm$N4Q6swV_QLVn%?{vec&C%->1xG z&ifHMz$bSp{`nlmcNHl9zcEUD1C3*Uw1#h@X=J0Fr+bdW=@1|169}iXHPt#dj9F63~rHn;wWK3#E3GOe^ zx=o4od(d*wPQuw}I|nHdXFEk6RV+>9&&_|IcQJ0Xjc>VQSv)zUB{ovxLowHQ|6#qeT;U64py*@ zF}xK@{*eE0vm|2-f;kQ7TSaJH#b{U7!+wUQcICCotR-KD-c>$T$s2A}@+S1NUv5EL zV}I4TO0oue@-B2Zz5|*qRNnt~okxwXUSgHNLqmNJfd0^_OQQnGUyTH-XuT2pU$xsroydn|VR zcJxQCziKaS=zKZ^vjXr&7G1}|3Xs~FixsRaBK7l^F zlCkv5(QHY_@qf}1rB1vZZMRgZQ_e^4Jq7J|AEi#)h!*@Zx-gpU_o?Ts95my%(2qH$ zun1i_3w?PlTJs#VXUfk&&pm%S+Vlo=>iOu^=+i~$$rq#hUcxb#@;i(Fj9W=vek+iP$jx`F-kHY;@#_jmKD zN>y-OznrJk{Dn&0vQ(+cTBUxqN~v3^^ER%hihKNZjZ(KSQRR2>|v{R{jo0M8SU#Z_5M~W+T-(sci&m#>hwS@c!#*N+{WD2+~(Z&+y>ni-6q{O-A3J3-Dcf(-G<$k-KO2P-NxP4 z-R9l)-3Qzk+$Y>O+(+D3+-FXse)l2wCHE=!E%!0^HTOC9J@-NPMfXYfP4`jvRrgu< zUH4)4W%p_KZTE5a^(<@~eLrIZYzu4?Y#VGNY%6RtY&&d2Y)foYY+Gz&Y-?RA@Y@2MOY^!XuY`bj3Y|CuZY};((Z0l_EZ2N2jZ3}G^Z5wSPZ7XdvZ98p4ZA)!a zZCh<)ZEJ0FZF_BlZHsM_ZJTYQZL4jwZM)03Pu^X|rrWk>Vjgn?n}6Zzzz5hD*eBRG z*hkn`*k{;x*oWAc*r(XH*vHt{*yq^y*az7c*(c$f?4vTi%0A1!>r;G~eVKileVcvU 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

    Tbjo_pVjPK_%)qinv5> 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%0A1! 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%0A1!%RbD$%s$P&%|6b)&OXn+ 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

    OJhv1pb=y9rrrxgX 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 ZN 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

  1. -u$St=_BFrV_QrS%y|`-M&?=_~zDc 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

    =mltXYj$h* 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

    !a|Oi8%GC|0D~ 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