Source code for pyrocko.gato.grid.location

# http://pyrocko.org - GPLv3
#
# The Pyrocko Developers, 21st Century
# ---|P------/S----------~Lg----------

import numpy as num

from pyrocko import orthodrome as od
from pyrocko.moment_tensor import euler_to_matrix
from pyrocko.model.location import Location
from pyrocko.guts import Float
from pyrocko.guts_array import Array
from .base import Grid, GridSnap, grid_coordinates
from ..error import GatoError

guts_prefix = 'gato'

d2r = num.pi / 180.
NA = num.newaxis


def distances_3d(grid_a, grid_b):
    a = grid_a.get_nodes('ecef')
    b = grid_b.get_nodes('ecef')
    return num.sqrt(
        num.sum((a[:, NA, :] - b[NA, :, :])**2, 2))


def distances_surface(grid_a, grid_b):
    a = grid_a.get_nodes('latlondepth')
    b = grid_b.get_nodes('latlondepth')

    return od.distance_proj(
        a[:, NA, 0],
        a[:, NA, 1],
        b[NA, :, 0],
        b[NA, :, 1])


def distances_projected(normal_ecef, grid_a, grid_b):
    a = grid_a.get_nodes('ecef')
    b = grid_b.get_nodes('ecef')

    a_proj = a - num.dot(a, normal_ecef)[:, NA] * normal_ecef[NA, :]
    b_proj = b - num.dot(b, normal_ecef)[:, NA] * normal_ecef[NA, :]

    return num.sqrt(
        num.sum((a_proj[:, NA, :] - b_proj[NA, :, :])**2, 2))


