def prefilter_ruptures(hdf5, ridx, idx_set, sites, integration_distance): """ Determines if a rupture is likely to be inside the integration distance by considering the set of fault plane centroids. :param hdf5: Source of UCERF file as h5py.File object :param list ridx: List of indices composing the rupture sections :param dict idx_set: Set of indices for the branch :param sites: Sites for consideration (can be None!) :param float integration_distance: Maximum distance from rupture to site for consideration """ # Generate array of sites if not sites: return True centroids = numpy.array([[0., 0., 0.]], dtype="f") for idx in ridx: trace_idx = "{:s}/{:s}".format(idx_set["sec_idx"], str(idx)) centroids = numpy.vstack( [centroids, hdf5[trace_idx + "/Centroids"][:].astype("float64")]) centroids = centroids[1:, :] distance = min_geodetic_distance(centroids[:, 0], centroids[:, 1], sites.lons, sites.lats) return numpy.any(distance <= integration_distance)
def prefilter_ruptures(hdf5, ridx, idx_set, sites, integration_distance): """ Determines if a rupture is likely to be inside the integration distance by considering the set of fault plane centroids. :param hdf5: Source of UCERF file as h5py.File object :param list ridx: List of indices composing the rupture sections :param dict idx_set: Set of indices for the branch :param sites: Sites for consideration (can be None!) :param float integration_distance: Maximum distance from rupture to site for consideration """ # Generate array of sites if not sites: return True centroids = numpy.array([[0., 0., 0.]], dtype="f") for idx in ridx: trace_idx = "{:s}/{:s}".format(idx_set["sec_idx"], str(idx)) centroids = numpy.vstack([ centroids, hdf5[trace_idx + "/Centroids"][:].astype("float64")]) centroids = centroids[1:, :] distance = min_geodetic_distance(centroids[:, 0], centroids[:, 1], sites.lons, sites.lats) #logging.info("%s - %s", str(ridx), str(numpy.min(distance))) return numpy.any(distance <= integration_distance)
def get_rupture_sites(self, hdf5, ridx, src_filter, mag): """ Determines if a rupture is likely to be inside the integration distance by considering the set of fault plane centroids and returns the affected sites if any. :param hdf5: Source of UCERF file as h5py.File object :param ridx: List of indices composing the rupture sections :param src_filter: SourceFilter instance :param mag: Magnitude of the rupture for consideration :returns: The sites affected by the rupture (or None) """ centroids = [] for idx in ridx: trace_idx = "{:s}/{:s}".format(self.idx_set["sec_idx"], str(idx)) centroids.append(hdf5[trace_idx + "/Centroids"].value) centroids = numpy.concatenate(centroids) lons, lats = src_filter.sitecol.lons, src_filter.sitecol.lats distance = min_geodetic_distance(centroids[:, 0], centroids[:, 1], lons, lats) idist = src_filter.integration_distance(DEFAULT_TRT, mag) return src_filter.sitecol.filter(distance <= idist)
def filter_sites_by_distance_from_rupture_set( self, rupset_idx, sites, max_dist): """ Filter sites by distances from a set of ruptures """ with h5py.File(self.source_file, "r") as hdf5: rup_index_key = "/".join([self.idx_set["geol_idx"], "RuptureIndex"]) # Find the combination of rupture sections used in this model rupture_set = set() # Determine which of the rupture sections used in this set # of indices for i in rupset_idx: rupture_set.update(hdf5[rup_index_key][i]) centroids = np.empty([1, 3]) # For each of the identified rupture sections, retreive the # centroids for ridx in rupture_set: trace_idx = "{:s}/{:s}".format(self.idx_set["sec_idx"], str(ridx)) centroids = np.vstack([ centroids, hdf5[trace_idx + "/Centroids"][:].astype("float64")]) distance = min_geodetic_distance(centroids[1:, 0], centroids[1:, 1], sites.lons, sites.lats) idx = distance <= max_dist if np.any(idx): return rupset_idx, sites.filter(idx) else: return [], []
def _test(self, mlons, mlats, mdepths, slons, slats, sdepths, expected_mpoint_indices): mlons, mlats, mdepths = [numpy.array(arr, float) for arr in (mlons, mlats, mdepths)] slons, slats, sdepths = [numpy.array(arr, float) for arr in (slons, slats, sdepths)] actual_indices = geodetic.min_distance(mlons, mlats, mdepths, slons, slats, sdepths, indices=True) numpy.testing.assert_equal(actual_indices, expected_mpoint_indices) dists = geodetic.min_distance(mlons, mlats, mdepths, slons, slats, sdepths) expected_closest_mlons = mlons.flat[expected_mpoint_indices] expected_closest_mlats = mlats.flat[expected_mpoint_indices] expected_closest_mdepths = mdepths.flat[expected_mpoint_indices] expected_distances = geodetic.distance( expected_closest_mlons, expected_closest_mlats, expected_closest_mdepths, slons, slats, sdepths ) self.assertTrue((dists == expected_distances).all()) # testing min_geodetic_distance with the same lons and lats min_geod_distance = geodetic.min_geodetic_distance(mlons, mlats, slons, slats) min_geo_distance2 = geodetic.min_distance(mlons, mlats, mdepths * 0, slons, slats, sdepths * 0) numpy.testing.assert_almost_equal(min_geod_distance, min_geo_distance2)
def gaussian(self, radius, sigma): # Values values = numpy.zeros((len(self.mesh))) # Compute the number of expected nodes numpnts = consts.pi*radius**2/(self.cellsize**2) # Smoothing the catalogue for lon, lat, mag in zip(self.catalogue.data['longitude'], self.catalogue.data['latitude'], self.catalogue.data['magnitude']): # set the bounding box minlon, minlat = point_at(lon, lat, 225, radius*2**0.5) maxlon, maxlat = point_at(lon, lat, 45, radius*2**0.5) # find nodes within the bounding box idxs = list(self.rtree.intersection((minlon, minlat, maxlon, maxlat))) # get distances dsts = min_geodetic_distance(lon, lat, self.mesh.lons[idxs], self.mesh.lats[idxs]) jjj = numpy.nonzero(dsts < 50)[0] idxs = numpy.array(idxs) iii = idxs[jjj] # set values tmpvalues= numpy.exp(-dsts[jjj]/sigma**2) normfact = sum(tmpvalues) values[iii] += tmpvalues/normfact return values
def get_background_sids(self): """ We can apply the filtering of the background sites as a pre-processing step - this is done here rather than in the sampling of the ruptures themselves """ branch_key = self.ukey["grid_key"] with h5py.File(self.source_file, 'r') as hdf5: bg_locations = hdf5["Grid/Locations"][()] if hasattr(self, 'src_filter'): # in event based idist = self.src_filter.integration_distance[DEFAULT_TRT][-1][ 1] else: # in classical return numpy.arange(len(bg_locations)) distances = min_geodetic_distance( self.src_filter.sitecol.xyz, (bg_locations[:, 0], bg_locations[:, 1])) # Add buffer equal to half of length of median area from Mmax mmax_areas = self.msr.get_median_area( hdf5["/".join(["Grid", branch_key, "MMax"])][()], 0.0) # for instance hdf5['Grid/FM0_0_MEANFS_MEANMSR/MMax'] mmax_lengths = numpy.sqrt(mmax_areas / self.aspect) ok = distances <= (0.5 * mmax_lengths + idist) # get list of indices from array of booleans return numpy.where(ok)[0].tolist()
def get_indices(self, src, ridx, mag): """ :param src: an UCERF source :param ridx: a set of rupture indices :param mag: magnitude to use to compute the integration distance :returns: array with the IDs of the sites close to the ruptures """ centroids = src.get_centroids(ridx) mindistance = min_geodetic_distance( (centroids[:, 0], centroids[:, 1]), self.sitecol.xyz) idist = self.integration_distance(DEFAULT_TRT, mag) indices, = (mindistance <= idist).nonzero() return indices
def get_indices(self, src, ridx, mag): """ :param src: an UCERF source :param ridx: a set of rupture indices :param mag: magnitude to use to compute the integration distance :returns: array with the IDs of the sites close to the ruptures """ centroids = src.get_centroids(ridx) mindistance = min_geodetic_distance((centroids[:, 0], centroids[:, 1]), self.sitecol.xyz) idist = self.integration_distance(DEFAULT_TRT, mag) indices, = (mindistance <= idist).nonzero() return indices
def get_joyner_boore_distance(self, mesh): # Get indexes of the finite points composing the edges iupp = np.nonzero(np.isfinite(self.mesh.lons[0, :]))[0] ilow = np.flipud(np.nonzero(np.isfinite(self.mesh.lons[-1, :]))[0]) irig = np.nonzero(np.isfinite(self.mesh.lons[:, -1]))[0] ilef = np.flipud(np.nonzero(np.isfinite(self.mesh.lons[:, 0]))[0]) # Building the polygon pnts = [] for corner in [(0, iupp), (irig, -1), (-1, ilow), (ilef, 0)]: pnts.extend( zip(self.mesh.lons[corner], self.mesh.lats[corner], self.mesh.depths[corner])) perimeter = np.array(pnts) distances = geodetic.min_geodetic_distance( (perimeter[:, 0], perimeter[:, 1]), (mesh.lons, mesh.lats)) idxs = (distances < 40).nonzero()[0] # indices on the first dimension if not len(idxs): # no point is close enough, return distances as they are return distances # Get the projection proj = geo_utils.OrthographicProjection( *geo_utils.get_spherical_bounding_box(perimeter[:, 0], perimeter[:, 1])) # Mesh projected coordinates mesh_xx, mesh_yy = proj(mesh.lons[idxs], mesh.lats[idxs]) # Create the shapely Polygon using projected coordinates xp, yp = proj(perimeter[:, 0], perimeter[:, 1]) polygon = Polygon([[x, y] for x, y in zip(xp, yp)]) # Calculate the distances distances[idxs] = geo_utils.point_to_polygon_distance( polygon, mesh_xx, mesh_yy) return distances
def get_rupture_sites(self, ridx, src_filter, mag): """ Determines if a rupture is likely to be inside the integration distance by considering the set of fault plane centroids and returns the affected sites if any. :param ridx: List of indices composing the rupture sections :param src_filter: SourceFilter instance :param mag: Magnitude of the rupture for consideration :returns: The sites affected by the rupture (or None) """ centroids = self.get_centroids(ridx) distance = min_geodetic_distance((centroids[:, 0], centroids[:, 1]), src_filter.sitecol.xyz) idist = src_filter.integration_distance(DEFAULT_TRT, mag) return src_filter.sitecol.filter(distance <= idist)
def get_background_sids(self, src_filter): """ We can apply the filtering of the background sites as a pre-processing step - this is done here rather than in the sampling of the ruptures themselves """ branch_key = self.idx_set["grid_key"] idist = src_filter.integration_distance(DEFAULT_TRT) lons, lats = src_filter.sitecol.lons, src_filter.sitecol.lats with h5py.File(self.source_file, 'r') as hdf5: bg_locations = hdf5["Grid/Locations"].value distances = min_geodetic_distance( lons, lats, bg_locations[:, 0], bg_locations[:, 1]) # Add buffer equal to half of length of median area from Mmax mmax_areas = self.msr.get_median_area( hdf5["/".join(["Grid", branch_key, "MMax"])].value, 0.0) # for instance hdf5['Grid/FM0_0_MEANFS_MEANMSR/MMax'] mmax_lengths = numpy.sqrt(mmax_areas / self.aspect) ok = distances <= (0.5 * mmax_lengths + idist) # get list of indices from array of booleans return numpy.where(ok)[0].tolist()
def get_background_sids(self, src_filter): """ We can apply the filtering of the background sites as a pre-processing step - this is done here rather than in the sampling of the ruptures themselves """ branch_key = self.idx_set["grid_key"] idist = src_filter.integration_distance(DEFAULT_TRT) with h5py.File(self.source_file, 'r') as hdf5: bg_locations = hdf5["Grid/Locations"].value distances = min_geodetic_distance( src_filter.sitecol.xyz, (bg_locations[:, 0], bg_locations[:, 1])) # Add buffer equal to half of length of median area from Mmax mmax_areas = self.msr.get_median_area( hdf5["/".join(["Grid", branch_key, "MMax"])].value, 0.0) # for instance hdf5['Grid/FM0_0_MEANFS_MEANMSR/MMax'] mmax_lengths = numpy.sqrt(mmax_areas / self.aspect) ok = distances <= (0.5 * mmax_lengths + idist) # get list of indices from array of booleans return numpy.where(ok)[0].tolist()
def get_joyner_boore_distance(self, mesh) -> np.ndarray: """ Computes the Rjb distance between the rupture and the points included in the mesh provided. :param mesh: An instance of :class:`openquake.hazardlib.geo.mesh.Mesh` :returns: A :class:`numpy.ndarray` instance with the Rjb values """ blo, bla = self._get_external_boundary() distances = geodetic.min_geodetic_distance((blo, bla), (mesh.lons, mesh.lats)) idxs = (distances < 40).nonzero()[0] # indices on the first dimension if len(idxs) < 1: # no point is close enough, return distances as they are return distances # Get the projection proj = geo_utils.OrthographicProjection( *geo_utils.get_spherical_bounding_box(blo, bla)) # Mesh projected coordinates mesh_xx, mesh_yy = proj(mesh.lons[idxs], mesh.lats[idxs]) # Create the shapely Polygon using projected coordinates xp, yp = proj(blo, bla) polygon = Polygon([[x, y] for x, y in zip(xp, yp)]) # Calculate the distances distances[idxs] = geo_utils.point_to_polygon_distance( polygon, mesh_xx, mesh_yy) return distances
def fault_surface_distance(srcs, mesh_spacing, types=None, dmax=40): """ :parameter srcs: A list of openquake sources :parameter float mesh_spacing: The spacing [km] of the grid used to represent the fault surface :parameter types: The type of sources to be analysed :parameter dmax: """ # # Fault distance array lnkg = np.ones((len(srcs), len(srcs))) * dmax # # Check input information if types is None: types = (SimpleFaultSource, CharacteristicFaultSource) # # Process srcs for idxa in range(0, len(srcs)): srca = srcs[idxa] if isinstance(srca, types): # # set the mesh for the first fault surface if isinstance(srca, SimpleFaultSource): meshA, chA = _get_mesh_ch_from_simple_fs(srca, mesh_spacing) elif isinstance(srca, CharacteristicFaultSource): meshA, chA = _get_mesh_ch_from_char_fs(srca, mesh_spacing) else: raise ValueError('Unsupported fault type') # # second loop for idxB in range(idxa, len(srcs)): srcb = srcs[idxB] if isinstance(srcb, types): # # set the mesh for the second fault surface if isinstance(srcb, SimpleFaultSource): meshB, chB = _get_mesh_ch_from_simple_fs( srcb, mesh_spacing) elif isinstance(srcb, CharacteristicFaultSource): meshB, chB = _get_mesh_ch_from_char_fs( srcb, mesh_spacing) else: raise ValueError('Unsupported fault type') # # Calculate the distance between the two bounding boxes la = [(a, b) for a, b in zip(chA.lons, chA.lats)] lb = [(a, b) for a, b in zip(chB.lons, chB.lats)] tmpd = min_geodetic_distance(la, lb) # # Calculate the distance between the two fault surfaces # (if needed) if (np.amin(tmpd) > dmax): mindst = np.amin(tmpd) else: mindst = np.amin(meshA.get_min_distance(meshB)) # # Update the array lnkg[idxa, idxB] = mindst # # check that the size of the linkage matrix corresponds to the size of the # initial list of fault sources assert len(lnkg) == len(srcs) return lnkg
def get_joyner_boore_distance(self, mesh): """ See :meth:`superclass' method <openquake.hazardlib.geo.surface.base.BaseSurface.get_joyner_boore_distance>`. This is an optimized version specific to planar surface that doesn't make use of the mesh. """ # we define four great circle arcs that contain four sides # of projected planar surface: # # ↓ II ↓ # I ↓ ↓ I # ↓ + ↓ # →→→→→TL→→→→1→→→→TR→→→→→ → azimuth direction → # ↓ - ↓ # ↓ ↓ # III -3+ IV -4+ III ↓ # ↓ ↓ downdip direction # ↓ + ↓ ↓ # →→→→→BL→→→→2→→→→BR→→→→→ # ↓ - ↓ # I ↓ ↓ I # ↓ II ↓ # # arcs 1 and 2 are directed from left corners to right ones (the # direction has an effect on the sign of the distance to an arc, # as it shown on the figure), arcs 3 and 4 are directed from top # corners to bottom ones. # # then we measure distance from each of the points in a mesh # to each of those arcs and compare signs of distances in order # to find a relative positions of projections of points and # projection of a surface. # # then we consider four special cases (labeled with Roman numerals) # and either pick one of distances to arcs or a closest distance # to corner. # # indices 0, 2 and 1 represent corners TL, BL and TR respectively. arcs_lons = self.corner_lons.take([0, 2, 0, 1]) arcs_lats = self.corner_lats.take([0, 2, 0, 1]) downdip_azimuth = (self.strike + 90) % 360 arcs_azimuths = [ self.strike, self.strike, downdip_azimuth, downdip_azimuth ] mesh_lons = mesh.lons.reshape((-1, 1)) mesh_lats = mesh.lats.reshape((-1, 1)) # calculate distances from all the target points to all four arcs dists_to_arcs = geodetic.distance_to_arc(arcs_lons, arcs_lats, arcs_azimuths, mesh_lons, mesh_lats) # ... and distances from all the target points to each of surface's # corners' projections (we might not need all of those but it's # better to do that calculation once for all). dists_to_corners = geodetic.min_geodetic_distance( (self.corner_lons, self.corner_lats), mesh.xyz) # extract from ``dists_to_arcs`` signs (represent relative positions # of an arc and a point: +1 means on the left hand side, 0 means # on arc and -1 means on the right hand side) and minimum absolute # values of distances to each pair of parallel arcs. ds1, ds2, ds3, ds4 = numpy.sign(dists_to_arcs).transpose() dists_to_arcs = numpy.abs(dists_to_arcs).reshape(-1, 2, 2).min(axis=-1) jb_dists = numpy.select( # consider four possible relative positions of point and arcs: condlist=[ # signs of distances to both parallel arcs are the same # in both pairs, case "I" on a figure above (ds1 == ds2) & (ds3 == ds4), # sign of distances to two parallels is the same only # in one pair, case "II" ds1 == ds2, # ... or another (case "III") ds3 == ds4 # signs are different in both pairs (this is a "default"), # case "IV" ], choicelist=[ # case "I": closest distance is the closest distance to corners dists_to_corners, # case "II": closest distance is distance to arc "1" or "2", # whichever is closer dists_to_arcs[:, 0], # case "III": closest distance is distance to either # arc "3" or "4" dists_to_arcs[:, 1] ], # default -- case "IV" default=0) return jb_dists.reshape(mesh.lons.shape)
def get_joyner_boore_distance(self, mesh): """ Compute and return Joyner-Boore distance to each point of ``mesh``. Point's depth is ignored. See :meth:`openquake.hazardlib.geo.surface.base.BaseQuadrilateralSurface.get_joyner_boore_distance` for definition of this distance. :returns: numpy array of distances in km of the same shape as ``mesh``. Distance value is considered to be zero if a point lies inside the polygon enveloping the projection of the mesh or on one of its edges. """ # we perform a hybrid calculation (geodetic mesh-to-mesh distance # and distance on the projection plane for close points). first, # we find the closest geodetic distance for each point of target # mesh to this one. in general that distance is greater than # the exact distance to enclosing polygon of this mesh and it # depends on mesh spacing. but the difference can be neglected # if calculated geodetic distance is over some threshold. distances = geodetic.min_geodetic_distance(self.lons, self.lats, mesh.lons.flatten(), mesh.lats.flatten()) # here we find the points for which calculated mesh-to-mesh # distance is below a threshold. this threshold is arbitrary: # lower values increase the maximum possible error, higher # values reduce the efficiency of that filtering. the maximum # error is equal to the maximum difference between a distance # from site to two adjacent points of the mesh and distance # from site to the line connecting them. thus the error is # a function of distance threshold and mesh spacing. the error # is maximum when the site lies on a perpendicular to the line # connecting points of the mesh and that passes the middle # point between them. the error then can be calculated as # ``err = trsh - d = trsh - \sqrt(trsh^2 - (ms/2)^2)``, where # ``trsh`` and ``d`` are distance to mesh points (the one # we found on the previous step) and distance to the line # connecting them (the actual distance) and ``ms`` is mesh # spacing. the threshold of 40 km gives maximum error of 314 # meters for meshes with spacing of 10 km and 5.36 km for # meshes with spacing of 40 km. if mesh spacing is over # ``(trsh / \sqrt(2)) * 2`` then points lying in the middle # of mesh cells (that is inside the polygon) will be filtered # out by the threshold and have positive distance instead of 0. # so for threshold of 40 km mesh spacing should not be more # than 56 km (typical values are 5 to 10 km). [idxs] = (distances < 40).nonzero() if not len(idxs): # no point is close enough, return distances as they are return distances # for all the points that are closer than the threshold we need # to recalculate the distance and set it to zero, if point falls # inside the enclosing polygon of the mesh. for doing that we # project both this mesh and the points of the second mesh--selected # by distance threshold--to the same Cartesian space, define # minimum shapely polygon enclosing the mesh and calculate point # to polygon distance, which gives the most accurate value # of distance in km (and that value is zero for points inside # the polygon). proj, polygon = self._get_proj_enclosing_polygon() if not isinstance(polygon, shapely.geometry.Polygon): # either line or point is our enclosing polygon. draw # a square with side of 10 m around in order to have # a proper polygon instead. polygon = polygon.buffer(self.DIST_TOLERANCE, 1) mesh_lons, mesh_lats = mesh.lons.take(idxs), mesh.lats.take(idxs) mesh_xx, mesh_yy = proj(mesh_lons, mesh_lats) distances_2d = geo_utils.point_to_polygon_distance( polygon, mesh_xx, mesh_yy) # replace geodetic distance values for points-closer-than-the-threshold # by more accurate point-to-polygon distance values. distances.put(idxs, distances_2d) return distances.reshape(mesh.shape)
def get_joyner_boore_distance(self, mesh): """ See :meth:`superclass' method <openquake.hazardlib.geo.surface.base.BaseQuadrilateralSurface.get_joyner_boore_distance>`. This is an optimized version specific to planar surface that doesn't make use of the mesh. """ # we define four great circle arcs that contain four sides # of projected planar surface: # # ↓ II ↓ # I ↓ ↓ I # ↓ + ↓ # →→→→→TL→→→→1→→→→TR→→→→→ → azimuth direction → # ↓ - ↓ # ↓ ↓ # III -3+ IV -4+ III ↓ # ↓ ↓ downdip direction # ↓ + ↓ ↓ # →→→→→BL→→→→2→→→→BR→→→→→ # ↓ - ↓ # I ↓ ↓ I # ↓ II ↓ # # arcs 1 and 2 are directed from left corners to right ones (the # direction has an effect on the sign of the distance to an arc, # as it shown on the figure), arcs 3 and 4 are directed from top # corners to bottom ones. # # then we measure distance from each of the points in a mesh # to each of those arcs and compare signs of distances in order # to find a relative positions of projections of points and # projection of a surface. # # then we consider four special cases (labeled with Roman numerals) # and either pick one of distances to arcs or a closest distance # to corner. # # indices 0, 2 and 1 represent corners TL, BL and TR respectively. arcs_lons = self.corner_lons.take([0, 2, 0, 1]) arcs_lats = self.corner_lats.take([0, 2, 0, 1]) downdip_azimuth = (self.strike + 90) % 360 arcs_azimuths = [self.strike, self.strike, downdip_azimuth, downdip_azimuth] mesh_lons = mesh.lons.reshape((-1, 1)) mesh_lats = mesh.lats.reshape((-1, 1)) # calculate distances from all the target points to all four arcs dists_to_arcs = geodetic.distance_to_arc( arcs_lons, arcs_lats, arcs_azimuths, mesh_lons, mesh_lats ) # ... and distances from all the target points to each of surface's # corners' projections (we might not need all of those but it's # better to do that calculation once for all). dists_to_corners = geodetic.min_geodetic_distance( self.corner_lons, self.corner_lats, mesh.lons.flatten(), mesh.lats.flatten() ) # extract from ``dists_to_arcs`` signs (represent relative positions # of an arc and a point: +1 means on the left hand side, 0 means # on arc and -1 means on the right hand side) and minimum absolute # values of distances to each pair of parallel arcs. ds1, ds2, ds3, ds4 = numpy.sign(dists_to_arcs).transpose() dists_to_arcs = numpy.abs(dists_to_arcs).reshape(-1, 2, 2).min(axis=-1) jb_dists = numpy.select( # consider four possible relative positions of point and arcs: condlist=[ # signs of distances to both parallel arcs are the same # in both pairs, case "I" on a figure above (ds1 == ds2) & (ds3 == ds4), # sign of distances to two parallels is the same only # in one pair, case "II" ds1 == ds2, # ... or another (case "III") ds3 == ds4 # signs are different in both pairs (this is a "default"), # case "IV" ], choicelist=[ # case "I": closest distance is the closest distance to corners dists_to_corners, # case "II": closest distance is distance to arc "1" or "2", # whichever is closer dists_to_arcs[:, 0], # case "III": closest distance is distance to either # arc "3" or "4" dists_to_arcs[:, 1] ], # default -- case "IV" default=0 ) return jb_dists.reshape(mesh.lons.shape)
def get_joyner_boore_distance(self, mesh): """ Compute and return Joyner-Boore distance to each point of ``mesh``. Point's depth is ignored. See :meth:`openquake.hazardlib.geo.surface.base.BaseSurface.get_joyner_boore_distance` for definition of this distance. :returns: numpy array of distances in km of the same shape as ``mesh``. Distance value is considered to be zero if a point lies inside the polygon enveloping the projection of the mesh or on one of its edges. """ # we perform a hybrid calculation (geodetic mesh-to-mesh distance # and distance on the projection plane for close points). first, # we find the closest geodetic distance for each point of target # mesh to this one. in general that distance is greater than # the exact distance to enclosing polygon of this mesh and it # depends on mesh spacing. but the difference can be neglected # if calculated geodetic distance is over some threshold. # get the highest slice from the 3D mesh distances = geodetic.min_geodetic_distance( (self.lons, self.lats), (mesh.lons, mesh.lats)) # here we find the points for which calculated mesh-to-mesh # distance is below a threshold. this threshold is arbitrary: # lower values increase the maximum possible error, higher # values reduce the efficiency of that filtering. the maximum # error is equal to the maximum difference between a distance # from site to two adjacent points of the mesh and distance # from site to the line connecting them. thus the error is # a function of distance threshold and mesh spacing. the error # is maximum when the site lies on a perpendicular to the line # connecting points of the mesh and that passes the middle # point between them. the error then can be calculated as # ``err = trsh - d = trsh - \sqrt(trsh^2 - (ms/2)^2)``, where # ``trsh`` and ``d`` are distance to mesh points (the one # we found on the previous step) and distance to the line # connecting them (the actual distance) and ``ms`` is mesh # spacing. the threshold of 40 km gives maximum error of 314 # meters for meshes with spacing of 10 km and 5.36 km for # meshes with spacing of 40 km. if mesh spacing is over # ``(trsh / \sqrt(2)) * 2`` then points lying in the middle # of mesh cells (that is inside the polygon) will be filtered # out by the threshold and have positive distance instead of 0. # so for threshold of 40 km mesh spacing should not be more # than 56 km (typical values are 5 to 10 km). idxs = (distances < 40).nonzero()[0] # indices on the first dimension if not len(idxs): # no point is close enough, return distances as they are return distances # for all the points that are closer than the threshold we need # to recalculate the distance and set it to zero, if point falls # inside the enclosing polygon of the mesh. for doing that we # project both this mesh and the points of the second mesh--selected # by distance threshold--to the same Cartesian space, define # minimum shapely polygon enclosing the mesh and calculate point # to polygon distance, which gives the most accurate value # of distance in km (and that value is zero for points inside # the polygon). proj, polygon = self._get_proj_enclosing_polygon() if not isinstance(polygon, shapely.geometry.Polygon): # either line or point is our enclosing polygon. draw # a square with side of 10 m around in order to have # a proper polygon instead. polygon = polygon.buffer(self.DIST_TOLERANCE, 1) mesh_xx, mesh_yy = proj(mesh.lons[idxs], mesh.lats[idxs]) # replace geodetic distance values for points-closer-than-the-threshold # by more accurate point-to-polygon distance values. distances[idxs] = geo_utils.point_to_polygon_distance( polygon, mesh_xx, mesh_yy) return distances