def check_aki_richards_convention(cls, edges): """ Verify that surface (as defined by corner points) conforms with Aki and Richard convention (i.e. surface dips right of surface strike) This method doesn't have to be called by hands before creating the surface object, because it is called from :meth:`from_fault_data`. """ # 1) extract 4 corner points of surface mesh # 2) compute cross products between left and right edges and top edge # (these define vectors normal to the surface) # 3) compute dot products between cross product results and # position vectors associated with upper left and right corners (if # both angles are less then 90 degrees then the surface is correctly # defined) ul = edges[0].points[0] ur = edges[0].points[-1] bl = edges[-1].points[0] br = edges[-1].points[-1] ul, ur, bl, br = spherical_to_cartesian( [ul.longitude, ur.longitude, bl.longitude, br.longitude], [ul.latitude, ur.latitude, bl.latitude, br.latitude], [ul.depth, ur.depth, bl.depth, br.depth], ) top_edge = ur - ul left_edge = bl - ul right_edge = br - ur left_cross_top = numpy.cross(left_edge, top_edge) right_cross_top = numpy.cross(right_edge, top_edge) left_cross_top /= numpy.sqrt(numpy.dot(left_cross_top, left_cross_top)) right_cross_top /= numpy.sqrt( numpy.dot(right_cross_top, right_cross_top) ) ul /= numpy.sqrt(numpy.dot(ul, ul)) ur /= numpy.sqrt(numpy.dot(ur, ur)) # rounding to 1st digit, to avoid ValueError raised for floating point # imprecision angle_ul = round( numpy.degrees(numpy.arccos(numpy.dot(ul, left_cross_top))), 1 ) angle_ur = round( numpy.degrees(numpy.arccos(numpy.dot(ur, right_cross_top))), 1 ) if (angle_ul > 90) or (angle_ur > 90): raise ValueError( "Surface does not conform with Aki & Richards convention" )
def from_fault_data(cls, edges, mesh_spacing): """ Create and return a fault surface using fault source data. :param edges: A list of at least two horizontal edges of the surface as instances of :class:`openquake.hazardlib.geo.line.Line`. The list should be in top-to-bottom order (the shallowest edge first). :param mesh_spacing: Distance between two subsequent points in a mesh, in km. :returns: An instance of :class:`ComplexFaultSurface` created using that data. :raises ValueError: If requested mesh spacing is too big for the surface geometry (doesn't allow to put a single mesh cell along length and/or width). Uses :meth:`check_fault_data` for checking parameters. """ cls.check_fault_data(edges, mesh_spacing) surface_nodes = [complex_fault_node(edges)] mean_length = numpy.mean([edge.get_length() for edge in edges]) num_hor_points = int(round(mean_length / mesh_spacing)) + 1 if num_hor_points <= 1: raise ValueError( 'mesh spacing %.1f km is too big for mean length %.1f km' % (mesh_spacing, mean_length)) edges = [ edge.resample_to_num_points(num_hor_points).points for i, edge in enumerate(edges) ] vert_edges = [Line(v_edge) for v_edge in zip(*edges)] mean_width = numpy.mean([v_edge.get_length() for v_edge in vert_edges]) num_vert_points = int(round(mean_width / mesh_spacing)) + 1 if num_vert_points <= 1: raise ValueError( 'mesh spacing %.1f km is too big for mean width %.1f km' % (mesh_spacing, mean_width)) points = zip(*[ v_edge.resample_to_num_points(num_vert_points).points for v_edge in vert_edges ]) mesh = RectangularMesh.from_points_list(list(points)) assert 1 not in mesh.shape self = cls(mesh) self.surface_nodes = surface_nodes return self
def from_fault_data(cls, edges, mesh_spacing): """ Create and return a fault surface using fault source data. :param edges: A list of at least two horizontal edges of the surface as instances of :class:`openquake.hazardlib.geo.line.Line`. The list should be in top-to-bottom order (the shallowest edge first). :param mesh_spacing: Distance between two subsequent points in a mesh, in km. :returns: An instance of :class:`ComplexFaultSurface` created using that data. :raises ValueError: If requested mesh spacing is too big for the surface geometry (doesn't allow to put a single mesh cell along length and/or width). Uses :meth:`check_fault_data` for checking parameters. """ cls.check_fault_data(edges, mesh_spacing) surface_nodes = [complex_fault_node(edges)] mean_length = numpy.mean([edge.get_length() for edge in edges]) num_hor_points = int(round(mean_length / mesh_spacing)) + 1 if num_hor_points <= 1: raise ValueError( 'mesh spacing %.1f km is too big for mean length %.1f km' % (mesh_spacing, mean_length) ) edges = [edge.resample_to_num_points(num_hor_points).points for i, edge in enumerate(edges)] vert_edges = [Line(v_edge) for v_edge in zip(*edges)] mean_width = numpy.mean([v_edge.get_length() for v_edge in vert_edges]) num_vert_points = int(round(mean_width / mesh_spacing)) + 1 if num_vert_points <= 1: raise ValueError( 'mesh spacing %.1f km is too big for mean width %.1f km' % (mesh_spacing, mean_width) ) points = zip(*[v_edge.resample_to_num_points(num_vert_points).points for v_edge in vert_edges]) mesh = RectangularMesh.from_points_list(list(points)) assert 1 not in mesh.shape self = cls(mesh) self.surface_nodes = surface_nodes return self
def intervals_between(lon1, lat1, depth1, lon2, lat2, depth2, length): """ Find a list of points between two given ones that lie on the same great circle arc and are equally spaced by ``length`` km. :param float lon1, lat1, depth1: Coordinates of a point to start placing intervals from. The first point in the resulting list has these coordinates. :param float lon2, lat2, depth2: Coordinates of the other end of the great circle arc segment to put intervals on. The last resulting point might be closer to the first reference point than the second one or further, since the number of segments is taken as rounded division of length between two reference points and ``length``. :param length: Required distance between two subsequent resulting points, in km. :returns: Tuple of three 1d numpy arrays: longitudes, latitudes and depths of resulting points respectively. Rounds the distance between two reference points with respect to ``length`` and calls :func:`npoints_towards`. """ assert length > 0 hdist = geodetic_distance(lon1, lat1, lon2, lat2) vdist = depth2 - depth1 # if this method is called multiple times with coordinates that are # separated by the same distance, because of floating point imprecisions # the total distance may have slightly different values (for instance if # the distance between two set of points is 65 km, total distance can be # 64.9999999999989910 and 65.0000000000020322). These two values bring to # two different values of num_intervals (32 in the first case and 33 in # the second), and this is a problem because for the same distance we # should have the same number of intervals. To reduce potential differences # due to floating point errors, we therefore round total_distance to a # fixed precision (7) total_distance = round(numpy.sqrt(hdist ** 2 + vdist ** 2), 7) num_intervals = int(round(total_distance / length)) if num_intervals == 0: return numpy.array([lon1]), numpy.array([lat1]), numpy.array([depth1]) dist_factor = (length * num_intervals) / total_distance return npoints_towards( lon1, lat1, depth1, azimuth(lon1, lat1, lon2, lat2), hdist * dist_factor, vdist * dist_factor, num_intervals + 1 )
def intervals_between(lon1, lat1, depth1, lon2, lat2, depth2, length): """ Find a list of points between two given ones that lie on the same great circle arc and are equally spaced by ``length`` km. :param float lon1, lat1, depth1: Coordinates of a point to start placing intervals from. The first point in the resulting list has these coordinates. :param float lon2, lat2, depth2: Coordinates of the other end of the great circle arc segment to put intervals on. The last resulting point might be closer to the first reference point than the second one or further, since the number of segments is taken as rounded division of length between two reference points and ``length``. :param length: Required distance between two subsequent resulting points, in km. :returns: Tuple of three 1d numpy arrays: longitudes, latitudes and depths of resulting points respectively. Rounds the distance between two reference points with respect to ``length`` and calls :func:`npoints_towards`. """ assert length > 0 hdist = geodetic_distance(lon1, lat1, lon2, lat2) vdist = depth2 - depth1 # if this method is called multiple times with coordinates that are # separated by the same distance, because of floating point imprecisions # the total distance may have slightly different values (for instance if # the distance between two set of points is 65 km, total distance can be # 64.9999999999989910 and 65.0000000000020322). These two values bring to # two different values of num_intervals (32 in the first case and 33 in # the second), and this is a problem because for the same distance we # should have the same number of intervals. To reduce potential differences # due to floating point errors, we therefore round total_distance to a # fixed precision (7) total_distance = round(numpy.sqrt(hdist**2 + vdist**2), 7) num_intervals = int(round(total_distance / length)) if num_intervals == 0: return numpy.array([lon1]), numpy.array([lat1]), numpy.array([depth1]) dist_factor = (length * num_intervals) / total_distance return npoints_towards(lon1, lat1, depth1, azimuth(lon1, lat1, lon2, lat2), hdist * dist_factor, vdist * dist_factor, num_intervals + 1)
def _get_rupture_dimensions(self, fault_length, fault_width, mag): """ Calculate rupture dimensions for a given magnitude. :param fault_length: The length of the fault as a sum of all segments, in km. :param fault_width: The width of the fault, in km. :param mag: Magnitude value to calculate rupture geometry for. :returns: A tuple of two integer items, representing rupture's dimensions: number of mesh points along length and along width respectively. The rupture is reshaped (conserving area, if possible) if one of dimensions exceeds fault geometry. If both do, the rupture is considered to cover the whole fault. """ area = self.magnitude_scaling_relationship.get_median_area( mag, self.rake) rup_length = math.sqrt(area * self.rupture_aspect_ratio) rup_width = area / rup_length # clip rupture's length and width to fault's length and width # if there is no way to fit the rupture otherwise if area >= fault_length * fault_width: rup_length = fault_length rup_width = fault_width # reshape rupture (conserving area) if its length or width # exceeds fault's length or width elif rup_width > fault_width: rup_length = rup_length * (rup_width / fault_width) rup_width = fault_width elif rup_length > fault_length: rup_width = rup_width * (rup_length / fault_length) rup_length = fault_length # round rupture dimensions with respect to mesh_spacing # and compute number of points in the rupture along length # and width (aka strike and dip) rup_cols = int(round(rup_length / self.rupture_mesh_spacing) + 1) rup_rows = int(round(rup_width / self.rupture_mesh_spacing) + 1) return rup_cols, rup_rows
def _get_min_mag_and_num_bins(self): """ Estimate the number of bins in the histogram and return it along with the first bin center value. Rounds ``min_mag`` and ``max_mag`` with respect to ``bin_width`` to make the distance between them include integer number of bins. :returns: A tuple of 2 items: first bin center, and total number of bins. """ min_mag = round(self.min_mag / self.bin_width) * self.bin_width max_mag = (round((self.char_mag + DELTA_CHAR / 2) / self.bin_width) * self.bin_width) min_mag += self.bin_width / 2.0 max_mag -= self.bin_width / 2.0 # here we use math round on the result of division and not just # cast it to integer because for some magnitude values that can't # be represented as an IEEE 754 double precisely the result can # look like 7.999999999999 which would become 7 instead of 8 # being naively casted to int so we would lose the last bin. num_bins = int(round((max_mag - min_mag) / self.bin_width)) + 1 return min_mag, num_bins
def _return_tables(self, mag, imt, val_type): """ Returns the vector of ground motions or standard deviations corresponding to the specific magnitude and intensity measure type. :param val_type: String indicating the type of data {"IMLs", "Total", "Inter" etc} """ if imt.name in 'PGA PGV': # Get scalar imt if val_type == "IMLs": iml_table = self.imls[imt.name][:] else: iml_table = self.stddevs[val_type][imt.name][:] n_d, n_s, n_m = iml_table.shape iml_table = iml_table.reshape([n_d, n_m]) else: if val_type == "IMLs": periods = self.imls["T"][:] iml_table = self.imls["SA"][:] else: periods = self.stddevs[val_type]["T"][:] iml_table = self.stddevs[val_type]["SA"][:] low_period = round(periods[0], 7) high_period = round(periods[-1], 7) if (round(imt.period, 7) < low_period) or (round(imt.period, 7) > high_period): raise ValueError("Spectral period %.3f outside of valid range " "(%.3f to %.3f)" % (imt.period, periods[0], periods[-1])) # Apply log-log interpolation for spectral period interpolator = interp1d(numpy.log10(periods), numpy.log10(iml_table), axis=1) iml_table = 10.**interpolator(numpy.log10(imt.period)) return self.apply_magnitude_interpolation(mag, iml_table)
def _return_tables(self, mag, imt, val_type): """ Returns the vector of ground motions or standard deviations corresponding to the specific magnitude and intensity measure type. :param val_type: String indicating the type of data {"IMLs", "Total", "Inter" etc} """ if isinstance(imt, (imt_module.PGA, imt_module.PGV)): # Get scalar imt if val_type == "IMLs": iml_table = self.imls[str(imt)][:] else: iml_table = self.stddevs[val_type][str(imt)][:] n_d, n_s, n_m = iml_table.shape iml_table = iml_table.reshape([n_d, n_m]) else: if val_type == "IMLs": periods = self.imls["T"][:] iml_table = self.imls["SA"][:] else: periods = self.stddevs[val_type]["T"][:] iml_table = self.stddevs[val_type]["SA"][:] low_period = round(periods[0], 7) high_period = round(periods[-1], 7) if imt.period < low_period or imt.period > high_period: raise ValueError("Spectral period %.3f outside of valid range " "(%.3f to %.3f)" % (imt.period, periods[0], periods[-1])) # Apply log-log interpolation for spectral period interpolator = interp1d(numpy.log10(periods), numpy.log10(iml_table), axis=1) iml_table = 10. ** interpolator(numpy.log10(imt.period)) return self.apply_magnitude_interpolation(mag, iml_table)