[docs]class LocationGrid(Grid): ''' Base class for location grids. '''
[docs]class CartesianLocationGrid(LocationGrid): ''' Regular Cartesian grid anchored at a reference point with optional rotation. Unrotated coordinate system is NED (north-east-down). 3D-indexing is ordered from z (slow dimension) to x (fast dimension) ``f[iz, iy, ix]``. 1D-indexing uses :py:func:`numpy.unravel_index` on ``(nz, ny, nx)``. If any attribute is changed after initialization, :py:meth:`update` must be called. ''' origin = Location.T( help='Anchor point of the grid.') azimuth = Float.T( default=0.0, help='Angle of x against north [deg].') dip = Float.T( default=0.0, help='Angle of y against horizontal [deg], rotation around x') x_min = Float.T( default=0.0, help='X axis minimum [m].') x_max = Float.T( default=0.0, help='X axis maximum [m].') y_min = Float.T( default=0.0, help='Y axis minimum [m].') y_max = Float.T( default=0.0, help='Y axis maximum [m].') z_min = Float.T( default=0.0, help='Z axis minimum [m].') z_max = Float.T( default=0.0, help='Z axis maximum [m].') x_delta = Float.T( default=1.0, help='X axis spacing [m].') y_delta = Float.T( default=1.0, help='Y axis spacing [m].') z_delta = Float.T( default=1.0, help='Z axis spacing [m].') snap = GridSnap.T( default='both', help='Flag to indicate how grid inconsistencies are handled.') def update(self): self.clear_cached() self._x = grid_coordinates( self.x_min, self.x_max, self.x_delta, self.snap) self._y = grid_coordinates( self.y_min, self.y_max, self.y_delta, self.snap) self._z = grid_coordinates( self.z_min, self.z_max, self.z_delta, self.snap) self._nx = self._x.size self._ny = self._y.size self._nz = self._z.size def clear_cached(self): self._xyz = None self._ned = None self._latlondepth = None self._ecef = None def _get_xyz(self): if self._xyz is None: z2, y2, x2 = [v.flatten() for v in num.meshgrid( self._z, self._y, self._x, indexing='ij')] self._xyz = num.vstack([x2, y2, z2]).T return self._xyz def _get_ned(self): if self._ned is None: rotmat = euler_to_matrix(self.dip * d2r, self.azimuth * d2r, 0.0) self._ned = num.dot(rotmat.T, self._get_xyz().T).T # Note: _ned is relative to reference point. return self._ned def _get_latlondepth(self): if self._latlondepth is None: ned = self._get_ned() lats, lons = od.ne_to_latlon_proj( self.origin.lat, self.origin.lon, self.origin.north_shift + ned[:, 0], self.origin.east_shift + ned[:, 1]) depths = self.origin.depth - self.origin.elevation + ned[:, 2] self._latlondepth = num.vstack((lats, lons, depths)).T return self._latlondepth def _get_ecef(self): if self._ecef is None: lat, lon, depth = self._get_latlondepth().T x, y, z = od.geodetic_to_ecef(lat, lon, -depth) self._ecef = num.vstack((x, y, z)).T return self._ecef @property def shape(self): ''' Logical shape of the grid. ''' return (self._nz, self._ny, self._nx) @property def size(self): ''' Number of grid nodes. ''' return self._nz * self._ny * self._nx @property def effective_dimension(self): ''' Number of non-flat dimensions. ''' return sum(int(x > 1) for x in self.shape)
[docs] def get_nodes(self, system): ''' Get node coordinates. :param system: Coordinate system: ``'latlondepth'``, world coordinates as ``(latitude, longitude, depth)``, ``'xyz'``, unrotated coordinate system, ``'ned'``, rotated coordinate system in ``(north, east, down)`` relative to :py:gattr:`origin`. :type system: str :returns: Point coordinates in requested coordinate system. :rtype: :py:class:`numpy.ndarray` of shape ``(size, 3)``, where size is the number of nodes ''' if system == 'latlondepth': return self._get_latlondepth() if system == 'ecef': return self._get_ecef() elif system == 'ned': return self._get_ned() elif system == 'xyz': return self._get_xyz() else: raise GatoError( 'Coordinate system not supported for %s: %s' % ( self.__class__.__name__, system))
[docs]class UnstructuredLocationGrid(LocationGrid): ''' Representation of a set of scattered locations. This class is useful to represent e.g. sensor locations in a seismic array or other collections of locations with irregular structure. ''' origin = Location.T( optional=True, help='Anchor point of the grid.') coordinates = Array.T( help='6C-Coordinates of grid nodes as ``(lat, lon, north_shift, ' 'east_shift, depth, elevation)``', shape=(None, 6), dtype=num.float64, serialize_as='npy') @classmethod def from_locations(cls, locations, ignore_position_duplicates=False): if ignore_position_duplicates: have = set() coordinates = num.zeros((len(locations), 6)) ilocation = 0 for location in locations: position = ( location.lat, location.lon, location.north_shift, location.east_shift, location.depth, location.elevation) if ignore_position_duplicates: if position in have: continue have.add(position) coordinates[ilocation, :] = position ilocation += 1 if ignore_position_duplicates: coordinates = coordinates[:ilocation, :] return cls(coordinates=coordinates) def update(self): self.clear_cached() def clear_cached(self): self._latlondepth = None self._ecef = None self._ned = None @property def shape(self): ''' Logical shape of the grid. ''' return (self.coordinates.shape[0],) @property def size(self): ''' Number of grid nodes. ''' return self.coordinates.shape[0] def set_origin_to_center(self): self.origin = self.get_center() self._ned = None def get_center(self): lld = self._get_latlondepth() lat, lon = od.geographic_midpoint( lats=lld[:, 0], lons=lld[:, 1]) depth = num.mean(self.coordinates[:, 4]) elevation = num.mean(self.coordinates[:, 5]) return Location(lat=lat, lon=lon, depth=depth, elevation=elevation) def get_effective_origin(self): if self.origin is not None: return self.origin else: return self.get_center() def _get_latlondepth(self): if self._latlondepth is None: lats, lons = od.ne_to_latlon_proj( self.coordinates[:, 0], self.coordinates[:, 1], self.coordinates[:, 2], self.coordinates[:, 3]) depths = self.coordinates[:, 4] - self.coordinates[:, 5] self._latlondepth = num.vstack((lats, lons, depths)).T return self._latlondepth def _get_ecef(self): if self._ecef is None: lat, lon, depth = self._get_latlondepth().T x, y, z = od.geodetic_to_ecef(lat, lon, -depth) self._ecef = num.vstack((x, y, z)).T return self._ecef def _get_ned(self): if self._ned is None: lld = self._get_latlondepth() origin = self.get_effective_origin() ns, es = od.latlon_to_ne_proj( origin.lat, origin.lon, lld[:, 0], lld[:, 1]) self._ned = num.vstack( (ns, es, lld[:, 2] + origin.elevation - origin.depth)).T return self._ned
[docs] def get_nodes(self, system): ''' Get node coordinates. :param system: Coordinate system: ``'latlondepth'``, world coordinates as ``(latitude, longitude, depth)``, ``'ned'``, coordinate system in ``(north, east, down)`` relative to :py:gattr:`origin`. :type system: str :returns: Point coordinates in requested coordinate system. :rtype: :py:class:`numpy.ndarray` of shape ``(size, 3)``, where size is the number of nodes ''' if system == 'latlondepth': return self._get_latlondepth() if system == 'ecef': return self._get_ecef() elif system == 'ned': return self._get_ned() else: raise GatoError( 'Coordinate system not supported for %s: %s' % ( self.__class__.__name__, system))
__all__ = [ 'distances_3d', 'distances_surface', 'distances_projected', 'CartesianLocationGrid', 'UnstructuredLocationGrid', ]