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 _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 get_joyner_boore_distance(self, mesh): """ See :meth:`superclass' method <nhlib.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.lons, mesh.lats ) # 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) return 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 )
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:`nhlib.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. 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() 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
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:`nhlib.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. 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() 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