Source code for shakelib.rupture.edge_rupture

#!/usr/bin/env python

# stdlib modules
import copy

# third party imports
import numpy as np
from openquake.hazardlib.geo.point import Point

from impactutils.vectorutils.ecef import latlon2ecef
from impactutils.vectorutils.vector import Vector
from impactutils.time.ancient_time import HistoricTime
from shakelib.rupture.base import Rupture
import shakelib.rupture.utils as utils
import shakelib.rupture.gc2 as gc2


[docs]class EdgeRupture(Rupture): """ Rupture class that representst the rupture surface by specifying the top edge and the bottom edges. These edges do not need to be horizontal. The freedom to allow for non-horizontal edges (as compared to QuadRupture) comes at the cost of slower distance calculations. This is because the rupture must be discretized and then the distances are compued in a brute force fashion based on this mesh, which can be quite large. """ def __init__(self, d, origin, mesh_dx=0.5): """ Initialization of an EdgeRupture from a GeoJSON dictionary and an Origin. Args: d (dict): Rupture GeoJSON dictionary. origin (Origin): Reference to a ShakeMap Origin object. mesh_dx (float): Target spacing (in km) for rupture discretization; default is 0.5 km and it is only used if the rupture file is an EdgeRupture. Returns: EdgeRupture instance. """ polys = d['features'][0]['geometry']['coordinates'][0] n_polygons = len(polys) toplons = np.empty(shape=(0, 0)) toplats = np.empty(shape=(0, 0)) topdeps = np.empty(shape=(0, 0)) botlons = np.empty(shape=(0, 0)) botlats = np.empty(shape=(0, 0)) botdeps = np.empty(shape=(0, 0)) g_ind = 0 group_index = [] for i in range(n_polygons): p = polys[i] n_points = len(p) n_pairs = int((n_points - 1) / 2) p_lons = [pt[0] for pt in p][0:-1] p_lats = [pt[1] for pt in p][0:-1] p_depths = [pt[2] for pt in p][0:-1] tlon = np.array(p_lons[0:n_pairs]) blon = np.array(p_lons[(n_pairs):])[::-1] tlat = np.array(p_lats[0:n_pairs]) blat = np.array(p_lats[(n_pairs):])[::-1] tdep = np.array(p_depths[0:n_pairs]) bdep = np.array(p_depths[(n_pairs):])[::-1] toplons = np.append(toplons, tlon) toplats = np.append(toplats, tlat) topdeps = np.append(topdeps, tdep) botlons = np.append(botlons, blon) botlats = np.append(botlats, blat) botdeps = np.append(botdeps, bdep) group_index.extend([g_ind] * n_pairs) g_ind = g_ind + 1 reference = d['features'][0]['properties']['reference'] # Add origin information to metadata odict = origin.__dict__ for k, v in odict.items(): if isinstance(v, HistoricTime): d['metadata'][k] = v.strftime('%Y-%m-%dT%H:%M:%SZ') else: d['metadata'][k] = v d['metadata']['mesh_dx'] = mesh_dx self._geojson = d self._toplons = np.array(toplons) self._toplats = np.array(toplats) self._topdeps = np.array(topdeps) self._botlons = np.array(botlons) self._botlats = np.array(botlats) self._botdeps = np.array(botdeps) self._origin = origin self._group_index = np.array(group_index) self._mesh_dx = mesh_dx self._reference = reference self._computeStikeDip()
[docs] @classmethod def fromArrays(cls, toplons, toplats, topdeps, botlons, botlats, botdeps, origin, group_index=None, mesh_dx=0.5, reference=''): """ Class method to initialize an EdgeRupture class from arrays of longitude, latitude, and depth along the top and bottom edges. Args: toplons (ndarray): Array of top edge longitudes. toplats (ndarray): Array of top edge latitudes. topdeps (ndarray): Array of top edge depths (km). botlons (ndarray): Array of bot edge longitudes. botlats (ndarray): Array of bot edge latitudes. botdeps (ndarray): Array of bot edge depths (km). origin (Origin): Reference to a ShakeMap Origin object. group_index (ndarray): Optional array of group index. If None, then assume only single group. mesh_dx (float): Target spacing (in km) for rupture discretization. reference (str): Citable reference for rupture. Returns: EdgeRupture instance. """ toplons = np.array(toplons) toplats = np.array(toplats) topdeps = np.array(topdeps) botlons = np.array(botlons) botlats = np.array(botlats) botdeps = np.array(botdeps) if group_index is not None: group_index = np.array(group_index) else: group_index = np.zeros_like(toplons) coords = [] u_groups = np.unique(group_index) n_groups = len(u_groups) for i in range(n_groups): ind = np.where(u_groups[i] == group_index)[0] lons = np.concatenate([toplons[ind], botlons[ind][::-1], toplons[ind][0].reshape((1,))]) lats = np.concatenate([toplats[ind], botlats[ind][::-1], toplats[ind][0].reshape((1,))]) deps = np.concatenate([topdeps[ind], botdeps[ind][::-1], topdeps[ind][0].reshape((1,))]) poly = [] for lon, lat, dep in zip(lons, lats, deps): poly.append([lon, lat, dep]) coords.append(poly) d = {"type": "FeatureCollection", "metadata": {}, "features": [{ "type": "Feature", "properties": { "rupture type": "rupture extent", "reference": reference, }, "geometry": { "type": "MultiPolygon", "coordinates": [coords] } }]} # Add origin information to metadata odict = origin.__dict__ for k, v in odict.items(): if isinstance(v, HistoricTime): d['metadata'][k] = v.strftime('%Y-%m-%dT%H:%M:%SZ') else: d['metadata'][k] = v d['metadata']['mesh_dx'] = mesh_dx return cls(d, origin, mesh_dx)
[docs] def getLength(self): """ Compute length of rupture (km). For EdgeRupture, we compute the length as the length of the top edge projected to the surface. Returns: float: Rupture length in km. """ lons = self._toplons lats = self._toplats seg = self._group_index groups = np.unique(seg) ng = len(groups) rlength = 0 for i in range(ng): group_segments = np.where(groups[i] == seg)[0] nseg = len(group_segments) - 1 for j in range(nseg): ind = group_segments[j] P0 = Point(lons[ind], lats[ind]) P1 = Point(lons[ind + 1], lats[ind + 1]) dist = P0.distance(P1) rlength = rlength + dist return rlength
[docs] def getWidth(self): """ Compute average rupture width in km. For EdgeRupture, we compute this as the rupture area divided by teh rupture length. Returns: float: Rupture width in km. """ area = self.getArea() length = self.getLength() return area / length
[docs] def getArea(self): """ Compute the rupture area. For EdgeRupture, we compute this by grouping the traces into "quadrilaterals" for which the verticies may not be co-planar. We then break up the quadrilaterals into triangles for which we can compute area. Returns: float: Rupture area in square km. """ seg = self._group_index groups = np.unique(seg) ng = len(groups) area = 0 for i in range(ng): group_segments = np.where(groups[i] == seg)[0] nseg = len(group_segments) - 1 for j in range(nseg): ind = group_segments[j] p0 = latlon2ecef(self._toplats[ind], self._toplons[ind], self._topdeps[ind]) p1 = latlon2ecef(self._toplats[ind + 1], self._toplons[ind + 1], self._topdeps[ind + 1]) p2 = latlon2ecef(self._botlats[ind + 1], self._botlons[ind + 1], self._botdeps[ind + 1]) p3 = latlon2ecef(self._botlats[ind], self._botlons[ind], self._botdeps[ind]) a = np.sqrt((p1[0] - p0[0])**2 + (p1[1] - p0[1])**2 + (p1[2] - p0[2])**2) b = np.sqrt((p2[0] - p0[0])**2 + (p2[1] - p0[1])**2 + (p2[2] - p0[2])**2) c = np.sqrt((p2[0] - p1[0])**2 + (p2[1] - p1[1])**2 + (p2[2] - p1[2])**2) s = (a + b + c) / 2 A1 = np.sqrt(s * (s - a) * (s - b) * (s - c)) a = np.sqrt((p0[0] - p3[0])**2 + (p0[1] - p3[1])**2 + (p0[2] - p3[2])**2) b = np.sqrt((p2[0] - p3[0])**2 + (p2[1] - p3[1])**2 + (p2[2] - p3[2])**2) c = np.sqrt((p0[0] - p2[0])**2 + (p0[1] - p2[1])**2 + (p0[2] - p2[2])**2) s = (a + b + c) / 2 A2 = np.sqrt(s * (s - a) * (s - b) * (s - c)) area = area + (A1 + A2) / 1000 / 1000 return area
[docs] def getStrike(self): """ Return representative strike for this rupture. Note that strike can vary along the rupture. Returns: float: Strike angle in degrees. """ return self._strike
[docs] def getDip(self): """ Representative dip for this rupture. Note that dip can vary along the rupture. Returns: float: dip angle in degrees. """ return self._dip
@property def lats(self): """ Return an array of latitudes for the rupture verticies arranged for plotting purposes; will give an outline of each group connected segments. Returns: array: Numpy array of closed-loop latitude values; disconnected segments are separated by nans. """ lats = np.empty(shape=(0,)) groups = self._group_index u_groups = np.unique(groups) ng = len(u_groups) nan = np.array(np.nan).reshape(1,) for i in range(ng): top_lats = self._toplats[groups == u_groups[i]] top0 = top_lats[0].reshape((1,)) bot_lats = self._botlats[groups == u_groups[i]] lats = np.concatenate((lats, top_lats, bot_lats[::-1], top0, nan)) return np.array(lats) @property def lons(self): """ Return an array of longitudes for the rupture verticies arranged for plotting purposes; will give an outline of each group connected segments. Returns: array: Numpy array of closed-loop longitude values; disconnected segments are separated by nans. """ lons = np.empty(shape=(0,)) groups = self._group_index u_groups = np.unique(groups) ng = len(u_groups) nan = np.array(np.nan).reshape(1,) for i in range(ng): top_lons = self._toplons[groups == u_groups[i]] top0 = top_lons[0].reshape((1,)) bot_lons = self._botlons[groups == u_groups[i]] lons = np.concatenate((lons, top_lons, bot_lons[::-1], top0, nan)) return np.array(lons) @property def depths(self): """ Return an array of depths for the rupture verticies arranged for plotting purposes; will give an outline of each group connected segments. Returns: array: Numpy array of closed-loop latitude values; disconnected segments are separated by nans. """ deps = np.empty(shape=(0,)) groups = self._group_index u_groups = np.unique(groups) ng = len(u_groups) nan = np.array(np.nan).reshape(1,) for i in range(ng): top_deps = self._topdeps[groups == u_groups[i]] top0 = top_deps[0].reshape((1,)) bot_deps = self._botdeps[groups == u_groups[i]] deps = np.concatenate((deps, top_deps, bot_deps[::-1], top0, nan)) return np.array(deps) def _getGroupIndex(self): """ Return a list of segment group indexes. Returns: list: Segment group indexes; length equals the number of quadrilaterals. """ return copy.deepcopy(self._group_index) def _computeStikeDip(self): """ Loop over all triangles and get the average normal, north, and up vectors in ECEF. Use these to compute a representative strike and dip. """ seg = self._group_index groups = np.unique(seg) ng = len(groups) norm_vec = Vector(0, 0, 0) north_vec = Vector(0, 0, 0) up_vec = Vector(0, 0, 0) for i in range(ng): group_segments = np.where(groups[i] == seg)[0] nseg = len(group_segments) - 1 for j in range(nseg): ind = group_segments[j] P0 = Point(self._toplons[ind], self._toplats[ind], self._topdeps[ind]) P1 = Point(self._toplons[ind + 1], self._toplats[ind + 1], self._topdeps[ind + 1]) P2 = Point(self._botlons[ind + 1], self._botlats[ind + 1], self._botdeps[ind + 1]) P3 = Point(self._botlons[ind], self._botlats[ind], self._botdeps[ind]) P1up = Point(self._toplons[ind + 1], self._toplats[ind + 1], self._topdeps[ind + 1] - 1.0) P1N = Point(self._toplons[ind + 1], self._toplats[ind + 1] + 0.001, self._topdeps[ind + 1]) P3up = Point(self._botlons[ind], self._botlats[ind], self._botdeps[ind] - 1.0) P3N = Point(self._botlons[ind], self._botlats[ind] + 0.001, self._botdeps[ind]) p0 = Vector.fromPoint(P0) p1 = Vector.fromPoint(P1) p2 = Vector.fromPoint(P2) p3 = Vector.fromPoint(P3) p1up = Vector.fromPoint(P1up) p1N = Vector.fromPoint(P1N) p3up = Vector.fromPoint(P3up) p3N = Vector.fromPoint(P3N) # Sides s01 = p1 - p0 s02 = p2 - p0 s03 = p3 - p0 s21 = p1 - p2 s23 = p3 - p2 # First triangle t1norm = (s02.cross(s01)).norm() a = s01.mag() b = s02.mag() c = s21.mag() s = (a + b + c) / 2 A1 = np.sqrt(s * (s - a) * (s - b) * (s - c)) / 1000 # Second triangle t2norm = (s03.cross(s02)).norm() a = s03.mag() b = s23.mag() c = s02.mag() s = (a + b + c) / 2 A2 = np.sqrt(s * (s - a) * (s - b) * (s - c)) / 1000 # Up and North p1up = (p1up - p1).norm() p3up = (p3up - p3).norm() p1N = (p1N - p1).norm() p3N = (p3N - p3).norm() # Combine norm_vec = norm_vec + A1 * t1norm + A2 * t2norm north_vec = north_vec + A1 * p1N + A2 * p3N up_vec = up_vec + A1 * p1up + A2 * p3up norm_vec = norm_vec.norm() north_vec = north_vec.norm() up_vec = up_vec.norm() # Do I need to flip the vector because it is pointing down (i.e., # right-hand rule is violated)? flip = np.sign(up_vec.dot(norm_vec)) norm_vec = flip * norm_vec # Angle between up_vec and norm_vec is dip self._dip = np.arcsin(up_vec.cross(norm_vec).mag()) * 180 / np.pi # Normal vector projected to horizontal plane nvph = (norm_vec - up_vec.dot(norm_vec) * up_vec).norm() # Dip direction is angle between nvph and north; strike is orthogonal. cp = nvph.cross(north_vec) sign = np.sign(cp.dot(up_vec)) dp = nvph.dot(north_vec) strike = np.arctan2(sign * cp.mag(), dp) * 180 / np.pi - 90 if strike < -180: strike = strike + 360 self._strike = strike
[docs] def getDepthToTop(self): """ Returns: float: Depth to top of rupture in km. """ return np.min(self._topdeps)
[docs] def getQuadrilaterals(self): """ Return a list of quadrilaterals. Unlike QuadRupture, these quadrilaterals are not restricted to be coplanar or have horizontal top/bottom edges. Return: list: List of quadrilaterals, where each quadrilateral is a list of OQ Points. """ ugroup = np.unique(self._group_index) ngroup = len(ugroup) qlist = [] for i in range(ngroup): ind = np.where(self._group_index == ugroup[i])[0] nq = len(ind) - 1 for j in range(nq): P0 = Point(self._toplons[j], self._toplats[j], self._topdeps[j]) P1 = Point(self._toplons[j + 1], self._toplats[j + 1], self._topdeps[j + 1]) P2 = Point(self._botlons[j + 1], self._botlats[j + 1], self._botdeps[j + 1]) P3 = Point(self._botlons[j], self._botlats[j], self._botdeps[j]) qlist.append([P0, P1, P2, P3]) return qlist
[docs] def computeRrup(self, lon, lat, depth): """ Method for computing rupture distance. Args: lon (array): Numpy array of longitudes. lat (array): Numpy array of latitudes. depth (array): Numpy array of depths (km; positive down). Returns: array: Rupture distance (km). """ mesh_dx = self._mesh_dx # --------------------------------------------------------------------- # Sort out sites # --------------------------------------------------------------------- oldshape = lon.shape if len(oldshape) == 2: newshape = (oldshape[0] * oldshape[1], 1) else: newshape = (oldshape[0], 1) x, y, z = latlon2ecef(lat, lon, depth) x.shape = newshape y.shape = newshape z.shape = newshape sites_ecef = np.hstack((x, y, z)) # --------------------------------------------------------------------- # Get mesh # --------------------------------------------------------------------- mx = [] my = [] mz = [] u_groups = np.unique(self._group_index) n_groups = len(u_groups) for j in range(n_groups): g_ind = np.where(u_groups[j] == self._group_index)[0] nq = len(self._toplats[g_ind]) - 1 for i in range(nq): q = [Point(self._toplons[g_ind[i]], self._toplats[g_ind[i]], self._topdeps[g_ind[i]]), Point(self._toplons[g_ind[i + 1]], self._toplats[g_ind[i + 1]], self._topdeps[g_ind[i + 1]]), Point(self._botlons[g_ind[i + 1]], self._botlats[g_ind[i + 1]], self._botdeps[g_ind[i + 1]]), Point(self._botlons[g_ind[i]], self._botlats[g_ind[i]], self._botdeps[g_ind[i]]) ] mesh = utils.get_quad_mesh(q, dx=mesh_dx) mx.extend(list(np.reshape(mesh['x'], (-1,)))) my.extend(list(np.reshape(mesh['y'], (-1,)))) mz.extend(list(np.reshape(mesh['z'], (-1,)))) mesh_mat = np.array([np.array(mx), np.array(my), np.array(mz)]) # --------------------------------------------------------------------- # Compute distance # --------------------------------------------------------------------- dist = np.zeros_like(x) for i in range(len(x)): sitecol = sites_ecef[i, :].reshape([3, 1]) dif = sitecol - mesh_mat distarray = np.sqrt(np.sum(dif * dif, axis=0)) dist[i] = np.min(distarray) / 1000.0 # convert to km dist = np.reshape(dist, oldshape) return dist
[docs] def computeRjb(self, lon, lat, depth): """ Method for computing Joyner-Boore distance. Args: lon (array): Numpy array of longitudes. lat (array): Numpy array of latitudes. depth (array): Numpy array of depths (km; positive down). Returns: array: Joyner-Boore distance (km). """ mesh_dx = self._mesh_dx # --------------------------------------------------------------------- # Sort out sites # --------------------------------------------------------------------- oldshape = lon.shape if len(oldshape) == 2: newshape = (oldshape[0] * oldshape[1], 1) else: newshape = (oldshape[0], 1) x, y, z = latlon2ecef(lat, lon, depth) x.shape = newshape y.shape = newshape z.shape = newshape sites_ecef = np.hstack((x, y, z)) # --------------------------------------------------------------------- # Get mesh # --------------------------------------------------------------------- mx = [] my = [] mz = [] u_groups = np.unique(self._group_index) n_groups = len(u_groups) for j in range(n_groups): g_ind = np.where(u_groups[j] == self._group_index)[0] nq = len(self._toplats[g_ind]) - 1 for i in range(nq): q = [Point(self._toplons[g_ind[i]], self._toplats[g_ind[i]], 0), Point(self._toplons[g_ind[i + 1]], self._toplats[g_ind[i + 1]], 0), Point(self._botlons[g_ind[i + 1]], self._botlats[g_ind[i + 1]], 0), Point(self._botlons[g_ind[i]], self._botlats[g_ind[i]], 0) ] mesh = utils.get_quad_mesh(q, dx=mesh_dx) mx.extend(list(np.reshape(mesh['x'], (-1,)))) my.extend(list(np.reshape(mesh['y'], (-1,)))) mz.extend(list(np.reshape(mesh['z'], (-1,)))) mesh_mat = np.array([np.array(mx), np.array(my), np.array(mz)]) # --------------------------------------------------------------------- # Compute distance # --------------------------------------------------------------------- dist = np.zeros_like(x) for i in range(len(x)): sitecol = sites_ecef[i, :].reshape([3, 1]) dif = sitecol - mesh_mat distarray = np.sqrt(np.sum(dif * dif, axis=0)) dist[i] = np.min(distarray) / 1000.0 # convert to km dist = np.reshape(dist, oldshape) return dist
[docs] def computeGC2(self, lon, lat, depth): """ Method for computing version 2 of the Generalized Coordinate system (GC2) by Spudich and Chiou OFR 2015-1028. Args: lon (array): Numpy array of longitudes. lat (array): Numpy array of latitudes. depth (array): Numpy array of depths (km; positive down). Returns: dict: Dictionary with keys for each of the GC2-related distances, which include 'rx', 'ry', 'ry0', 'U', 'T'. """ # This just hands off to the module-level method # NOTE: Not sure if the non-horizontal top edges of EdgeRupture will # case problems. Should do some checking. It might be okay to # bring quad vertices up to the surface in this case. dict = gc2._computeGC2(self, lon, lat, depth) return dict