def _validate(self): """ Ensure that all segments are closed. :raises ShakeMapException: if unclosed segments exist. """ # TODO - implement ccw algorithm... # http://stackoverflow.com/questions/1165647/how-to-determine-if-a-list-of-polygon-points-are-in-clockwise-order if len(self._lon) != len(self._lat) or len(self._lon) != len( self._depth): raise ShakeMapException("Fault coordinates don't match") inan = np.where(np.isnan(self._lon))[0] if not len(inan): return if not np.isnan(self._lon[inan[-1]]): inan = list(inan).append(len(self._lon)) istart = 0 for i in range(0, len(inan)): iend = inan[i] - 1 x1 = self._lon[istart] x2 = self._lon[iend] y1 = self._lat[istart] y2 = self._lat[iend] z1 = self._depth[istart] z2 = self._depth[iend] if x1 != x2 or y1 != y2 or z1 != z2: raise ShakeMapException( 'Unclosed segments exist in fault file.') istart = inan[i] + 1
def __init__(self,shakemap,topofile,stations,fault,layerdict,source,cities=None): req_keys = set(['coast','ocean','lake','country','state','roads']) if len(set(layerdict.keys()).intersection(req_keys)) != len(req_keys): raise ShakeMapException('layerdict input must have all keys from %s' % str(req_keys)) self.shakemap = shakemap self.topofile = topofile self.layerdict = layerdict self.cities = cities self.city_cols = CITY_COLS self.city_rows = CITY_ROWS self.cities_per_grid = CITIES_PER_GRID self.imt_layer = None self.contour_layer = None self.intensity_colormap = None self.contour_colormap = None self.stations = stations self.fault = fault self.source = source self.fig_width = FIG_WIDTH self.fig_height = FIG_HEIGHT #clip all the vector data now so that map rendering will be fast t1 = time.time() self._clipBounds() t2 = time.time() print('%.1f seconds to clip vectors.' % (t2-t1))
def _load(vs30File, samplegeodict=None, resample=False, method='linear', doPadding=False, padValue=np.nan): try: vs30grid = GMTGrid.load(vs30File, samplegeodict=samplegeodict, resample=resample, method=method, doPadding=doPadding, padValue=padValue) except Exception as msg1: try: vs30grid = GDALGrid.load(vs30File, samplegeodict=samplegeodict, resample=resample, method=method, doPadding=doPadding, padValue=padValue) except Exception as msg2: msg = 'Load failure of %s - error messages: "%s"\n "%s"' % ( vs30File, str(msg1), str(msg2)) raise ShakeMapException(msg) if vs30grid.getData().dtype != np.float64: vs30grid.setData(vs30grid.getData().astype(np.float64)) return vs30grid
def send(self): # we can really only support sending of one file and/or one directory, so error out # if someone has specified more than one of either. if len(self.files) > 1: raise ShakeMapException( 'For PDL, you may only send one file at a time.') if len(self.directories) > 1: raise ShakeMapException( 'For PDL, you may only send one directory at a time.') # make sure we have all the required properties for prop in self.required_properties: if prop not in list(self.properties.keys()): raise ShakeMapException( '"%s" property must be supplied to send via PDL') # build pdl command line from properties self.properties['command'] = 'send' self.properties['status'] = 'UPDATE' if self.files: self.properties['file'] = self.files[0] else: self.properties['file'] = '' if self.directories: self.properties['directory'] = self.directories[0] else: self.properties['directory'] = '' cmd = self.pdlcmd for propkey, propvalue in self.properties.items(): cmd = cmd.replace('[' + propkey.upper() + ']', propvalue) # call PDL on the command line retcode, stdout, stderr = get_command_output(cmd) if not retcode: fmt = 'Could not send product "%s" due to error "%s"' tpl = (code, stdout + stderr) raise ShakeMapException(fmt % tpl) # return the number of files we just sent nfiles = 0 if self.properties['file']: nfiles += 1 if self.properties['directory']: nfiles += len(os.listdir(self.properties['directory'])) return nfiles
def readFaultFile(cls, faultfile): """ Read fault file format as defined in ShakeMap Software Guide. :param faultfile: Path to fault file OR file-like object in GMT psxy format, where * Fault vertices are space separated lat,lon,depth triplets on a single line. * Fault segments are separated by lines containing ">" * Fault segments must be closed. * Fault segments must be all clockwise or all counter-clockwise. :returns: Fault object. :raises ShakeMapException: when any of above conditions are not met. """ x = [] y = [] z = [] isFile = False if isinstance(faultfile, str): isFile = True faultfile = open(faultfile, 'rt') faultlines = faultfile.readlines() else: faultlines = faultfile.readlines() reference = '' for line in faultlines: sline = line.strip() if sline.startswith('#'): reference += sline continue if sline.startswith('>'): if len(x): # start of new line segment x.append(np.nan) y.append(np.nan) z.append(np.nan) continue else: # start of file continue if not len(sline.strip()): continue parts = sline.split() if len(parts) < 3: raise ShakeMapException( 'Finite fault file %s has no depth values.' % faultfile) y.append(float(parts[0])) x.append(float(parts[1])) z.append(float(parts[2])) if isFile: faultfile.close() if np.isnan(x[-1]): x = x[0:-1] y = y[0:-1] z = z[0:-1] return cls(x, y, z, reference)
def connect(self): """Initiate an ssh connection with properties passed to constructor. :returns: Instance of the paramiko SSHClient class. """ usePrivateKey = True for prop in self.required_props1: if prop not in list(self.properties.keys()): usePrivateKey = False break usePassword = True for prop in self.required_props2: if prop not in list(self.properties.keys()): usePassword = False break if not usePrivateKey and not usePassword: raise ShakeMapException( 'Either username/password must be specified, or the name of an SSH private key file.' ) ssh = SSHClient() #load hosts found in ~/.ssh/known_hosts ssh.load_system_host_keys( ) #should we not assume that the user has these configured already? if usePrivateKey: try: ssh.connect(self.properties['remotehost'], key_filename=self.properties['privatekey'], compress=True) except Exception as obj: raise ShakeMapException( 'Could not connect with private key file %s' % self.properties['privatekey']) else: try: ssh.connect(self.properties['remotehost'], username=self.properties['username'], password=self.properties['password'], compress=True) except Exception as obj: raise ShakeMapException( 'Could not connect with private key file %s' % self.properties['privatekey']) return ssh
def delete(self): for prop in self.required_properties: if prop not in list(self.properties.keys()): raise ShakeMapException( '"%s" property must be supplied to send via PDL') # build pdl command line from properties self.properties['status'] = 'DELETE' self.properties['files'] = '' self.properties['directories'] = '' cmd = self.pdlcmd for propkey, propvalue in self.properties.items(): cmd = cmd.replace('[' + propkey.upper() + ']', propvalue) retcode, stdout, stderr = get_command_output(cmd) if not retcode: fmt = 'Could not delete product "%s" due to error "%s"' tpl = (code, stdout + stderr) raise ShakeMapException(fmt % tpl)
def _reverseQuad(self, P0, P1, P2, P3): newP0 = copy.deepcopy(P1) newP1 = copy.deepcopy(P0) newP2 = copy.deepcopy(P3) newP3 = copy.deepcopy(P2) if not self._isPointToRight(newP0, newP1, newP2): raise ShakeMapException( 'Third vertex of quadrilateral must be to the right of the second vertex' ) return (newP0, newP1, newP2, newP3)
def setup(self): """ Initiate an ftp connection with properties passed to constructor. :returns: Instance of the ftplib.FTP class. """ if 'host' not in list(self.properties.keys()): raise NameError('"host" keyword must be supplied to send via FTP') if 'directory' not in list(self.properties.keys()): raise NameError( '"directory" keyword must be supplied to send via FTP') host = self.properties['host'] folder = self.properties['directory'] try: dirparts = folder.strip().split('/') ftp = FTP(host) if 'user' in self.properties: user = self.properties['user'] else: user = '' if 'password' in self.properties: password = self.properties['password'] else: password = '' if user == '': ftp.login() else: ftp.login(user, password) for d in dirparts: if d == '': continue try: ftp.cwd(d) except ftplib.error_perm as msg: raise ShakeMapException( 'Could not login to host "%s" and navigate to directory "%s"' % (host, folder)) except Exception as obj: raise ShakeMapException('Could not send to %s. Error "%s"' % (host, str(obj))) return ftp
def delete(self): '''Delete any files and folders that have been passed to constructor. :returns: The number of files deleted from local directory. ''' if 'directory' not in self.properties: raise ShakeMapException('Property "directory" not specified.') if not os.path.isdir(self.properties['directory']): raise ShakeMapException('Output directory "%s" does not exist.' % self.properties['directory']) for filename in self.files: fbase, fname = os.path.split(filename) dfile = os.path.join(self.properties['directory'], fname) os.remove(dfile) for folder in self.directories: fbase, dirname = os.path.split(folder) dfolder = os.path.join(self.properties['directory'], dirname) shutil.rmtree(dfolder)
def _getFileGeoDict(fname): geodict = None try: geodict = GMTGrid.getFileGeoDict(fname) except Exception as msg1: try: geodict = GDALGrid.getFileGeoDict(fname) except Exception as msg2: msg = 'File geodict failure with %s - error messages: "%s"\n "%s"' % ( fname, str(msg1), str(msg2)) raise ShakeMapException(msg) return geodict
def __init__(self, properties=None, files=None, directory=None): self.properties = properties if files is not None: if not isinstance(files, list): raise ShakeMapException('Input files must be a list') for f in files: if not os.path.isfile(f): raise ShakeMapException( 'Input file %s could not be found' % f) if directory is not None: if not os.path.isdir(directory): raise ShakeMapException( 'Input directory %s could not be found' % directory) if files is not None: self.files = files else: self.files = [] if directory is not None: self.directories = [directory] else: self.directories = []
def getRuptureContext(self, gmpelist): """ Return an Openquake `RuptureContext <http://docs.openquake.org/oq-hazardlib/master/gsim/index.html#openquake.hazardlib.gsim.base.RuptureContext>`__ suitable for any GMPE (except for those requiring hypo_loc). If Source does not contain a Fault, strike, dip, ztor, and width will be filled with default values. Rake may not be known, or may be estimated from a focal mechanism. :param gmpelist: Sequence of hazardlib GMPE objects. :raises: ShakeMapException when a GMPE requiring 'hypo_loc' is passed in. :returns: RuptureContext object with all known parameters filled in. """ # rupturecontext constructor inputs: # 'mag', 'strike', 'dip', 'rake', 'ztor', 'hypo_lon', 'hypo_lat', # 'hypo_depth', 'width', 'hypo_loc' for gmpe in gmpelist: reqset = gmpe.REQUIRES_RUPTURE_PARAMETERS if 'hypo_loc' in reqset: raise ShakeMapException( 'Rupture parameter "hypo_loc" is not supported!') rup = base.RuptureContext() rup.mag = self.getEventParam('mag') if self._fault is not None: rup.strike = self._fault.getStrike() rup.dip = self._fault.getDip() rup.ztor = self._fault.getTopOfRupture() rup.width = self._fault.getWidth() else: rup.strike = DEFAULT_STRIKE rup.dip = self.getEventParam('dip') rup.ztor = DEFAULT_ZTOR rup.width = DEFAULT_WIDTH if 'rake' in self._event_dict: rup.rake = self.getEventParam('rake') elif 'mech' in self._event_dict: mech = self._event_dict['mech'] rup.rake = RAKEDICT[mech] else: rup.rake = RAKEDICT['ALL'] rup.hypo_lat = self.getEventParam('lat') rup.hypo_lon = self.getEventParam('lon') rup.hypo_depth = self.getEventParam('depth') return rup
def send(self): '''Send any files or folders that have been passed to constructor. :returns: Number of files sent to local directory. ''' nfiles = 0 if 'directory' not in self.properties: raise ShakeMapException('Property "directory" not specified.') if not os.path.isdir(self.properties['directory']): raise ShakeMapException('Output directory "%s" does not exist.' % self.properties['directory']) for filename in self.files: shutil.copy(filename, self.properties['directory']) nfiles += len(self.files) for folder in self.directories: shutil.copytree(folder, self.properties['directory']) nfiles += len(os.walk(folder).next()[2]) return nfiles
def _testSendFile(properties): print('Testing sending single file...') thisfile = os.path.abspath(__file__) thispath, thisfilename = os.path.split(thisfile) try: sender = FTPSender(properties=properties, files=[thisfile]) sender.send() url = 'ftp://%s%s%s' % (properties['host'], properties['directory'], thisfilename) fh = urllib.request.urlopen(url) fh.close() sender.delete() except Exception as obj: fmt = 'Test failed - you may have a file called %s on host %s and directory %s' tpl = (thisfile, properties['host'], ['directory']) raise ShakeMapException(fmt % tpl) print('Passed sending single file.')
def send(self): '''Send any files or folders that have been passed to constructor. :returns: Number of files sent to remote SSH server. ''' if 'host' not in list(self.properties.keys()): raise NameError('"host" keyword must be supplied to send via FTP') if 'directory' not in list(self.properties.keys()): raise NameError( '"directory" keyword must be supplied to send via FTP') try: host = self.properties['host'] folder = self.properties['directory'] ftp = self.setup() # ftp.cwd(self.properties['directory']) nfiles = 0 if self.files is not None: for f in self.files: self.__sendfile(f, ftp) nfiles += 1 if self.directories is not None: for directory in self.directories: # root is the top level local directory root, thisfolder = os.path.split(directory) for path, subdirs, files in os.walk(directory): # mpath is the relative path on the ftp server mpath = path.replace(root, '').lstrip(os.sep) allfiles = ftp.nlst() if mpath not in allfiles: ftp.mkd(mpath) # full path to the folder on ftp server ftpfolder = os.path.join(folder, mpath) ftp.cwd(ftpfolder) for f in files: # f is the file name within the current folder # the full path to the local file fpath = os.path.join(path, f) self.__sendfile(fpath, ftp) nfiles += 1 ftp.cwd(folder) # go back to the root ftp.quit() return nfiles except Exception as obj: raise ShakeMapException('Could not send to %s. Error "%s"' % (host, str(obj)))
def _testSendFolder(properties): #modify this to create a temporary folder and send that - I think __pycache__ is screwing up the deletes... #although maybe I should test deleting directories with directories in them... print('Testing sending folder...') thisfile = os.path.abspath(__file__) thispath, thisfilename = os.path.split(thisfile) try: sender = FTPSender(properties=properties, directory=thispath) sender.send() url = 'ftp://%s%s' % (properties['host'], properties['directory']) fh = urllib.request.urlopen(url) fh.close() sender.delete() except Exception as obj: fmt = 'Test failed - you may have a file called %s on host %s and directory %s' tpl = (thisfile, properties['host'], ['directory']) raise ShakeMapException(fmt % tpl) print('Passed sending folder.')
def plot(self, ax=None): if ax is None: fig = plt.figure() ax = fig.add_subplot(111, projection='3d') else: if 'xlim3d' not in list(ax.properties.keys()): raise ShakeMapException( 'Non-3d axes object passed to plot() method.') for quad in self._quadrilaterals: P0, P1, P2, P3 = quad ax.plot([P0.longitude], [P0.latitude], [-P0.depth], 'B.') ax.text([P0.longitude], [P0.latitude], [-P0.depth], 'P0') ax.plot([P1.longitude], [P1.latitude], [-P1.depth], 'b.') ax.text([P1.longitude], [P1.latitude], [-P1.depth], 'P1') ax.plot([P2.longitude], [P2.latitude], [-P2.depth], 'b.') ax.text([P2.longitude], [P2.latitude], [-P2.depth], 'P2') ax.plot([P3.longitude], [P3.latitude], [-P3.depth], 'b.') ax.text([P3.longitude], [P3.latitude], [-P3.depth], 'P3')
def sampleFromSites(self, lats, lons, vs30measured_grid=None): """ Create a SitesContext object by sampling the current Sites object. :param lats: Sequence of latitudes. :param lons: Sequence of longitudes. :param vs30measured_grid: Sequence of booleans of the same shape as lats/lons indicating whether the vs30 values are measured or inferred. :returns: SitesContext object where data are sampled from the current Sites object. :raises ShakeMapException: When lat/lon input sequences do not share dimensionality. """ lats = np.array(lats) lons = np.array(lons) latshape = lats.shape lonshape = lons.shape if latshape != lonshape: msg = 'Input lat/lon arrays must have the same dimensions' raise ShakeMapException(msg) site = SitesContext() # use default vs30 if outside grid site.vs30 = self._Vs30.getValue(lats, lons, default=self._defaultVs30) site.lats = lats site.lons = lons site.z1pt0 = _calculate_z1p0(site.vs30) site.z2pt5 = _calculate_z2p5(site.z1pt0) if vs30measured_grid is None: # If we don't know, then use false site.vs30measured = np.zeros_like(lons, dtype=bool) else: site.vs30measured = vs30measured_grid site.backarc = self._backarc return site
def addDirectory(self, directory): if not os.path.isdir(directory): raise ShakeMapException('Input directory %s could not be found' % directory) self.directories += directory
def drawContourMap(self,outfolder,cmin=None,cmax=None): if self.contour_colormap is None: raise ShakeMapException('MapMaker.setGMTColormap() has not been called.') t0 = time.time() #resample shakemap to topogrid #get the geodict for the topo file topodict = GMTGrid.getFileGeoDict(self.topofile) #get the geodict for the ShakeMap smdict = self.shakemap.getGeoDict() #get a geodict that is aligned with topo, but inside shakemap sampledict = topodict.getBoundsWithin(smdict) self.shakemap = self.shakemap.interpolateToGrid(sampledict) gd = self.shakemap.getGeoDict() #establish the basemap object m = self._setMap(gd) #get topo layer and project it topogrid = GMTGrid.load(self.topofile,samplegeodict=sampledict,resample=False) topodata = topogrid.getData().copy() ptopo = self._projectGrid(topodata,m,gd) #get contour layer and project it1 imtdata = self.shakemap.getLayer(self.contour_layer).getData().copy() pimt = self._projectGrid(imtdata,m,gd) #get the draped intensity data hillshade = self._getShaded(ptopo) #draw the draped intensity data m.imshow(hillshade, interpolation='none',zorder=IMG_ZORDER); #draw the contours of imt data xmin = gd.xmin if gd.xmax < gd.xmin: xmin -= 360 lons = np.linspace(xmin, gd.xmax, gd.nx) lats = np.linspace(gd.ymax, gd.ymin, gd.ny) # backwards so it plots right side up x, y = m(*np.meshgrid(lons,lats)) pimt = gaussian_filter(pimt,5.0) dmin = pimt.min() dmax = pimt.max() levels = self.getContourLevels(dmin,dmax,self.contour_layer) cs = m.contour(x,y,np.flipud(pimt),colors='w',cmap=None,levels=levels,zorder=CONTOUR_ZORDER) clabels = plt.clabel(cs,colors='k',fmt='%.1f',fontsize=8.0,zorder=CONTOUR_ZORDER) for cl in clabels: bbox = dict(boxstyle="round",facecolor='white',edgecolor='w') cl.set_bbox(bbox) cl.set_zorder(CONTOUR_ZORDER) #draw country/state boundaries self._drawBoundaries(m) #draw lakes self._drawLakes(m,gd) #draw oceans (pre-processed with islands taken out) t1 = time.time() self._drawOceans(m,gd) t2 = time.time() print('%.1f seconds to render oceans.' % (t2-t1)) #draw coastlines self._drawCoastlines(m,gd) #draw meridians, parallels, labels, ticks self._drawGraticules(m,gd) #draw filled symbols for MMI and instrumented measures self._drawStations(m,fill=True,imt=self.contour_layer) #draw map scale scalex = gd.xmin + (gd.xmax-gd.xmin)/5.0 scaley = gd.ymin + (gd.ymax-gd.ymin)/10.0 yoff = (0.007*(m.ymax-m.ymin)) clon = (gd.xmin + gd.xmax)/2.0 clat = (gd.ymin + gd.ymax)/2.0 m.drawmapscale(scalex,scaley,clon,clat,length=100,barstyle='fancy',yoffset=yoff,zorder=SCALE_ZORDER) #draw fault polygon, if present self._drawFault(m) #get the fault loaded #draw epicenter hlon = self.shakemap.getEventDict()['lon'] hlat = self.shakemap.getEventDict()['lat'] m.plot(hlon,hlat,'k*',latlon=True,fillstyle='none',markersize=22,mew=1.2,zorder=EPICENTER_ZORDER); #draw cities #reduce the number of cities to those whose labels don't collide #set up cities if self.city_cols is not None: self.cities = self.cities.limitByBounds((gd.xmin,gd.xmax,gd.ymin,gd.ymax)) self.cities = self.cities.limitByGrid(nx=self.city_cols,ny=self.city_rows, cities_per_grid=self.cities_per_grid) self.cities = self.cities.limitByMapCollision(m) self.cities.renderToMap(m.ax,zorder=CITIES_ZORDER) #draw title and supertitle eventid = self._drawTitle(isContour=True) #draw whatever road data is available #self._drawRoads(m) #save plot to file plt.draw() outfile = os.path.join(outfolder,'contour_%s_%s.pdf' % (self.contour_layer,eventid)) plt.savefig(outfile) tn = time.time() print('%.1f seconds to render entire map.' % (tn-t0)) return outfile
def drawIntensityMap(self,outfolder): if self.intensity_colormap is None: raise ShakeMapException('MapMaker.setGMTColormap() has not been called.') t0 = time.time() #resample shakemap to topogrid #get the geodict for the topo file topodict = GMTGrid.getFileGeoDict(self.topofile) #get the geodict for the ShakeMap smdict = self.shakemap.getGeoDict() #get a geodict that is aligned with topo, but inside shakemap sampledict = topodict.getBoundsWithin(smdict) self.shakemap = self.shakemap.interpolateToGrid(sampledict) gd = self.shakemap.getGeoDict() #establish the basemap object m = self._setMap(gd) #get topo layer and project it topogrid = GMTGrid.load(self.topofile,samplegeodict=sampledict,resample=False) topodata = topogrid.getData().copy() ptopo = self._projectGrid(topodata,m,gd) #get intensity layer and project it imtdata = self.shakemap.getLayer(self.imt_layer).getData().copy() pimt = self._projectGrid(imtdata,m,gd) #get the draped intensity data draped_hsv = self._getDraped(pimt,ptopo) #where will 10.0 come from #draw the draped intensity data m.imshow(draped_hsv, interpolation='none',zorder=IMG_ZORDER); #draw country/state boundaries self._drawBoundaries(m) #draw whatever road data is available self._drawRoads(m) #draw lakes self._drawLakes(m,gd) #draw oceans (pre-processed with islands taken out) t1 = time.time() self._drawOceans(m,gd) t2 = time.time() print('%.1f seconds to render oceans.' % (t2-t1)) #draw coastlines self._drawCoastlines(m,gd) #draw meridians, parallels, labels, ticks self._drawGraticules(m,gd) #draw map scale scalex = gd.xmin + (gd.xmax-gd.xmin)/5.0 scaley = gd.ymin + (gd.ymax-gd.ymin)/10.0 yoff = (0.007*(m.ymax-m.ymin)) clon = (gd.xmin + gd.xmax)/2.0 clat = (gd.ymin + gd.ymax)/2.0 m.drawmapscale(scalex,scaley,clon,clat,length=100,barstyle='fancy',yoffset=yoff,zorder=SCALE_ZORDER) #draw fault polygon, if present self._drawFault(m) #get the fault loaded #draw epicenter hlon = self.shakemap.getEventDict()['lon'] hlat = self.shakemap.getEventDict()['lat'] m.plot(hlon,hlat,'k*',latlon=True,fillstyle='none',markersize=22,mew=1.2,zorder=EPICENTER_ZORDER); #draw cities #reduce the number of cities to those whose labels don't collide #set up cities if self.city_cols is not None: self.cities = self.cities.limitByBounds((gd.xmin,gd.xmax,gd.ymin,gd.ymax)) self.cities = self.cities.limitByGrid(nx=self.city_cols,ny=self.city_rows, cities_per_grid=self.cities_per_grid) self.cities = self.cities.limitByMapCollision(m) self.cities.renderToMap(m.ax,zorder=CITIES_ZORDER) #draw title and supertitle eventid = self._drawTitle() #draw station and macroseismic locations self._drawStations(m) #need stationlist object #save plot to file plt.draw() outfile = os.path.join(outfolder,'intensity_%s.pdf' % eventid) plt.savefig(outfile) tn = time.time() print('%.1f seconds to render entire map.' % (tn-t0)) return outfile
def fromVertices(cls, xp0, yp0, zp0, xp1, yp1, zp1, xp2, yp2, zp2, xp3, yp3, zp3, reference=None): """ Create a fault object from the vector of vertices that fully define the quadrilaterals. The points p0, ..., p3 are labeled below for a trapezoid: :: p0--------p1 / | / | p3-----------p2 All of the following vector arguments must have the same length. :param xp0: Vector of longitudes of p0. :param yp0: Vector of latitudes of p0. :param zp0: Vector of depths of p0 (positive down). :param xp1: Vector of longitudes of p1. :param yp1: Vector of latitudes of p1. :param zp1: Vector of depths of p1 (positive down). :param xp2: Vector of longitudes of p2. :param yp2: Vector of latitudes of p2. :param zp2: Vector of depths of p2 (positive down). :param xp3: Vector of longitudes of p3. :param yp3: Vector of latitudes of p3. :param zp3: Vector of depths of p3 (positive down). :param reference: String explaining where the fault definition came from (publication style reference, etc.) :returns: Fault object, where the fault is modeled as a series of trapezoids. """ if len(xp0) == len(yp0) == len(zp0) == len(xp1) == len(yp1) == len(zp1) == \ len(xp2) == len(yp2) == len(zp2) == len(xp3) == len(yp3) == len(zp3): pass else: raise ShakeMapException('All vectors specifying quadrilateral ' 'vertices must have the same length.') nq = len(xp0) xp0 = np.array(xp0, dtype='d') yp0 = np.array(yp0, dtype='d') zp0 = np.array(zp0, dtype='d') xp1 = np.array(xp1, dtype='d') yp1 = np.array(yp1, dtype='d') zp1 = np.array(zp1, dtype='d') xp2 = np.array(xp2, dtype='d') yp2 = np.array(yp2, dtype='d') zp2 = np.array(zp2, dtype='d') xp3 = np.array(xp3, dtype='d') yp3 = np.array(yp3, dtype='d') zp3 = np.array(zp3, dtype='d') # assemble the vertices as the Fault constructor needs them... # which is: for each rectangle, there should be the four corners, the # first corner repeated, and then a nan. anan = np.ones_like(xp0) * np.nan lon = np.array(list(zip(xp0, xp1, xp2, xp3, xp0, anan))).reshape( (nq, 6)).flatten(order='C') lat = np.array(list(zip(yp0, yp1, yp2, yp3, yp0, anan))).reshape( (nq, 6)).flatten(order='C') dep = np.array(list(zip(zp0, zp1, zp2, zp3, zp0, anan))).reshape( (nq, 6)).flatten(order='C') return cls(lon, lat, dep, reference)
def get_distance(methods, lat, lon, dep, source, use_median_distance=True): """ Calculate distance using any one of a number of distance measures. One of quadlist OR hypo must be specified. The following table gives the allowed distance strings and a description of each. +--------+----------------------------------------------------------+ | String | Description | +========+==========================================================+ | repi | Distance to epicenter. | +--------+----------------------------------------------------------+ | rhypo | Distance to hypocenter. | +--------+----------------------------------------------------------+ | rjb | Joyner-Boore distance; this is closest distance to the | | | surface projection of the rupture plane. | +--------+----------------------------------------------------------+ | rrup | Rupture distance; closest distance to the rupture plane. | +--------+----------------------------------------------------------+ | rx | Strike-normal distance; same as GC2 coordiante T. | +--------+----------------------------------------------------------+ | ry | Strike-parallel distance; same as GC2 coordiante U, but | | | with a shift in origin definition. See Spudich and Chiou | | | (2015) http://dx.doi.org/10.3133/ofr20151028. | +--------+----------------------------------------------------------+ | ry0 | Horizontal distance off the end of the rupture measured | | | parallel to strike. Can only be zero or positive. We | | | compute this as a function of GC2 coordinate U. | +--------+----------------------------------------------------------+ | U | GC2 coordinate U. | +--------+----------------------------------------------------------+ | T | GC2 coordinate T. | +--------+----------------------------------------------------------+ :param methods: List of strings (or just a string) of distances to compute. :param lat: A numpy array of latitudes. :param lon: A numpy array of longidues. :param dep: A numpy array of depths (km). :param source: source instance. :param use_median_distance: Boolean; only used if GMPE requests fault distances and not fault is availalbe. Default is True, meaning that point-source distances are adjusted based on magnitude to get the median fault distance. :returns: dictionary of numpy array of distances, size of lon.shape """ fault = source.getFault() hypo = source.getHypo() if fault is not None: quadlist = fault.getQuadrilaterals() else: quadlist = None # Dictionary for holding the distances distdict = dict() if not isinstance(methods, list): methods = [methods] methods_available = set( ['repi', 'rhypo', 'rjb', 'rrup', 'rx', 'ry', 'ry0', 'U', 'T']) if not set(methods).issubset(methods_available): raise NotImplementedError( 'One or more requested distance method is not ' 'valid or is not implemented yet') if (lat.shape == lon.shape) and (lat.shape == dep.shape): pass else: raise ShakeMapException('lat, lon, and dep must have the same shape.') oldshape = lon.shape if len(oldshape) == 2: newshape = (oldshape[0] * oldshape[1], 1) else: newshape = (oldshape[0], 1) if ('rrup' in methods) or ('rjb' in methods): x, y, z = latlon2ecef(lat, lon, dep) x.shape = newshape y.shape = newshape z.shape = newshape sites_ecef = np.hstack((x, y, z)) # Define a projection that spands sites and fault if fault is None: all_lat = lat all_lon = lon else: all_lat = np.append(lat, fault.getLats()) all_lon = np.append(lon, fault.getLons()) west = np.nanmin(all_lon) east = np.nanmax(all_lon) south = np.nanmin(all_lat) north = np.nanmax(all_lat) proj = get_orthographic_projection(west, east, north, south) # --------------------------------------------- # Distances that do not require loop over quads # --------------------------------------------- if ('repi' in methods) or \ (('rjb' in methods) and (quadlist is None)) or \ (('rrup' in methods) and (quadlist is None)) or \ (('ry0' in methods) and (quadlist is None)) or \ (('rx' in methods) and (quadlist is None)) or \ (('T' in methods) and (quadlist is None)) or \ (('U' in methods) and (quadlist is None)): if hypo is None: raise ShakeMapException('Cannot calculate epicentral distance ' 'without a point object') repidist = geodetic.distance(hypo.longitude, hypo.latitude, 0.0, lon, lat, dep) repidist = repidist.reshape(oldshape) distdict['repi'] = repidist if ('rhypo' in methods) or \ (('rrup' in methods) and (quadlist is None)): if hypo is None: raise ShakeMapException('Cannot calculate epicentral distance ' 'without a point object') rhypodist = geodetic.distance(hypo.longitude, hypo.latitude, hypo.depth, lon, lat, dep) rhypodist = rhypodist.reshape(oldshape) distdict['rhypo'] = rhypodist # -------------------------------------------------------- # Loop over quadlist for those distances that require loop # -------------------------------------------------------- if 'rrup' in methods: minrrup = np.ones(newshape, dtype=lon.dtype) * 1e16 if 'rjb' in methods: minrjb = np.ones(newshape, dtype=lon.dtype) * 1e16 if ('rx' in methods) or ('ry' in methods) or \ ('ry0' in methods) or ('U' in methods) or ('T' in methods): totweight = np.zeros(newshape, dtype=lon.dtype) GC2T = np.zeros(newshape, dtype=lon.dtype) GC2U = np.zeros(newshape, dtype=lon.dtype) if quadlist is not None: #----------------------------------------------------------------- # For these distances, we need to sort out strike discordance and # nominal strike prior to starting the loop if there is more than # one segment. #----------------------------------------------------------------- segind = fault._getSegmentIndex() segindnp = np.array(segind) uind = np.unique(segind) nseg = len(uind) #------------------------------------------------------------------- # The first thing we need to worry about is finding the coordinate # shift. U's origin is " selected from the two endpoints most # distant from each other." #------------------------------------------------------------------- if nseg > 1: # Need to get index of first and last quad # for each segment iq0 = np.zeros(nseg, dtype='int16') iq1 = np.zeros(nseg, dtype='int16') for k in uind: ii = [i for i, j in enumerate(segind) if j == uind[k]] iq0[k] = int(np.min(ii)) iq1[k] = int(np.max(ii)) #--------------------------------------------------------------- # This is an iterator for each possible combination of segments # including segment orientations (i.e., flipped). #--------------------------------------------------------------- it_seg = it.product(it.combinations(uind, 2), it.product([0, 1], [0, 1])) # Placeholder for the segment pair/orientation that gives the # largest distance. dist_save = 0 for k in it_seg: s0ind = k[0][0] s1ind = k[0][1] p0ind = k[1][0] p1ind = k[1][1] if p0ind == 0: P0 = quadlist[iq0[s0ind]][0] else: P0 = quadlist[iq1[s0ind]][1] if p1ind == 0: P1 = quadlist[iq1[s1ind]][0] else: P1 = quadlist[iq0[s1ind]][1] dist = geodetic.distance(P0.longitude, P0.latitude, 0.0, P1.longitude, P1.latitude, 0.0) if dist > dist_save: dist_save = dist A0 = P0 A1 = P1 #--------------------------------------------------------------- # A0 and A1 are the furthest two segment endpoints, but we still # need to sort out which one is the "origin". #--------------------------------------------------------------- # Primate fixes the trend of the trial a vector. primate = -1 while primate < 0: A0.depth = 0 A1.depth = 0 p_origin = Vector.fromPoint(A0) a0 = Vector.fromPoint(A0) a1 = Vector.fromPoint(A1) ahat = (a1 - a0).norm() # Loop over traces e_j = np.zeros(nseg) b_prime = [None] * nseg for j in range(nseg): P0 = quadlist[iq0[j]][0] P1 = quadlist[iq1[j]][1] P0.depth = 0 P1.depth = 0 p0 = Vector.fromPoint(P0) p1 = Vector.fromPoint(P1) b_prime[j] = p1 - p0 e_j[j] = ahat.dot(b_prime[j]) E = np.sum(e_j) # List of discordancy dc = [np.sign(a) * np.sign(E) for a in e_j] b = Vector(0, 0, 0) for j in range(nseg): b.x = b.x + b_prime[j].x * dc[j] b.y = b.y + b_prime[j].y * dc[j] b.z = b.z + b_prime[j].z * dc[j] bhat = b.norm() primate = bhat.dot(ahat) if primate < 0: tmpA0 = copy.copy(A0) tmpA1 = copy.copy(A1) A0 = tmpA1 A1 = tmpA0 if quadlist is not None: # Length of prior segments s_i = 0.0 l_i = np.zeros(len(quadlist)) for i in range(len(quadlist)): P0, P1, P2, P3 = quadlist[i] if 'rrup' in methods: rrupdist = _calc_rupture_distance(P0, P1, P2, P3, sites_ecef) minrrup = np.minimum(minrrup, rrupdist) if 'rjb' in methods: S0 = copy.deepcopy(P0) S1 = copy.deepcopy(P1) S2 = copy.deepcopy(P2) S3 = copy.deepcopy(P3) S0.depth = 0.0 S1.depth = 0.0 S2.depth = 0.0 S3.depth = 0.0 rjbdist = _calc_rupture_distance(S0, S1, S2, S3, sites_ecef) minrjb = np.minimum(minrjb, rjbdist) if ('rx' in methods) or ('ry' in methods) or \ ('ry0' in methods) or ('U' in methods) or ('T' in methods): # Rx, Ry, and Ry0 are all computed if one is requested since # they all require similar information for the weights. This # isn't necessary for a single segment fault though. # Note, we are basing these calculations on GC2 coordinates U # and T as described in: # Spudich and Chiou (2015) # http://dx.doi.org/10.3133/ofr20151028. # Compute u_i and t_i for this segment t_i = __calc_t_i(P0, P1, lat, lon, proj) u_i = __calc_u_i(P0, P1, lat, lon, proj) # Quad length l_i[i] = get_quad_length(quadlist[i]) # Weight of segment, three cases # Case 3: t_i == 0 and 0 <= u_i <= l_i w_i = np.zeros_like(t_i) # Case 1: ix = t_i != 0 w_i[ix] = (1.0 / t_i[ix]) * (np.arctan( (l_i[i] - u_i[ix]) / t_i[ix]) - np.arctan(-u_i[ix] / t_i[ix])) # Case 2: ix = (t_i == 0) & ((u_i < 0) | (u_i > l_i[i])) w_i[ix] = 1 / (u_i[ix] - l_i[i]) - 1 / u_i[ix] totweight = totweight + w_i GC2T = GC2T + w_i * t_i if nseg == 1: GC2U = GC2U + w_i * (u_i + s_i) else: if i == 0: qind = np.array(range(len(quadlist))) l_kj = 0 s_ij_1 = 0 else: l_kj = l_i[(segindnp == segindnp[i]) & (qind < i)] s_ij_1 = np.sum(l_kj) p1 = Vector.fromPoint(quadlist[iq0[segind[i]]][0]) s_ij_2 = ( (p1 - p_origin) * dc[segind[i]]).dot(ahat) / 1000.0 # This is implemented with GC2N, for GC2T use: # s_ij_2 = (p1 - p_origin).dot(bhat) / 1000.0 s_ij = s_ij_1 + s_ij_2 GC2U = GC2U + w_i * (u_i + s_ij) s_i = s_i + l_i[i] # Collect distances from loop into the distance dict if 'rjb' in methods: minrjb = minrjb.reshape(oldshape) distdict['rjb'] = minrjb if ('rx' in methods) or ('ry' in methods) or \ ('ry0' in methods) or ('U' in methods) or ('T' in methods): # Normalize by sum of quad weights GC2T = GC2T / totweight GC2U = GC2U / totweight distdict['T'] = copy.deepcopy(GC2T).reshape(oldshape) distdict['U'] = copy.deepcopy(GC2U).reshape(oldshape) # Take care of Rx Rx = copy.deepcopy(GC2T) # preserve sign (no absolute value) Rx = Rx.reshape(oldshape) distdict['rx'] = Rx # Ry Ry = GC2U - s_i / 2.0 Ry = Ry.reshape(oldshape) distdict['ry'] = Ry # Ry0 Ry0 = np.zeros_like(GC2U) ix = GC2U < 0 Ry0[ix] = np.abs(GC2U[ix]) if nseg > 1: s_i = s_ij + l_i[-1] ix = GC2U > s_i Ry0[ix] = GC2U[ix] - s_i Ry0 = Ry0.reshape(oldshape) distdict['ry0'] = Ry0 if 'rrup' in methods: minrrup = minrrup.reshape(oldshape) distdict['rrup'] = minrrup else: if 'rjb' in methods: if use_median_distance: warnings.warn( 'No fault; Replacing rjb with median rjb given M and repi.' ) cdir, tmp = os.path.split(__file__) # ------------------- # Sort out file names # ------------------- mech = source.getEventParam('mech') if not hasattr(source, '_tectonic_region'): rf = os.path.join( cdir, "data", "ps2ff", "Rjb_WC94_mechA_ar1p0_seis0_20_Ratios.csv") vf = os.path.join(cdir, "data", "ps2ff", "Rjb_WC94_mechA_ar1p0_seis0_20_Var.csv") elif source._tectonic_region == 'Active Shallow Crust': if mech == 'ALL': rf = os.path.join( cdir, "data", "ps2ff", "Rjb_WC94_mechA_ar1p7_seis0_20_Ratios.csv") vf = os.path.join( cdir, "data", "ps2ff", "Rjb_WC94_mechA_ar1p7_seis0_20_Var.csv") elif mech == 'RS': rf = os.path.join( cdir, "data", "ps2ff", "Rjb_WC94_mechR_ar1p7_seis0_20_Ratios.csv") vf = os.path.join( cdir, "data", "ps2ff", "Rjb_WC94_mechR_ar1p7_seis0_20_Var.csv") elif mech == 'NM': rf = os.path.join( cdir, "data", "ps2ff", "Rjb_WC94_mechN_ar1p7_seis0_20_Ratios.csv") vf = os.path.join( cdir, "data", "ps2ff", "Rjb_WC94_mechN_ar1p7_seis0_20_Var.csv") elif mech == 'SS': rf = os.path.join( cdir, "data", "ps2ff", "Rjb_WC94_mechSS_ar1p7_seis0_20_Ratios.csv") vf = os.path.join( cdir, "data", "ps2ff", "Rjb_WC94_mechSS_ar1p7_seis0_20_Var.csv") elif source._tectonic_region == 'Stable Shallow Crust': if mech == 'ALL': rf = os.path.join( cdir, "data", "ps2ff", "Rjb_S14_mechA_ar1p0_seis0_15_Ratios.csv") vf = os.path.join( cdir, "data", "ps2ff", "Rjb_S14_mechA_ar1p0_seis0_15_Var.csv") elif mech == 'RS': rf = os.path.join( cdir, "data", "ps2ff", "Rjb_S14_mechR_ar1p0_seis0_15_Ratios.csv") vf = os.path.join( cdir, "data", "ps2ff", "Rjb_S14_mechR_ar1p0_seis0_15_Var.csv") elif mech == 'NM': rf = os.path.join( cdir, "data", "ps2ff", "Rjb_S14_mechN_ar1p0_seis0_15_Ratios.csv") vf = os.path.join( cdir, "data", "ps2ff", "Rjb_S14_mechN_ar1p0_seis0_15_Var.csv") elif mech == 'SS': rf = os.path.join( cdir, "data", "ps2ff", "Rjb_S14_mechSS_ar1p0_seis0_15_Ratios.csv") vf = os.path.join( cdir, "data", "ps2ff", "Rjb_S14_mechSS_ar1p0_seis0_15_Var.csv") else: warnings.warn( 'Unsupported tectonic region; using coefficients for unknown' 'tectonic region.') rf = os.path.join( cdir, "data", "ps2ff", "Rjb_WC94_mechA_ar1p0_seis0_20_Ratios.csv") vf = os.path.join(cdir, "data", "ps2ff", "Rjb_WC94_mechA_ar1p0_seis0_20_Var.csv") # ----------------- # Start with ratios # ----------------- repi2rjb_ratios_tbl = pd.read_csv(rf, comment='#') r2rrt_cols = repi2rjb_ratios_tbl.columns[1:] mag_list = [] for column in (r2rrt_cols): if re.search('R\d+\.*\d*', column): magnitude = float( re.findall('R(\d+\.*\d*)', column)[0]) mag_list.append(magnitude) mag_list = np.array(mag_list) dist_list = np.log(np.array(repi2rjb_ratios_tbl['Repi_km'])) repi2rjb_grid = repi2rjb_ratios_tbl.values[:, 1:] repi2rjb_obj = spint.RectBivariateSpline(dist_list, mag_list, repi2rjb_grid, kx=1, ky=1) def repi2rjb_tbl(repi, M): ratio = repi2rjb_obj.ev(np.log(repi), M) rjb = repi * ratio return rjb repis = distdict['repi'] mags = np.ones_like(repis) * source.getEventParam('mag') rjb_hat = repi2rjb_tbl(repis, mags) distdict['rjb'] = rjb_hat # ------------------- # Additional Variance # ------------------- repi2rjbvar_ratios_tbl = pd.read_csv(vf, comment='#') repi2rjbvar_grid = repi2rjbvar_ratios_tbl.values[:, 1:] repi2rjbvar_obj = spint.RectBivariateSpline(dist_list, mag_list, repi2rjbvar_grid, kx=1, ky=1) rjbvar = repi2rjbvar_obj.ev(np.log(repis), mags) distdict['rjbvar'] = rjbvar else: warnings.warn('No fault; Replacing rjb with repi') distdict['rjb'] = distdict['repi'] if 'rrup' in methods: if use_median_distance: warnings.warn( 'No fault; Replacing rrup with median rrup given M and repi.' ) cdir, tmp = os.path.split(__file__) # ------------------- # Sort out file names # ------------------- rake = source._event_dict.get('rake') mech = rake_to_mech(rake) if not hasattr(source, '_tectonic_region'): rf = os.path.join( cdir, "data", "ps2ff", "Rrup_WC94_mechA_ar1p0_seis0-20_Ratios.csv") vf = os.path.join( cdir, "data", "ps2ff", "Rrup_WC94_mechA_ar1p0_seis0-20_Var.csv") elif source._tectonic_region == 'Active Shallow Crust': if mech == 'ALL': rf = os.path.join( cdir, "data", "ps2ff", "Rrup_WC94_mechA_ar1p7_seis0-20_Ratios.csv") vf = os.path.join( cdir, "data", "ps2ff", "Rrup_WC94_mechA_ar1p7_seis0-20_Var.csv") elif mech == 'RS': rf = os.path.join( cdir, "data", "ps2ff", "Rrup_WC94_mechR_ar1p7_seis0-20_Ratios.csv") vf = os.path.join( cdir, "data", "ps2ff", "Rrup_WC94_mechR_ar1p7_seis0-20_Var.csv") elif mech == 'NM': rf = os.path.join( cdir, "data", "ps2ff", "Rrup_WC94_mechN_ar1p7_seis0-20_Ratios.csv") vf = os.path.join( cdir, "data", "ps2ff", "Rrup_WC94_mechN_ar1p7_seis0-20_Var.csv") elif mech == 'SS': rf = os.path.join( cdir, "data", "ps2ff", "Rrup_WC94_mechSS_ar1p7_seis0-20_Ratios.csv") vf = os.path.join( cdir, "data", "ps2ff", "Rrup_WC94_mechSS_ar1p7_seis0-20_Var.csv") elif source._tectonic_region == 'Stable Shallow Crust': if mech == 'ALL': rf = os.path.join( cdir, "data", "ps2ff", "Rrup_S14_mechA_ar1p0_seis0-15_Ratios.csv") vf = os.path.join( cdir, "data", "ps2ff", "Rrup_S14_mechA_ar1p0_seis0-15_Var.csv") elif mech == 'RS': rf = os.path.join( cdir, "data", "ps2ff", "Rrup_S14_mechR_ar1p0_seis0-15_Ratios.csv") vf = os.path.join( cdir, "data", "ps2ff", "Rrup_S14_mechR_ar1p0_seis0-15_Var.csv") elif mech == 'NM': rf = os.path.join( cdir, "data", "ps2ff", "Rrup_S14_mechN_ar1p0_seis0-15_Ratios.csv") vf = os.path.join( cdir, "data", "ps2ff", "Rrup_S14_mechN_ar1p0_seis0-15_Var.csv") elif mech == 'SS': rf = os.path.join( cdir, "data", "ps2ff", "Rrup_S14_mechSS_ar1p0_seis0-15_Ratios.csv") vf = os.path.join( cdir, "data", "ps2ff", "Rrup_S14_mechSS_ar1p0_seis0-15_Var.csv") else: warnings.warn( 'Unsupported tectonic region; using coefficients for unknown' 'tectonic region.') rf = os.path.join( cdir, "data", "ps2ff", "Rrup_WC94_mechA_ar1p0_seis0-20_Ratios.csv") vf = os.path.join( cdir, "data", "ps2ff", "Rrup_WC94_mechA_ar1p0_seis0-20_Var.csv") # ----------------- # Start with ratios # ----------------- repi2rrup_ratios_tbl = pd.read_csv(rf, comment='#') r2rrt_cols = repi2rrup_ratios_tbl.columns[1:] mag_list = [] for column in (r2rrt_cols): if re.search('R\d+\.*\d*', column): magnitude = float( re.findall('R(\d+\.*\d*)', column)[0]) mag_list.append(magnitude) mag_list = np.array(mag_list) dist_list = np.log(np.array(repi2rrup_ratios_tbl['Repi_km'])) repi2rrup_grid = repi2rrup_ratios_tbl.values[:, 1:] repi2rrup_obj = spint.RectBivariateSpline(dist_list, mag_list, repi2rrup_grid, kx=1, ky=1) def repi2rrup_tbl(repi, M): ratio = repi2rrup_obj.ev(np.log(repi), M) rrup = repi * ratio return rrup repis = distdict['repi'] mags = np.ones_like(repis) * source.getEventParam('mag') rrup_hat = repi2rrup_tbl(repis, mags) distdict['rrup'] = rrup_hat # ------------------- # Additional Variance # ------------------- repi2rrupvar_ratios_tbl = pd.read_csv(vf, comment='#') repi2rrupvar_grid = repi2rrupvar_ratios_tbl.values[:, 1:] repi2rrupvar_obj = spint.RectBivariateSpline(dist_list, mag_list, repi2rrupvar_grid, kx=1, ky=1) rrupvar = repi2rrupvar_obj.ev(np.log(repis), mags) distdict['rrupvar'] = rrupvar else: warnings.warn('No fault; Replacing rrup with rhypo') distdict['rrup'] = distdict['rhypo'] if 'rx' in methods: warnings.warn('No fault; Setting Rx to zero.') distdict['rx'] = np.zeros_like(distdict['repi']) if 'ry0' in methods: warnings.warn('No fault; Replacing ry0 with repi') distdict['ry0'] = distdict['repi'] if 'ry' in methods: warnings.warn('No fault; Replacing ry with repi') distdict['ry'] = distdict['repi'] return distdict
def _validateQuad(self, P0, P1, P2, P3): """ Validate and fix* a given quadrilateral (*currently "fix" means check third vertex for co-planarity with other three points, and force it to be co-planar if it's not wildly out of the plane.() :param P0: First vertex https://github.com/gem/oq-hazardlib/blob/master/openquake/hazardlib/geo/point.py :param P1: Second vertex https://github.com/gem/oq-hazardlib/blob/master/openquake/hazardlib/geo/point.py :param P2: Third vertex https://github.com/gem/oq-hazardlib/blob/master/openquake/hazardlib/geo/point.py :param P3: Fourth vertex https://github.com/gem/oq-hazardlib/blob/master/openquake/hazardlib/geo/point.py :returns: Tuple of (potentially) modified vertices. :raises ShakeMapException: * if top and bottom edges are not parallel to surface * if dip angle is not dipping to the right relative to strike (defined by first two vertices) * if all 4 points are not reasonably co-planar (P2 is more than 5% of mean length of trapezoid out of plane) """ # TODO: Someday fix the rule about dip angle being clockwise and 0-90 degrees # In theory, you could flip the quadrilateral by 180 degrees and it # would be ok. # Are the top and bottom edges both parallel to the surface? topDepthsEqual = np.allclose(P0.depth, P1.depth, atol=2e-3) bottomDepthsEqual = np.allclose(P2.depth, P3.depth, atol=2e-3) if not topDepthsEqual or not bottomDepthsEqual: raise ShakeMapException( 'Top and bottom edges of fault quadrilateral must be parallel to the surface' ) # Is top edge defined by first two vertices? if P1.depth > P2.depth: raise ShakeMapException( 'Top edge of a quadrilateral must be defined by the first two vertices' ) # Is dip angle clockwise and btw 0-90 degrees? if not self._isPointToRight(P0, P1, P2): P0, P1, P2, P3 = self._reverseQuad(P0, P1, P2, P3) print('Reversing quad where dip not between 0 and 90 degrees.') # Are all 4 points (reasonably) co-planar? # Translate vertices to ECEF p0 = Vector.fromPoint(P0) p1 = Vector.fromPoint(P1) p2 = Vector.fromPoint(P2) p3 = Vector.fromPoint(P3) # Calculate normalized vector along top edge v0 = (p1 - p0).norm() # Calculate distance btw p3 and p2 d = (p3 - p2).mag() # get the new P2 value v1 = v0 * d newp2 = p3 + v1 planepoints = [p0, p1, p2] dnormal = self.getDistanceToPlane(planepoints, p2) geometricMean = self._getTrapMeanLength(p0, p1, newp2, p3) if dnormal / geometricMean > OFFPLANE_TOLERANCE: raise ShakeMapException( 'Points in quadrilateral are not co-planar') newP0 = p0.toPoint() newP1 = p1.toPoint() newP2 = newp2.toPoint() newP3 = p3.toPoint() return [newP0, newP1, newP2, newP3]
def addFiles(self, files): for f in files: if not os.path.isfile(f): raise ShakeMapException('Input file %s could not be found' % f) self.files += files
def fromTrace(cls, xp0, yp0, xp1, yp1, zp, widths, dips, strike=None, reference=None): """ Create a fault object from a set of vertices that define the top of the fault, and an array of widths/dips. These top of rupture points are defined by specifying the x and y coordinates of each of the two vertices, and then specifying an array of depths,widths, and dips for each rectangle. :param xp0: Array of longitude coordinates for the first (top of rupture) vertex of each rectangle (decimal degrees). :param yp0: Array of latitude coordinates for the first (top of rupture) vertex of each rectangle (decimal degrees). :param xp1: Array of longitude coordinates for the second (top of rupture) vertex of each rectangle (decimal degrees). :param yp1: Array of latitude coordinates for the second (top of rupture) vertex of each rectangle (decimal degrees). :param zp: Array of depths for each of the top of rupture rectangles (km). :param widths: Array of widths for each of rectangle (km). :param dips: Array of dips for each of rectangle (degrees). :param strike: If None then strike is computed from verticies of top edge of each quadrilateral. If a scalar, then all quadrilaterals are constructed assuming this strike direction. If a vector with the same length as the trace coordinates then it specifies the strike for each quadrilateral. :param reference: String explaining where the fault definition came from (publication style reference, etc.) :returns: Fault object, where the fault is modeled as a series of rectangles. """ if len(xp0) == len(yp0) == len(xp1) == len(yp1) == len(zp) == len( dips) == len(widths): pass else: raise ShakeMapException( 'Number of xp0,yp0,xp1,yp1,zp,widths,dips points must be equal.' ) if strike is None: pass else: if (len(xp0) == len(strike)) | (len(strike) == 1): pass else: raise ShakeMapException( 'Strike must be None, scalar, or same length as trace coordinates.' ) # convert dips to radians dips = np.radians(dips) # ensure that all input sequences are numpy arrays xp0 = np.array(xp0, dtype='d') xp1 = np.array(xp1, dtype='d') yp0 = np.array(yp0, dtype='d') yp1 = np.array(yp1, dtype='d') zp = np.array(zp, dtype='d') widths = np.array(widths, dtype='d') dips = np.array(dips, dtype='d') # get a projection object west = np.min((xp0.min(), xp1.min())) east = np.max((xp0.max(), xp1.max())) south = np.min((yp0.min(), yp1.min())) north = np.max((yp0.max(), yp1.max())) # projected coordinates are in km proj = get_orthographic_projection(west, east, north, south) surfaces = [] xp2 = np.zeros_like(xp0) xp3 = np.zeros_like(xp0) yp2 = np.zeros_like(xp0) yp3 = np.zeros_like(xp0) zpdown = np.zeros_like(zp) for i in range(0, len(xp0)): # Project the top edge coordinates p0x, p0y = proj(xp0[i], yp0[i]) p1x, p1y = proj(xp1[i], yp1[i]) # Get the rotation angle defined by these two points if strike is None: dx = p1x - p0x dy = p1y - p0y theta = np.arctan2(dx, dy) # theta is angle from north elif len(strike) == 1: theta = np.radians(strike) else: theta = np.radians(strike[i]) R = np.array([[np.cos(theta), -np.sin(theta)], [np.sin(theta), np.cos(theta)]]) # Rotate the top edge points into a new coordinate system (vertical # line) p0 = np.array([p0x, p0y]) p1 = np.array([p1x, p1y]) p0p = np.dot(R, p0) p1p = np.dot(R, p1) # Get right side coordinates in project,rotated system dz = np.sin(dips[i]) * widths[i] dx = np.cos(dips[i]) * widths[i] p3xp = p0p[0] + dx p3yp = p0p[1] p2xp = p1p[0] + dx p2yp = p1p[1] # Get right side coordinates in un-rotated projected system p3p = np.array([p3xp, p3yp]) p2p = np.array([p2xp, p2yp]) Rback = np.array([[np.cos(-theta), -np.sin(-theta)], [np.sin(-theta), np.cos(-theta)]]) p3 = np.dot(Rback, p3p) p2 = np.dot(Rback, p2p) p3x = np.array([p3[0]]) p3y = np.array([p3[1]]) p2x = np.array([p2[0]]) p2y = np.array([p2[1]]) # project lower edge points back to lat/lon coordinates lon3, lat3 = proj(p3x, p3y, reverse=True) lon2, lat2 = proj(p2x, p2y, reverse=True) xp2[i] = lon2 xp3[i] = lon3 yp2[i] = lat2 yp3[i] = lat3 zpdown[i] = zp[i] + dz # assemble the vertices as the Fault constructor needs them... # which is: for each rectangle, there should be the four corners, the # first corner repeated, and then a nan. nrects = len(zp) anan = np.ones_like(xp0) * np.nan lon = np.array(list(zip(xp0, xp1, xp2, xp3, xp0, anan))).reshape( (nrects, 6)).flatten(order='C') lat = np.array(list(zip(yp0, yp1, yp2, yp3, yp0, anan))).reshape( (nrects, 6)).flatten(order='C') # we need an array of depths, but we need to double each zp and zpdown # element we have dep = [] for i in range(0, nrects): dep += [zp[i], zp[i], zpdown[i], zpdown[i], zp[i], np.nan] dep = np.array(dep) # take the nans off the end of each array lon = lon[0:-1] lat = lat[0:-1] dep = dep[0:-1] return cls(lon, lat, dep, reference)
def _calc_rupture_distance(P0, P1, P2, P3, points): """ Calculate the shortest distance from a set of points to a rupture surface. :param P0: Point object, representing the first top-edge vertex of a fault quadrilateral. :param P1: Point object, representing the second top-edge vertex of a fault quadrilateral. :param P2: Point object, representing the first bottom-edge vertex of a fault quadrilateral. :param P3: Point object, representing the second bottom-edge vertex of a fault quadrilateral. :param points: Numpy array Nx3 of points (ECEF) to calculate distance from. :returns: Array of size N of distances (in km) from input points to rupture surface. """ # Convert to ecef p0 = Vector.fromPoint(P0) p1 = Vector.fromPoint(P1) p2 = Vector.fromPoint(P2) p3 = Vector.fromPoint(P3) # Make a unit vector normal to the plane normalVector = (p1 - p0).cross(p2 - p0).norm() dist = np.ones_like(points[:, 0]) * np.nan p0d = p0.getArray() - points p1d = p1.getArray() - points p2d = p2.getArray() - points p3d = p3.getArray() - points # Create 4 planes with normals pointing outside rectangle n0 = (p1 - p0).cross(normalVector).getArray() n1 = (p2 - p1).cross(normalVector).getArray() n2 = (p3 - p2).cross(normalVector).getArray() n3 = (p0 - p3).cross(normalVector).getArray() sgn0 = np.signbit(np.sum(n0 * p0d, axis=1)) sgn1 = np.signbit(np.sum(n1 * p1d, axis=1)) sgn2 = np.signbit(np.sum(n2 * p2d, axis=1)) sgn3 = np.signbit(np.sum(n3 * p3d, axis=1)) inside_idx = (sgn0 == sgn1) & (sgn1 == sgn2) & (sgn2 == sgn3) dist[inside_idx] = np.power( np.abs(np.sum(p0d[inside_idx, :] * normalVector.getArray(), axis=1)), 2) outside_idx = np.logical_not(inside_idx) s0 = _distance_sq_to_segment(p0d, p1d) s1 = _distance_sq_to_segment(p1d, p2d) s2 = _distance_sq_to_segment(p2d, p3d) s3 = _distance_sq_to_segment(p3d, p0d) smin = np.minimum(np.minimum(s0, s1), np.minimum(s2, s3)) dist[outside_idx] = smin[outside_idx] dist = np.sqrt(dist) / 1000.0 shp = dist.shape if len(shp) == 1: dist.shape = (shp[0], 1) if np.any(np.isnan(dist)): raise ShakeMapException("Could not calculate some distances!") dist = np.fliplr(dist) return dist
def setMechanism(self, mech, rake=None, dip=None): """ Set the earthquake mechanism manually (overriding any values read in from event.xml or source.txt. If rake and dip are not specified, they will be assigned by mechanism as follows: +-------+--------+-----+ | Mech | Rake | Dip | +=======+========+=====+ | RS | 90 | 40 | +-------+--------+-----+ | NM | -90 | 50 | +-------+--------+-----+ | SS | 0 | 90 | +-------+--------+-----+ | ALL | 45 | 90 | +-------+--------+-----+ :param mech: String - one of 'RS' (reverse), 'NM' (normal), 'SS' (strike slip), or 'ALL' (unknown). :param rake: Value between -360 and 360 degrees. If set, will override default value for mechanism (see table above). :param dip: Value betweeen 0 and 90 degrees. If set, will override default value for mechanism (see table above). Value will be converted to range between -180 and 180 degrees. """ mechs = { 'RS': { 'rake': 90.0, 'dip': 40.0 }, 'NM': { 'rake': -90.0, 'dip': 50.0 }, 'SS': { 'rake': 0.0, 'dip': 90.0 }, 'ALL': { 'rake': 45.0, 'dip': 90.0 } } if mech not in list(mechs.keys()): raise ShakeMapException('Mechanism must be one of: %s' % str(list(mechs.keys()))) if dip is not None: if dip < 0 or dip > 90: raise ShakeMapException('Dip must be in range 0-90 degrees.') if dip < 0 or dip > 90: raise ShakeMapException('Dip must be in range 0-90 degrees.') else: dip = mechs[mech]['dip'] if rake is not None: if rake < -180: rake += 360 if rake > 180: rake -= 360 if rake < -180 or rake > 180: raise ShakeMapException( 'Rake must be transformable to be in range -180 to 180 degrees.' ) else: rake = mechs[mech]['rake'] self.setEventParam('dip', dip) self.setEventParam('rake', rake) self.setEventParam('mech', mech)