Exemple #1
0
    def __computeD(self, i):
        """Compute d for the i-th quad/segment.

        Y = d/W, where d is the portion (in km) of the width of the fault which
        ruptures up-dip from the hypocenter to the top of the fault.

        :param i:
            index of segment for which d is to be computed.
        """
        hyp_ecef = self.phyp[i]  # already in ECEF
        hyp_col = np.array([[hyp_ecef.x], [hyp_ecef.y], [hyp_ecef.z]])

        # First compute "updip" vector
        P0, P1, P2 = self._rup.getQuadrilaterals()[i][0:3]
        p1 = Vector.fromPoint(P1)  # convert to ECEF
        p2 = Vector.fromPoint(P2)
        e21 = p1 - p2
        e21norm = e21.norm()
        hp1 = p1 - hyp_ecef
        # convert to km (used as max later)
        udip_len = Vector.dot(hp1, e21norm) / 1000.0
        udip_col = np.array([[e21norm.x], [e21norm.y],
                             [e21norm.z]])  # ECEF coords

        # Sites
        slat = self._lat
        slon = self._lon

        # Convert sites to ECEF:
        site_ecef_x = np.ones_like(slat)
        site_ecef_y = np.ones_like(slat)
        site_ecef_z = np.ones_like(slat)

        # Make a 3x(#number of sites) matrix of site locations
        # (rows are x, y, z) in ECEF
        site_ecef_x, site_ecef_y, site_ecef_z = ecef.latlon2ecef(
            slat, slon, np.zeros(slon.shape))
        site_mat = np.array([
            np.reshape(site_ecef_x, (-1, )),
            np.reshape(site_ecef_y, (-1, )),
            np.reshape(site_ecef_z, (-1, ))
        ])

        # Hypocenter-to-site matrix
        h2s_mat = site_mat - hyp_col  # in ECEF

        # Dot hypocenter-to-site with updip vector
        d_raw = np.abs(np.sum(h2s_mat * udip_col, axis=0)) / \
            1000.0  # convert to km
        d_raw = np.reshape(d_raw, self._lat.shape)
        self.d = d_raw.clip(min=1.0, max=udip_len)
Exemple #2
0
    def __computeD(self, i):
        """Compute d for the i-th quad/segment.

        Y = d/W, where d is the portion (in km) of the width of the fault which
        ruptures up-dip from the hypocenter to the top of the fault.

        :param i:
            index of segment for which d is to be computed.
        """
        hyp_ecef = self.phyp[i]  # already in ECEF
        hyp_col = np.array([[hyp_ecef.x], [hyp_ecef.y], [hyp_ecef.z]])

        # First compute "updip" vector
        P0, P1, P2, P3 = self._flt.getQuadrilaterals()[i]
        p1 = Vector.fromPoint(P1)  # convert to ECEF
        p2 = Vector.fromPoint(P2)
        e21 = p1 - p2
        e21norm = e21.norm()
        hp1 = p1 - hyp_ecef
        # convert to km (used as max later)
        udip_len = Vector.dot(hp1, e21norm) / 1000.0
        udip_col = np.array([[e21norm.x], [e21norm.y],
                             [e21norm.z]])  # ECEF coords

        # Sites
        slat = self._lat
        slon = self._lon

        # Convert sites to ECEF:
        site_ecef_x = np.ones_like(slat)
        site_ecef_y = np.ones_like(slat)
        site_ecef_z = np.ones_like(slat)

        # Make a 3x(#number of sites) matrix of site locations
        # (rows are x, y, z) in ECEF
        site_ecef_x, site_ecef_y, site_ecef_z = ecef.latlon2ecef(
            slat, slon, np.zeros(slon.shape))
        site_mat = np.array([
            np.reshape(site_ecef_x, (-1, )),
            np.reshape(site_ecef_y, (-1, )),
            np.reshape(site_ecef_z, (-1, ))
        ])

        # Hypocenter-to-site matrix
        h2s_mat = site_mat - hyp_col  # in ECEF

        # Dot hypocenter-to-site with updip vector
        d_raw = np.abs(np.sum(h2s_mat * udip_col, axis=0)) / \
            1000.0  # convert to km
        d_raw = np.reshape(d_raw, self._lat.shape)
        self.d = d_raw.clip(min=1.0, max=udip_len)
Exemple #3
0
    def __computeWrup(self):
        """
        Compute the the portion (in km) of the width of the fault which ruptures
        up-dip from the hypocenter to the top of the fault.

        Wrup is the portion (in km) of the width of the fault which
        ruptures up-dip from the hypocenter to the top of the fault.

        * This is ambiguous for faults with varible top of rupture (not
          allowed in NGA). For now, lets just compute this for the
          quad where the hypocenter is located.
        * Alternative is to compute max Wrup for the different quads.

        """
        nquad = len(self._flt._quadrilaterals)

        #-------------------------------------------
        # First find which quad the hypocenter is on
        #-------------------------------------------

        x, y, z = latlon2ecef(
            self._hyp.latitude, self._hyp.longitude, self._hyp.depth)
        hyp_ecef = np.array([[x, y, z]])
        qdist = np.zeros(nquad)
        for i in range(0, nquad):
            P0, P1, P2, P3 = self._flt.getQuadrilaterals()[i]
            qdist[i] = _calc_rupture_distance(P0, P1, P2, P3, hyp_ecef)
        ind = int(np.where(qdist == np.min(qdist))[0])
        # *** check that this doesn't break with more than one quad
        q = self._flt.getQuadrilaterals()[ind]

        #--------------------------
        # Compute Wrup on that quad
        #--------------------------

        pp0 = Vector.fromPoint(geo.point.Point(
            q[0].longitude, q[0].latitude, q[0].depth))
        hyp_ecef = Vector.fromPoint(geo.point.Point(
            self._hyp.longitude, self._hyp.latitude, self._hyp.depth))
        hp0 = hyp_ecef - pp0
        ddv = fault.get_quad_down_dip_vector(q)
        self._Wrup = Vector.dot(ddv, hp0) / 1000
Exemple #4
0
def test_ecef():
    print('Testing ECEF conversion code...')
    lat = 32.1
    lon = 118.5
    dep = 10.0

    x, y, z = latlon2ecef(lat, lon, dep)

    TESTX = -2576515.419
    TESTY = 4745351.087
    TESTZ = 3364516.124

    np.testing.assert_almost_equal(x, TESTX, decimal=2)
    np.testing.assert_almost_equal(y, TESTY, decimal=2)
    np.testing.assert_almost_equal(z, TESTZ, decimal=2)

    lat2, lon2, dep2 = ecef2latlon(x, y, z)
    np.testing.assert_almost_equal(lat2, lat, decimal=2)
    np.testing.assert_almost_equal(lon2, lon, decimal=2)
    np.testing.assert_almost_equal(dep2, dep, decimal=2)
    print('Passed tests of ECEF conversion code.')
Exemple #5
0
def test_ecef():
    print('Testing ECEF conversion code...')
    lat = 32.1
    lon = 118.5
    dep = 10.0

    x, y, z = latlon2ecef(lat, lon, dep)

    TESTX = -2576515.419
    TESTY = 4745351.087
    TESTZ = 3364516.124

    np.testing.assert_almost_equal(x, TESTX, decimal=2)
    np.testing.assert_almost_equal(y, TESTY, decimal=2)
    np.testing.assert_almost_equal(z, TESTZ, decimal=2)

    lat2, lon2, dep2 = ecef2latlon(x, y, z)
    np.testing.assert_almost_equal(lat2, lat, decimal=2)
    np.testing.assert_almost_equal(lon2, lon, decimal=2)
    np.testing.assert_almost_equal(dep2, dep, decimal=2)
    print('Passed tests of ECEF conversion code.')
Exemple #6
0
def _get_quad_slip_ds_ss(q, rake, cp, p):
    """
    Compute the DIP SLIP and STRIKE SLIP components of the unit slip vector in
    ECEF coords for a quad and rake angle.
    :param q:
        A quadrilateral.
    :param rake:
        Direction of motion of the hanging wall relative to the
        foot wall, as measured by the angle (deg) from the strike vector.
    :param cp:
        A 3x(n sub fault) array giving center points of each sub fault
        in ECEF coords.
    :param p:
        A 3x(n sub fault) array giving the unit vectors of the propagation
        vector on each sub fault in ECEF coords.
    :returns:
        List of dip slip and strike slip components (each is a matrix)
        of the unit slip vector in ECEF space.
    """
    # Get quad vertices, strike, dip
    P0, P1, P2 = q[0:3]
    strike = P0.azimuth(P1)
    dip = fault.get_quad_dip(q)

    # Slip unit vectors in 'local' (i.e., z-up, x-east) coordinates
    d1_local = fault.get_local_unit_slip_vector_DS(strike, dip, rake)
    s1_local = fault.get_local_unit_slip_vector_SS(strike, dip, rake)

    # Convert to a column array
    d1_col = np.array([[d1_local.x],
                       [d1_local.y],
                       [d1_local.z]])
    s1_col = np.array([[s1_local.x],
                       [s1_local.y],
                       [s1_local.z]])

    # Define 'local' coordinate system
    qlats = [a.latitude for a in q]
    qlons = [a.longitude for a in q]
    proj = get_orthographic_projection(
        np.min(qlons), np.max(qlons), np.min(qlats), np.max(qlats))

    # Convert p and cp to geographic coords
    p0lat, p0lon, p0z = ecef2latlon(cp[0, ], cp[1, ], cp[2, ])
    p1lat, p1lon, p1z = ecef2latlon(cp[0, ] + p[0, ],
                                    cp[1, ] + p[1, ],
                                    cp[2, ] + p[2, ])

    # Convert p to 'local' coords
    p0x, p0y = proj(p0lon, p0lat)
    p1x, p1y = proj(p1lon, p1lat)
    px = p1x - p0x
    py = p1y - p0y
    pz = p1z - p0z

    # Apply sign changes in 'local' coords
    s1mat = np.array([[np.abs(s1_col[0]) * np.sign(px)],
                      [np.abs(s1_col[1]) * np.sign(py)],
                      [np.abs(s1_col[2]) * np.sign(pz)]])
#                      [np.abs(s1_col[2])*np.ones_like(pz)]])

    dipsign = -np.sign(np.sin(np.radians(rake)))
    d1mat = np.array([[d1_col[0] * np.ones_like(px) * dipsign],
                      [d1_col[1] * np.ones_like(py) * dipsign],
                      [d1_col[2] * np.ones_like(pz) * dipsign]])

    # Need to track 'origin'
    s0 = np.array([[0], [0], [0]])

    # Convert from 'local' to geographic coords
    s1_ll = proj(s1mat[0, ], s1mat[1, ], reverse=True)
    d1_ll = proj(d1mat[0, ], d1mat[1, ], reverse=True)
    s0_ll = proj(s0[0], s0[1], reverse=True)

    # And then back to ECEF:
    s1_ecef = latlon2ecef(s1_ll[1], s1_ll[0], s1mat[2, ])
    d1_ecef = latlon2ecef(d1_ll[1], d1_ll[0], d1mat[2, ])
    s0_ecef = latlon2ecef(s0_ll[1], s0_ll[0], s0[2])
    s00 = s0_ecef[0].reshape(-1)
    s01 = s0_ecef[1].reshape(-1)
    s02 = s0_ecef[2].reshape(-1)
    d_mat = np.array([d1_ecef[0].reshape(-1) - s00,
                      d1_ecef[1].reshape(-1) - s01,
                      d1_ecef[2].reshape(-1) - s02])
    s_mat = np.array([s1_ecef[0].reshape(-1) - s00,
                      s1_ecef[1].reshape(-1) - s01,
                      s1_ecef[2].reshape(-1) - s02])
    return d_mat, s_mat
Exemple #7
0
    def __computeXiPrime(self):
        """
        Computes the xi' value.
        """
        hypo_ecef = Vector.fromPoint(geo.point.Point(
            self._hyp.longitude, self._hyp.latitude, self._hyp.depth))
        epi_ll = Vector(self._hyp.longitude, self._hyp.latitude, 0)
        epi_ecef = Vector.fromPoint(geo.point.Point(
            epi_ll.x, epi_ll.y, 0))

        slat = self._lat
        slon = self._lon

        # Convert site to ECEF:
        site_ecef_x = np.ones_like(slat)
        site_ecef_y = np.ones_like(slat)
        site_ecef_z = np.ones_like(slat)

        # Make a 3x(#number of sites) matrix of site locations
        # (rows are x, y, z) in ECEF
        site_ecef_x, site_ecef_y, site_ecef_z = latlon2ecef(
            slat, slon, np.zeros(slon.shape))
        site_mat = np.array([np.reshape(site_ecef_x, (-1,)),
                             np.reshape(site_ecef_y, (-1,)),
                             np.reshape(site_ecef_z, (-1,))])

        xi_prime_unscaled = np.zeros_like(slat)

        # Normalize by total number of subfaults. For mtype == 1, the number
        # of subfaults will vary with site and be different for xi_s and
        # xi_p, so keep two variables and sum them for each quad.
        nsubs = np.zeros(np.product(slat.shape))
        nsubp = np.zeros(np.product(slat.shape))

        xi_prime_s = np.zeros(np.product(slat.shape))
        xi_prime_p = np.zeros(np.product(slat.shape))

        for k in range(len(self._flt._quadrilaterals)):
            # Select a quad
            q = self._flt.getQuadrilaterals()[k]

            # Quad mesh (ECEF coords)
            mesh = fault.get_quad_mesh(q, self._dx)

            # Rupture plane normal vector (ECEF coords)
            rpnv = fault.get_quad_normal(q)
            rpnvcol = np.array([[rpnv.x],
                                [rpnv.y],
                                [rpnv.z]])

            # Strike vector (ECEF coords)
            strike_vec = fault.get_quad_strike_vector(q)
            strike_vec_col = np.array([[strike_vec.x], [strike_vec.y], [
                                      strike_vec.z]])  # convert to column vector

            # Make 3x(i*j) matrix of cp
            ni, nj = mesh['llx'].shape

            cp_mat = np.array([np.reshape(mesh['cpx'], (-1,)),
                               np.reshape(mesh['cpy'], (-1,)),
                               np.reshape(mesh['cpz'], (-1,))])

            # Compute matrix of p vectors
            hypcol = np.array([[hypo_ecef.x],
                               [hypo_ecef.y],
                               [hypo_ecef.z]])
            pmat = cp_mat - hypcol

            # Project pmat onto quad
            ndotp = np.sum(pmat * rpnvcol, axis=0)
            pmat = pmat - ndotp * rpnvcol

            mag = np.sqrt(np.sum(pmat * pmat, axis=0))
            pmatnorm = pmat / mag  # like r1

            # According to Rowshandel:
            #   "The choice of the +/- sign in the above equations
            #    depends on the (along-the-strike and across-the-dip)
            #    location of the rupturing sub-fault relative to the
            #    location of the hypocenter."
            # and:
            #   "for the along the strike component of the slip unit
            #    vector, the choice of the sign should result in the
            #    slip unit vector (s) being exactly the same as  the
            #    rupture unit vector (p) for a pure strike-slip case"

            # Strike slip and dip slip components of unit slip vector
            # (ECEF coords)
            ds_mat, ss_mat = _get_quad_slip_ds_ss(
                q, self._rake, cp_mat, pmatnorm)

            slpmat = (ds_mat + ss_mat)
            mag = np.sqrt(np.sum(slpmat * slpmat, axis=0))
            slpmatnorm = slpmat / mag

            # Loop over sites
            for i in range(site_mat.shape[1]):
                sitecol = np.array([[site_mat[0, i]],
                                    [site_mat[1, i]],
                                    [site_mat[2, i]]])

                qmat = sitecol - cp_mat  # 3x(ni*nj), like r2
                mag = np.sqrt(np.sum(qmat * qmat, axis=0))
                qmatnorm = qmat / mag

                # Propagation dot product
                pdotqraw = np.sum(pmatnorm * qmatnorm, axis=0)

                # Slip vector dot product
                sdotqraw = np.sum(slpmatnorm * qmatnorm, axis=0)

                if self._mtype == 1:
                    # Only sum over (+) directivity effect subfaults

                    # xi_p_prime
                    pdotq = pdotqraw.clip(min=0)
                    nsubp[i] = nsubp[i] + np.sum(pdotq > 0)

                    # xi_s_prime
                    sdotq = sdotqraw.clip(min=0)
                    nsubs[i] = nsubs[i] + np.sum(sdotq > 0)

                elif self._mtype == 2:
                    # Sum over contributing subfaults

                    # xi_p_prime
                    pdotq = pdotqraw
                    nsubp[i] = nsubp[i] + cp_mat.shape[1]

                    # xi_s_prime
                    sdotq = sdotqraw
                    nsubs[i] = nsubs[i] + cp_mat.shape[1]

                # Normalize by n sub faults later
                xi_prime_s[i] = xi_prime_s[i] + np.sum(sdotq)
                xi_prime_p[i] = xi_prime_p[i] + np.sum(pdotq)

        # Apply a water level to nsubp and nsubs to avoid division by
        # zero. This should only occur when the numerator is also zero
        # and so the resulting value should be zero.
        nsubs = np.maximum(nsubs, 1)
        nsubp = np.maximum(nsubp, 1)

        # We are outside the 'k' loop over nquads.
        # o Normalize xi_prime_s and xi_prime_p
        # o Reshape them
        # o Add them together with the 'a' weights
        xi_prime_tmp = (self._a_weight) * (xi_prime_s / nsubs) + \
                       (1 - self._a_weight) * (xi_prime_p / nsubp)
        xi_prime_unscaled = xi_prime_unscaled + \
            np.reshape(xi_prime_tmp, slat.shape)

        # Scale so that xi_prime has range (0, 1)
        if self._mtype == 1:
            xi_prime = xi_prime_unscaled
        elif self._mtype == 2:
            xi_prime = 0.5 * (xi_prime_unscaled + 1)

        self._xi_prime = xi_prime
Exemple #8
0
    def __computeThetaAndS(self, i):
        """
        :param i:
            Compute d for the i-th quad/segment.
        """
        # self.phyp is in ECEF
        tmp = ecef.ecef2latlon(self.phyp[i].x, self.phyp[i].y, self.phyp[i].z)
        epi_ecef = Vector.fromPoint(geo.point.Point(tmp[1], tmp[0], 0.0))
        epi_col = np.array([[epi_ecef.x], [epi_ecef.y], [epi_ecef.z]])

        # First compute along strike vector
        P0, P1, P2, P3 = self._rup.getQuadrilaterals()[i]
        p0 = Vector.fromPoint(P0)  # convert to ECEF
        p1 = Vector.fromPoint(P1)
        e01 = p1 - p0
        e01norm = e01.norm()
        hp0 = p0 - epi_ecef
        hp1 = p1 - epi_ecef
        strike_min = Vector.dot(hp0, e01norm) / 1000.0  # convert to km
        strike_max = Vector.dot(hp1, e01norm) / 1000.0  # convert to km
        strike_col = np.array([[e01norm.x], [e01norm.y],
                               [e01norm.z]])  # ECEF coords

        # Sites
        slat = self._lat
        slon = self._lon

        # Convert sites to ECEF:
        site_ecef_x = np.ones_like(slat)
        site_ecef_y = np.ones_like(slat)
        site_ecef_z = np.ones_like(slat)

        # Make a 3x(#number of sites) matrix of site locations
        # (rows are x, y, z) in ECEF
        site_ecef_x, site_ecef_y, site_ecef_z = ecef.latlon2ecef(
            slat, slon, np.zeros(slon.shape))
        site_mat = np.array([
            np.reshape(site_ecef_x, (-1, )),
            np.reshape(site_ecef_y, (-1, )),
            np.reshape(site_ecef_z, (-1, ))
        ])

        # Epicenter-to-site matrix
        e2s_mat = site_mat - epi_col  # in ECEF
        mag = np.sqrt(np.sum(e2s_mat * e2s_mat, axis=0))

        # Avoid division by zero
        mag[mag == 0] = 1e-12
        e2s_norm = e2s_mat / mag

        # Dot epicenter-to-site with along-strike vector
        s_raw = np.sum(e2s_mat * strike_col, axis=0) / 1000.0  # conver to km

        # Put back into a 2d array
        s_raw = np.reshape(s_raw, self._lat.shape)
        self.s = np.abs(s_raw.clip(min=strike_min,
                                   max=strike_max)).clip(min=np.exp(1))

        # Compute theta
        sdots = np.sum(e2s_norm * strike_col, axis=0)
        theta_raw = np.arccos(sdots)

        # But theta is defined to be the reference angle
        # (i.e., the equivalent angle between 0 and 90 deg)
        sintheta = np.abs(np.sin(theta_raw))
        costheta = np.abs(np.cos(theta_raw))
        theta = np.arctan2(sintheta, costheta)
        self.theta = np.reshape(theta, self._lat.shape)
Exemple #9
0
def get_distance(methods, lat, lon, dep, source, use_median_distance=True):
    """
    Calculate distance using any one of a number of distance measures.
    One of quadlist OR hypo must be specified. The following table gives
    the allowed distance strings and a description of each. 

    +--------+----------------------------------------------------------+
    | String | Description                                              |
    +========+==========================================================+
    | repi   | Distance to epicenter.                                   |
    +--------+----------------------------------------------------------+
    | rhypo  | Distance to hypocenter.                                  |
    +--------+----------------------------------------------------------+
    | rjb    | Joyner-Boore distance; this is closest distance to the   |
    |        | surface projection of the rupture plane.                 |
    +--------+----------------------------------------------------------+
    | rrup   | Rupture distance; closest distance to the rupture plane. |
    +--------+----------------------------------------------------------+
    | rx     | Strike-normal distance; same as GC2 coordiante T.        |
    +--------+----------------------------------------------------------+
    | ry     | Strike-parallel distance; same as GC2 coordiante U, but  |
    |        | with a shift in origin definition. See Spudich and Chiou |
    |        | (2015) http://dx.doi.org/10.3133/ofr20151028.            |
    +--------+----------------------------------------------------------+
    | ry0    | Horizontal distance off the end of the rupture measured  |
    |        | parallel to strike. Can only be zero or positive. We     |
    |        | compute this as a function of GC2 coordinate U.          |
    +--------+----------------------------------------------------------+
    | U      | GC2 coordinate U.                                        |
    +--------+----------------------------------------------------------+
    | T      | GC2 coordinate T.                                        |
    +--------+----------------------------------------------------------+

    :param methods:
        List of strings (or just a string) of distances to compute.
    :param lat:
       A numpy array of latitudes.
    :param lon:
       A numpy array of longidues.
    :param dep:
       A numpy array of depths (km).
    :param source:
       source instance.
    :param use_median_distance:
        Boolean; only used if GMPE requests fault distances and not fault is
        availalbe. Default is True, meaning that point-source distances are
        adjusted based on magnitude to get the median fault distance.
    :returns:
       dictionary of numpy array of distances, size of lon.shape
    """
    fault = source.getFault()
    hypo = source.getHypo()
    if fault is not None:
        quadlist = fault.getQuadrilaterals()
    else:
        quadlist = None

    # Dictionary for holding the distances
    distdict = dict()

    if not isinstance(methods, list):
        methods = [methods]

    methods_available = set(
        ['repi', 'rhypo', 'rjb', 'rrup', 'rx', 'ry', 'ry0', 'U', 'T'])
    if not set(methods).issubset(methods_available):
        raise NotImplementedError(
            'One or more requested distance method is not '
            'valid or is not implemented yet')

    if (lat.shape == lon.shape) and (lat.shape == dep.shape):
        pass
    else:
        raise ShakeMapException('lat, lon, and dep must have the same shape.')

    oldshape = lon.shape

    if len(oldshape) == 2:
        newshape = (oldshape[0] * oldshape[1], 1)
    else:
        newshape = (oldshape[0], 1)

    if ('rrup' in methods) or ('rjb' in methods):
        x, y, z = latlon2ecef(lat, lon, dep)
        x.shape = newshape
        y.shape = newshape
        z.shape = newshape
        sites_ecef = np.hstack((x, y, z))

    # Define a projection that spands sites and fault
    if fault is None:
        all_lat = lat
        all_lon = lon
    else:
        all_lat = np.append(lat, fault.getLats())
        all_lon = np.append(lon, fault.getLons())

    west = np.nanmin(all_lon)
    east = np.nanmax(all_lon)
    south = np.nanmin(all_lat)
    north = np.nanmax(all_lat)
    proj = get_orthographic_projection(west, east, north, south)

    # ---------------------------------------------
    # Distances that do not require loop over quads
    # ---------------------------------------------

    if ('repi' in methods) or \
       (('rjb' in methods) and (quadlist is None)) or \
       (('rrup' in methods) and (quadlist is None)) or \
       (('ry0' in methods) and (quadlist is None)) or \
       (('rx' in methods) and (quadlist is None)) or \
       (('T' in methods) and (quadlist is None)) or \
       (('U' in methods) and (quadlist is None)):
        if hypo is None:
            raise ShakeMapException('Cannot calculate epicentral distance '
                                    'without a point object')
        repidist = geodetic.distance(hypo.longitude, hypo.latitude, 0.0, lon,
                                     lat, dep)
        repidist = repidist.reshape(oldshape)
        distdict['repi'] = repidist

    if ('rhypo' in methods) or \
       (('rrup' in methods) and (quadlist is None)):
        if hypo is None:
            raise ShakeMapException('Cannot calculate epicentral distance '
                                    'without a point object')
        rhypodist = geodetic.distance(hypo.longitude, hypo.latitude,
                                      hypo.depth, lon, lat, dep)
        rhypodist = rhypodist.reshape(oldshape)
        distdict['rhypo'] = rhypodist

    # --------------------------------------------------------
    # Loop over quadlist for those distances that require loop
    # --------------------------------------------------------
    if 'rrup' in methods:
        minrrup = np.ones(newshape, dtype=lon.dtype) * 1e16
    if 'rjb' in methods:
        minrjb = np.ones(newshape, dtype=lon.dtype) * 1e16
    if ('rx' in methods) or ('ry' in methods) or \
       ('ry0' in methods) or ('U' in methods) or ('T' in methods):
        totweight = np.zeros(newshape, dtype=lon.dtype)
        GC2T = np.zeros(newshape, dtype=lon.dtype)
        GC2U = np.zeros(newshape, dtype=lon.dtype)

        if quadlist is not None:
            #-----------------------------------------------------------------
            # For these distances, we need to sort out strike discordance and
            # nominal strike prior to starting the loop if there is more than
            # one segment.
            #-----------------------------------------------------------------

            segind = fault._getSegmentIndex()
            segindnp = np.array(segind)
            uind = np.unique(segind)
            nseg = len(uind)

            #-------------------------------------------------------------------
            # The first thing we need to worry about is finding the coordinate
            # shift. U's origin is " selected from the two endpoints most
            # distant from each other."
            #-------------------------------------------------------------------

            if nseg > 1:
                # Need to get index of first and last quad
                # for each segment
                iq0 = np.zeros(nseg, dtype='int16')
                iq1 = np.zeros(nseg, dtype='int16')
                for k in uind:
                    ii = [i for i, j in enumerate(segind) if j == uind[k]]
                    iq0[k] = int(np.min(ii))
                    iq1[k] = int(np.max(ii))

                #---------------------------------------------------------------
                # This is an iterator for each possible combination of segments
                # including segment orientations (i.e., flipped).
                #---------------------------------------------------------------

                it_seg = it.product(it.combinations(uind, 2),
                                    it.product([0, 1], [0, 1]))

                # Placeholder for the segment pair/orientation that gives the
                # largest distance.
                dist_save = 0

                for k in it_seg:
                    s0ind = k[0][0]
                    s1ind = k[0][1]
                    p0ind = k[1][0]
                    p1ind = k[1][1]
                    if p0ind == 0:
                        P0 = quadlist[iq0[s0ind]][0]
                    else:
                        P0 = quadlist[iq1[s0ind]][1]
                    if p1ind == 0:
                        P1 = quadlist[iq1[s1ind]][0]
                    else:
                        P1 = quadlist[iq0[s1ind]][1]

                    dist = geodetic.distance(P0.longitude, P0.latitude, 0.0,
                                             P1.longitude, P1.latitude, 0.0)
                    if dist > dist_save:
                        dist_save = dist
                        A0 = P0
                        A1 = P1

                #---------------------------------------------------------------
                # A0 and A1 are the furthest two segment endpoints, but we still
                # need to sort out which one is the "origin".
                #---------------------------------------------------------------

                # Primate fixes the trend of the trial a vector.
                primate = -1
                while primate < 0:
                    A0.depth = 0
                    A1.depth = 0
                    p_origin = Vector.fromPoint(A0)
                    a0 = Vector.fromPoint(A0)
                    a1 = Vector.fromPoint(A1)
                    ahat = (a1 - a0).norm()

                    # Loop over traces
                    e_j = np.zeros(nseg)
                    b_prime = [None] * nseg
                    for j in range(nseg):
                        P0 = quadlist[iq0[j]][0]
                        P1 = quadlist[iq1[j]][1]
                        P0.depth = 0
                        P1.depth = 0
                        p0 = Vector.fromPoint(P0)
                        p1 = Vector.fromPoint(P1)
                        b_prime[j] = p1 - p0
                        e_j[j] = ahat.dot(b_prime[j])
                    E = np.sum(e_j)

                    # List of discordancy
                    dc = [np.sign(a) * np.sign(E) for a in e_j]
                    b = Vector(0, 0, 0)
                    for j in range(nseg):
                        b.x = b.x + b_prime[j].x * dc[j]
                        b.y = b.y + b_prime[j].y * dc[j]
                        b.z = b.z + b_prime[j].z * dc[j]
                    bhat = b.norm()
                    primate = bhat.dot(ahat)
                    if primate < 0:
                        tmpA0 = copy.copy(A0)
                        tmpA1 = copy.copy(A1)
                        A0 = tmpA1
                        A1 = tmpA0

    if quadlist is not None:
        # Length of prior segments
        s_i = 0.0
        l_i = np.zeros(len(quadlist))
        for i in range(len(quadlist)):
            P0, P1, P2, P3 = quadlist[i]

            if 'rrup' in methods:
                rrupdist = _calc_rupture_distance(P0, P1, P2, P3, sites_ecef)
                minrrup = np.minimum(minrrup, rrupdist)

            if 'rjb' in methods:
                S0 = copy.deepcopy(P0)
                S1 = copy.deepcopy(P1)
                S2 = copy.deepcopy(P2)
                S3 = copy.deepcopy(P3)
                S0.depth = 0.0
                S1.depth = 0.0
                S2.depth = 0.0
                S3.depth = 0.0
                rjbdist = _calc_rupture_distance(S0, S1, S2, S3, sites_ecef)
                minrjb = np.minimum(minrjb, rjbdist)

            if ('rx' in methods) or ('ry' in methods) or \
               ('ry0' in methods) or ('U' in methods) or ('T' in methods):
                # Rx, Ry, and Ry0 are all computed if one is requested since
                # they all require similar information for the weights. This
                # isn't necessary for a single segment fault though.
                # Note, we are basing these calculations on GC2 coordinates U
                # and T as described in:
                # Spudich and Chiou (2015)
                # http://dx.doi.org/10.3133/ofr20151028.

                # Compute u_i and t_i for this segment
                t_i = __calc_t_i(P0, P1, lat, lon, proj)
                u_i = __calc_u_i(P0, P1, lat, lon, proj)

                # Quad length
                l_i[i] = get_quad_length(quadlist[i])

                # Weight of segment, three cases
                # Case 3: t_i == 0 and 0 <= u_i <= l_i
                w_i = np.zeros_like(t_i)
                # Case 1:
                ix = t_i != 0
                w_i[ix] = (1.0 / t_i[ix]) * (np.arctan(
                    (l_i[i] - u_i[ix]) / t_i[ix]) -
                                             np.arctan(-u_i[ix] / t_i[ix]))
                # Case 2:
                ix = (t_i == 0) & ((u_i < 0) | (u_i > l_i[i]))
                w_i[ix] = 1 / (u_i[ix] - l_i[i]) - 1 / u_i[ix]

                totweight = totweight + w_i
                GC2T = GC2T + w_i * t_i
                if nseg == 1:
                    GC2U = GC2U + w_i * (u_i + s_i)
                else:
                    if i == 0:
                        qind = np.array(range(len(quadlist)))
                        l_kj = 0
                        s_ij_1 = 0
                    else:
                        l_kj = l_i[(segindnp == segindnp[i]) & (qind < i)]
                        s_ij_1 = np.sum(l_kj)

                    p1 = Vector.fromPoint(quadlist[iq0[segind[i]]][0])
                    s_ij_2 = (
                        (p1 - p_origin) * dc[segind[i]]).dot(ahat) / 1000.0
                    # This is implemented with GC2N, for GC2T use:
                    #                    s_ij_2 = (p1 - p_origin).dot(bhat) / 1000.0

                    s_ij = s_ij_1 + s_ij_2
                    GC2U = GC2U + w_i * (u_i + s_ij)
                s_i = s_i + l_i[i]

        # Collect distances from loop into the distance dict
        if 'rjb' in methods:
            minrjb = minrjb.reshape(oldshape)
            distdict['rjb'] = minrjb

        if ('rx' in methods) or ('ry' in methods) or \
           ('ry0' in methods) or ('U' in methods) or ('T' in methods):
            # Normalize by sum of quad weights
            GC2T = GC2T / totweight
            GC2U = GC2U / totweight
            distdict['T'] = copy.deepcopy(GC2T).reshape(oldshape)
            distdict['U'] = copy.deepcopy(GC2U).reshape(oldshape)

            # Take care of Rx
            Rx = copy.deepcopy(GC2T)  # preserve sign (no absolute value)
            Rx = Rx.reshape(oldshape)
            distdict['rx'] = Rx

            # Ry
            Ry = GC2U - s_i / 2.0
            Ry = Ry.reshape(oldshape)
            distdict['ry'] = Ry

            # Ry0
            Ry0 = np.zeros_like(GC2U)
            ix = GC2U < 0
            Ry0[ix] = np.abs(GC2U[ix])
            if nseg > 1:
                s_i = s_ij + l_i[-1]
            ix = GC2U > s_i
            Ry0[ix] = GC2U[ix] - s_i
            Ry0 = Ry0.reshape(oldshape)
            distdict['ry0'] = Ry0

        if 'rrup' in methods:
            minrrup = minrrup.reshape(oldshape)
            distdict['rrup'] = minrrup

    else:
        if 'rjb' in methods:
            if use_median_distance:
                warnings.warn(
                    'No fault; Replacing rjb with median rjb given M and repi.'
                )
                cdir, tmp = os.path.split(__file__)

                # -------------------
                # Sort out file names
                # -------------------
                mech = source.getEventParam('mech')
                if not hasattr(source, '_tectonic_region'):
                    rf = os.path.join(
                        cdir, "data", "ps2ff",
                        "Rjb_WC94_mechA_ar1p0_seis0_20_Ratios.csv")
                    vf = os.path.join(cdir, "data", "ps2ff",
                                      "Rjb_WC94_mechA_ar1p0_seis0_20_Var.csv")
                elif source._tectonic_region == 'Active Shallow Crust':
                    if mech == 'ALL':
                        rf = os.path.join(
                            cdir, "data", "ps2ff",
                            "Rjb_WC94_mechA_ar1p7_seis0_20_Ratios.csv")
                        vf = os.path.join(
                            cdir, "data", "ps2ff",
                            "Rjb_WC94_mechA_ar1p7_seis0_20_Var.csv")
                    elif mech == 'RS':
                        rf = os.path.join(
                            cdir, "data", "ps2ff",
                            "Rjb_WC94_mechR_ar1p7_seis0_20_Ratios.csv")
                        vf = os.path.join(
                            cdir, "data", "ps2ff",
                            "Rjb_WC94_mechR_ar1p7_seis0_20_Var.csv")
                    elif mech == 'NM':
                        rf = os.path.join(
                            cdir, "data", "ps2ff",
                            "Rjb_WC94_mechN_ar1p7_seis0_20_Ratios.csv")
                        vf = os.path.join(
                            cdir, "data", "ps2ff",
                            "Rjb_WC94_mechN_ar1p7_seis0_20_Var.csv")
                    elif mech == 'SS':
                        rf = os.path.join(
                            cdir, "data", "ps2ff",
                            "Rjb_WC94_mechSS_ar1p7_seis0_20_Ratios.csv")
                        vf = os.path.join(
                            cdir, "data", "ps2ff",
                            "Rjb_WC94_mechSS_ar1p7_seis0_20_Var.csv")
                elif source._tectonic_region == 'Stable Shallow Crust':
                    if mech == 'ALL':
                        rf = os.path.join(
                            cdir, "data", "ps2ff",
                            "Rjb_S14_mechA_ar1p0_seis0_15_Ratios.csv")
                        vf = os.path.join(
                            cdir, "data", "ps2ff",
                            "Rjb_S14_mechA_ar1p0_seis0_15_Var.csv")
                    elif mech == 'RS':
                        rf = os.path.join(
                            cdir, "data", "ps2ff",
                            "Rjb_S14_mechR_ar1p0_seis0_15_Ratios.csv")
                        vf = os.path.join(
                            cdir, "data", "ps2ff",
                            "Rjb_S14_mechR_ar1p0_seis0_15_Var.csv")
                    elif mech == 'NM':
                        rf = os.path.join(
                            cdir, "data", "ps2ff",
                            "Rjb_S14_mechN_ar1p0_seis0_15_Ratios.csv")
                        vf = os.path.join(
                            cdir, "data", "ps2ff",
                            "Rjb_S14_mechN_ar1p0_seis0_15_Var.csv")
                    elif mech == 'SS':
                        rf = os.path.join(
                            cdir, "data", "ps2ff",
                            "Rjb_S14_mechSS_ar1p0_seis0_15_Ratios.csv")
                        vf = os.path.join(
                            cdir, "data", "ps2ff",
                            "Rjb_S14_mechSS_ar1p0_seis0_15_Var.csv")
                else:
                    warnings.warn(
                        'Unsupported tectonic region; using coefficients for unknown'
                        'tectonic region.')
                    rf = os.path.join(
                        cdir, "data", "ps2ff",
                        "Rjb_WC94_mechA_ar1p0_seis0_20_Ratios.csv")
                    vf = os.path.join(cdir, "data", "ps2ff",
                                      "Rjb_WC94_mechA_ar1p0_seis0_20_Var.csv")

                # -----------------
                # Start with ratios
                # -----------------
                repi2rjb_ratios_tbl = pd.read_csv(rf, comment='#')
                r2rrt_cols = repi2rjb_ratios_tbl.columns[1:]
                mag_list = []
                for column in (r2rrt_cols):
                    if re.search('R\d+\.*\d*', column):
                        magnitude = float(
                            re.findall('R(\d+\.*\d*)', column)[0])
                        mag_list.append(magnitude)
                mag_list = np.array(mag_list)
                dist_list = np.log(np.array(repi2rjb_ratios_tbl['Repi_km']))
                repi2rjb_grid = repi2rjb_ratios_tbl.values[:, 1:]
                repi2rjb_obj = spint.RectBivariateSpline(dist_list,
                                                         mag_list,
                                                         repi2rjb_grid,
                                                         kx=1,
                                                         ky=1)

                def repi2rjb_tbl(repi, M):
                    ratio = repi2rjb_obj.ev(np.log(repi), M)
                    rjb = repi * ratio
                    return rjb

                repis = distdict['repi']
                mags = np.ones_like(repis) * source.getEventParam('mag')
                rjb_hat = repi2rjb_tbl(repis, mags)
                distdict['rjb'] = rjb_hat
                # -------------------
                # Additional Variance
                # -------------------
                repi2rjbvar_ratios_tbl = pd.read_csv(vf, comment='#')
                repi2rjbvar_grid = repi2rjbvar_ratios_tbl.values[:, 1:]
                repi2rjbvar_obj = spint.RectBivariateSpline(dist_list,
                                                            mag_list,
                                                            repi2rjbvar_grid,
                                                            kx=1,
                                                            ky=1)
                rjbvar = repi2rjbvar_obj.ev(np.log(repis), mags)
                distdict['rjbvar'] = rjbvar
            else:
                warnings.warn('No fault; Replacing rjb with repi')
                distdict['rjb'] = distdict['repi']
        if 'rrup' in methods:
            if use_median_distance:
                warnings.warn(
                    'No fault; Replacing rrup with median rrup given M and repi.'
                )
                cdir, tmp = os.path.split(__file__)

                # -------------------
                # Sort out file names
                # -------------------
                rake = source._event_dict.get('rake')
                mech = rake_to_mech(rake)
                if not hasattr(source, '_tectonic_region'):
                    rf = os.path.join(
                        cdir, "data", "ps2ff",
                        "Rrup_WC94_mechA_ar1p0_seis0-20_Ratios.csv")
                    vf = os.path.join(
                        cdir, "data", "ps2ff",
                        "Rrup_WC94_mechA_ar1p0_seis0-20_Var.csv")
                elif source._tectonic_region == 'Active Shallow Crust':
                    if mech == 'ALL':
                        rf = os.path.join(
                            cdir, "data", "ps2ff",
                            "Rrup_WC94_mechA_ar1p7_seis0-20_Ratios.csv")
                        vf = os.path.join(
                            cdir, "data", "ps2ff",
                            "Rrup_WC94_mechA_ar1p7_seis0-20_Var.csv")
                    elif mech == 'RS':
                        rf = os.path.join(
                            cdir, "data", "ps2ff",
                            "Rrup_WC94_mechR_ar1p7_seis0-20_Ratios.csv")
                        vf = os.path.join(
                            cdir, "data", "ps2ff",
                            "Rrup_WC94_mechR_ar1p7_seis0-20_Var.csv")
                    elif mech == 'NM':
                        rf = os.path.join(
                            cdir, "data", "ps2ff",
                            "Rrup_WC94_mechN_ar1p7_seis0-20_Ratios.csv")
                        vf = os.path.join(
                            cdir, "data", "ps2ff",
                            "Rrup_WC94_mechN_ar1p7_seis0-20_Var.csv")
                    elif mech == 'SS':
                        rf = os.path.join(
                            cdir, "data", "ps2ff",
                            "Rrup_WC94_mechSS_ar1p7_seis0-20_Ratios.csv")
                        vf = os.path.join(
                            cdir, "data", "ps2ff",
                            "Rrup_WC94_mechSS_ar1p7_seis0-20_Var.csv")
                elif source._tectonic_region == 'Stable Shallow Crust':
                    if mech == 'ALL':
                        rf = os.path.join(
                            cdir, "data", "ps2ff",
                            "Rrup_S14_mechA_ar1p0_seis0-15_Ratios.csv")
                        vf = os.path.join(
                            cdir, "data", "ps2ff",
                            "Rrup_S14_mechA_ar1p0_seis0-15_Var.csv")
                    elif mech == 'RS':
                        rf = os.path.join(
                            cdir, "data", "ps2ff",
                            "Rrup_S14_mechR_ar1p0_seis0-15_Ratios.csv")
                        vf = os.path.join(
                            cdir, "data", "ps2ff",
                            "Rrup_S14_mechR_ar1p0_seis0-15_Var.csv")
                    elif mech == 'NM':
                        rf = os.path.join(
                            cdir, "data", "ps2ff",
                            "Rrup_S14_mechN_ar1p0_seis0-15_Ratios.csv")
                        vf = os.path.join(
                            cdir, "data", "ps2ff",
                            "Rrup_S14_mechN_ar1p0_seis0-15_Var.csv")
                    elif mech == 'SS':
                        rf = os.path.join(
                            cdir, "data", "ps2ff",
                            "Rrup_S14_mechSS_ar1p0_seis0-15_Ratios.csv")
                        vf = os.path.join(
                            cdir, "data", "ps2ff",
                            "Rrup_S14_mechSS_ar1p0_seis0-15_Var.csv")
                else:
                    warnings.warn(
                        'Unsupported tectonic region; using coefficients for unknown'
                        'tectonic region.')
                    rf = os.path.join(
                        cdir, "data", "ps2ff",
                        "Rrup_WC94_mechA_ar1p0_seis0-20_Ratios.csv")
                    vf = os.path.join(
                        cdir, "data", "ps2ff",
                        "Rrup_WC94_mechA_ar1p0_seis0-20_Var.csv")

                # -----------------
                # Start with ratios
                # -----------------
                repi2rrup_ratios_tbl = pd.read_csv(rf, comment='#')
                r2rrt_cols = repi2rrup_ratios_tbl.columns[1:]
                mag_list = []
                for column in (r2rrt_cols):
                    if re.search('R\d+\.*\d*', column):
                        magnitude = float(
                            re.findall('R(\d+\.*\d*)', column)[0])
                        mag_list.append(magnitude)
                mag_list = np.array(mag_list)
                dist_list = np.log(np.array(repi2rrup_ratios_tbl['Repi_km']))
                repi2rrup_grid = repi2rrup_ratios_tbl.values[:, 1:]
                repi2rrup_obj = spint.RectBivariateSpline(dist_list,
                                                          mag_list,
                                                          repi2rrup_grid,
                                                          kx=1,
                                                          ky=1)

                def repi2rrup_tbl(repi, M):
                    ratio = repi2rrup_obj.ev(np.log(repi), M)
                    rrup = repi * ratio
                    return rrup

                repis = distdict['repi']
                mags = np.ones_like(repis) * source.getEventParam('mag')
                rrup_hat = repi2rrup_tbl(repis, mags)
                distdict['rrup'] = rrup_hat

                # -------------------
                # Additional Variance
                # -------------------
                repi2rrupvar_ratios_tbl = pd.read_csv(vf, comment='#')
                repi2rrupvar_grid = repi2rrupvar_ratios_tbl.values[:, 1:]
                repi2rrupvar_obj = spint.RectBivariateSpline(dist_list,
                                                             mag_list,
                                                             repi2rrupvar_grid,
                                                             kx=1,
                                                             ky=1)
                rrupvar = repi2rrupvar_obj.ev(np.log(repis), mags)
                distdict['rrupvar'] = rrupvar
            else:
                warnings.warn('No fault; Replacing rrup with rhypo')
                distdict['rrup'] = distdict['rhypo']
        if 'rx' in methods:
            warnings.warn('No fault; Setting Rx to zero.')
            distdict['rx'] = np.zeros_like(distdict['repi'])
        if 'ry0' in methods:
            warnings.warn('No fault; Replacing ry0 with repi')
            distdict['ry0'] = distdict['repi']
        if 'ry' in methods:
            warnings.warn('No fault; Replacing ry with repi')
            distdict['ry'] = distdict['repi']

    return distdict
Exemple #10
0
def get_distance(methods, lat, lon, dep, source,
                 use_median_distance=True):
    """
    Calculate distance using any one of a number of distance measures.
    One of quadlist OR hypo must be specified. The following table gives
    the allowed distance strings and a description of each. 

    +--------+----------------------------------------------------------+
    | String | Description                                              |
    +========+==========================================================+
    | repi   | Distance to epicenter.                                   |
    +--------+----------------------------------------------------------+
    | rhypo  | Distance to hypocenter.                                  |
    +--------+----------------------------------------------------------+
    | rjb    | Joyner-Boore distance; this is closest distance to the   |
    |        | surface projection of the rupture plane.                 |
    +--------+----------------------------------------------------------+
    | rrup   | Rupture distance; closest distance to the rupture plane. |
    +--------+----------------------------------------------------------+
    | rx     | Strike-normal distance; same as GC2 coordiante T.        |
    +--------+----------------------------------------------------------+
    | ry     | Strike-parallel distance; same as GC2 coordiante U, but  |
    |        | with a shift in origin definition. See Spudich and Chiou |
    |        | (2015) http://dx.doi.org/10.3133/ofr20151028.            |
    +--------+----------------------------------------------------------+
    | ry0    | Horizontal distance off the end of the rupture measured  |
    |        | parallel to strike. Can only be zero or positive. We     |
    |        | compute this as a function of GC2 coordinate U.          |
    +--------+----------------------------------------------------------+
    | U      | GC2 coordinate U.                                        |
    +--------+----------------------------------------------------------+
    | T      | GC2 coordinate T.                                        |
    +--------+----------------------------------------------------------+

    :param methods:
        List of strings (or just a string) of distances to compute.
    :param lat:
       A numpy array of latitudes.
    :param lon:
       A numpy array of longidues.
    :param dep:
       A numpy array of depths (km).
    :param source:
       source instance.
    :param use_median_distance:
        Boolean; only used if GMPE requests fault distances and not fault is
        availalbe. Default is True, meaning that point-source distances are
        adjusted based on magnitude to get the median fault distance.
    :returns:
       dictionary of numpy arrays of distances, size of lon.shape
       IMPORTANT: If a finite fault is not supplied, and the distance
       measures requested include rx, ry, ry0, U, or T, then zeros will
       be returned; if rjb is requested, repi will be returned; if rrup
       is requested, rhypo will be returned.
    """
    fault = source.getFault()
    hypo = source.getHypo()
    if fault is not None:
        quadlist = fault.getQuadrilaterals()
        # Need a copy for GC2 since order of verticies/quads needs to be modivied.
        quadgc2 = copy.deepcopy(quadlist)
    else:
        quadlist = None

    # Dictionary for holding the distances
    distdict = dict()

    if not isinstance(methods, list):
        methods = [methods]

    methods_available = set(get_distance_measures())
    if not set(methods).issubset(methods_available):
        raise NotImplementedError(
            'One or more requested distance method is not '
            'valid or is not implemented yet')

    if (lat.shape == lon.shape) and (lat.shape == dep.shape):
        pass
    else:
        raise ShakeMapException('lat, lon, and dep must have the same shape.')

    oldshape = lon.shape

    if len(oldshape) == 2:
        newshape = (oldshape[0] * oldshape[1], 1)
    else:
        newshape = (oldshape[0], 1)

    if ('rrup' in methods) or ('rjb' in methods):
        x, y, z = latlon2ecef(lat, lon, dep)
        x.shape = newshape
        y.shape = newshape
        z.shape = newshape
        sites_ecef = np.hstack((x, y, z))

    # Define a projection that spands sites and fault
    if fault is None:
        all_lat = lat
        all_lon = lon
    else:
        all_lat = np.append(lat, fault.getLats())
        all_lon = np.append(lon, fault.getLons())

    west = np.nanmin(all_lon)
    east = np.nanmax(all_lon)
    south = np.nanmin(all_lat)
    north = np.nanmax(all_lat)
    proj = get_orthographic_projection(west, east, north, south)

    # ---------------------------------------------
    # Distances that do not require loop over quads
    # ---------------------------------------------

    if ('repi' in methods) or \
       (('rjb' in methods) and (quadlist is None)) or \
       (('rrup' in methods) and (quadlist is None)) or \
       (('ry0' in methods) and (quadlist is None)) or \
       (('rx' in methods) and (quadlist is None)) or \
       (('T' in methods) and (quadlist is None)) or \
       (('U' in methods) and (quadlist is None)):
        # I don't think this error check makes sense any more because hypo
        # is assigned above with source.getHypo() that constructs it from
        # source._event_dict entries. 
        if hypo is None:
            raise ShakeMapException('Cannot calculate epicentral distance '
                                    'without a point object')
        repidist = geodetic.distance(hypo.longitude, hypo.latitude, 0.0,
                                     lon, lat, dep)
        repidist = repidist.reshape(oldshape)
        distdict['repi'] = repidist

    if ('rhypo' in methods) or \
       (('rrup' in methods) and (quadlist is None)):
        if hypo is None:
            raise ShakeMapException('Cannot calculate epicentral distance '
                                    'without a point object')
        rhypodist = geodetic.distance(
            hypo.longitude, hypo.latitude, hypo.depth, lon, lat, dep)
        rhypodist = rhypodist.reshape(oldshape)
        distdict['rhypo'] = rhypodist

    # --------------------------------------------------------
    # Loop over quadlist for those distances that require loop
    # --------------------------------------------------------
    if 'rrup' in methods:
        minrrup = np.ones(newshape, dtype=lon.dtype) * 1e16
    if 'rjb' in methods:
        minrjb = np.ones(newshape, dtype=lon.dtype) * 1e16
    if ('rx' in methods) or ('ry' in methods) or \
       ('ry0' in methods) or ('U' in methods) or ('T' in methods):
        totweight = np.zeros(newshape, dtype=lon.dtype)
        GC2T = np.zeros(newshape, dtype=lon.dtype)
        GC2U = np.zeros(newshape, dtype=lon.dtype)

        if quadlist is not None:
            #-----------------------------------------------------------------
            # For these distances, we need to sort out strike discordance and
            # nominal strike prior to starting the loop if there is more than
            # one segment.
            #-----------------------------------------------------------------

            segind = fault._getSegmentIndex()
            segindnp = np.array(segind)
            uind = np.unique(segind)
            nseg = len(uind)

            #-------------------------------------------------------------------
            # The first thing we need to worry about is finding the coordinate
            # shift. U's origin is " selected from the two endpoints most
            # distant from each other." 
            #-------------------------------------------------------------------

            if nseg > 1:
                # Need to get index of first and last quad
                # for each segment
                iq0 = np.zeros(nseg, dtype='int16')
                iq1 = np.zeros(nseg, dtype='int16')
                for k in uind:
                    ii = [i for i, j in enumerate(segind) if j == uind[k]]
                    iq0[k] = int(np.min(ii))
                    iq1[k] = int(np.max(ii))

                #---------------------------------------------------------------
                # This is an iterator for each possible combination of segments
                # including segment orientations (i.e., flipped). 
                #---------------------------------------------------------------

                it_seg = it.product(it.combinations(uind, 2),
                                    it.product([0, 1], [0, 1]))

                # Placeholder for the segment pair/orientation that gives the
                # largest distance. 
                dist_save = 0

                for k in it_seg:
                    s0ind = k[0][0]
                    s1ind = k[0][1]
                    p0ind = k[1][0]
                    p1ind = k[1][1]
                    if p0ind == 0:
                        P0 = quadlist[iq0[s0ind]][0]
                    else:
                        P0 = quadlist[iq1[s0ind]][1]
                    if p1ind == 0:
                        P1 = quadlist[iq1[s1ind]][0]
                    else:
                        P1 = quadlist[iq0[s1ind]][1]

                    dist = geodetic.distance(P0.longitude, P0.latitude, 0.0,
                                             P1.longitude, P1.latitude, 0.0)
                    if dist > dist_save:
                        dist_save = dist
                        A0 = P0
                        A1 = P1

                #---------------------------------------------------------------
                # A0 and A1 are the furthest two segment endpoints, but we still
                # need to sort out which one is the "origin".
                #---------------------------------------------------------------

                # Goofy while-loop is to adjust the side of the fault where the
                # origin is located
                dummy = -1
                while dummy < 0:
                    A0.depth = 0
                    A1.depth = 0
                    p_origin = Vector.fromPoint(A0)
                    a0 = Vector.fromPoint(A0)
                    a1 = Vector.fromPoint(A1)
                    ahat = (a1 - a0).norm()

                    # Loop over traces
                    e_j = np.zeros(nseg)
                    b_prime = [None] * nseg
                    for j in range(nseg):
                        P0 = quadlist[iq0[j]][0]
                        P1 = quadlist[iq1[j]][1]
                        P0.depth = 0
                        P1.depth = 0
                        p0 = Vector.fromPoint(P0)
                        p1 = Vector.fromPoint(P1)
                        b_prime[j] = p1 - p0
                        e_j[j] = ahat.dot(b_prime[j])
                    E = np.sum(e_j)

                    # List of discordancy
                    dc = [np.sign(a) * np.sign(E) for a in e_j]
                    b = Vector(0, 0, 0)
                    for j in range(nseg):
                        b.x = b.x + b_prime[j].x * dc[j]
                        b.y = b.y + b_prime[j].y * dc[j]
                        b.z = b.z + b_prime[j].z * dc[j]
                    bhat = b.norm()
                    dummy = bhat.dot(ahat)
                    if dummy < 0:
                        tmpA0 = copy.copy(A0)
                        tmpA1 = copy.copy(A1)
                        A0 = tmpA1
                        A1 = tmpA0

                # To fix discordancy, need to flip quads and rearrange
                # the order of quadgc2

                # 1) flip quads
                for i in range(len(quadgc2)):
                    if dc[segind[i]] < 0: #***************U*UUUUUUUSDFUSDfkjjhakjsdhfljkhn
                        quadgc2[i] = reverse_quad(quadgc2[i])

                # 2) rearrange quadlist to remove discordancy
                qind = np.arange(len(quadgc2))
                segnp = np.array(segind)
                for i in range(nseg):
                    qsel = qind[segnp == uind[i]]
                    if dc[i] < 0:
                        qrev = qsel[::-1]
                        qind[segnp == uind[i]] = qrev

                quadgc2old = copy.deepcopy(quadgc2)
                for i in range(len(qind)):
                    quadgc2[i] = quadgc2old[qind[i]]


    if quadlist is not None:
        # Length of prior segments
        s_i = 0.0
        l_i = np.zeros(len(quadlist))
        for i in range(len(quadlist)):
            P0, P1, P2, P3 = quadlist[i]
            G0, G1, G2, G3 = quadgc2[i]

            if 'rrup' in methods:
                rrupdist = _calc_rupture_distance(P0, P1, P2, P3, sites_ecef)
                minrrup = np.minimum(minrrup, rrupdist)

            if 'rjb' in methods:
                S0 = copy.deepcopy(P0)
                S1 = copy.deepcopy(P1)
                S2 = copy.deepcopy(P2)
                S3 = copy.deepcopy(P3)
                S0.depth = 0.0
                S1.depth = 0.0
                S2.depth = 0.0
                S3.depth = 0.0
                rjbdist = _calc_rupture_distance(S0, S1, S2, S3, sites_ecef)
                minrjb = np.minimum(minrjb, rjbdist)

            if ('rx' in methods) or ('ry' in methods) or \
               ('ry0' in methods) or ('U' in methods) or ('T' in methods):
                # Rx, Ry, and Ry0 are all computed if one is requested since
                # they all require similar information for the weights. This
                # isn't necessary for a single segment fault though.
                # Note, we are basing these calculations on GC2 coordinates U
                # and T as described in:
                # Spudich and Chiou (2015)
                # http://dx.doi.org/10.3133/ofr20151028.

                # Compute u_i and t_i for this quad
                t_i = __calc_t_i(G0, G1, lat, lon, proj)
                u_i = __calc_u_i(G0, G1, lat, lon, proj)

                # Quad length
                l_i[i] = get_quad_length(quadlist[i])

                # Weight of segment, three cases
                # Case 3: t_i == 0 and 0 <= u_i <= l_i
                w_i = np.zeros_like(t_i)
                # Case 1:
                ix = t_i != 0
                w_i[ix] = (1.0 / t_i[ix]) * (np.arctan((l_i[i] -
                    u_i[ix]) / t_i[ix]) - np.arctan(-u_i[ix] / t_i[ix]))
                # Case 2:
                ix = (t_i == 0) & ((u_i < 0) | (u_i > l_i[i]))
                w_i[ix] = 1 / (u_i[ix] - l_i[i]) - 1 / u_i[ix]

                totweight = totweight + w_i
                GC2T = GC2T + w_i * t_i
                if nseg == 1:
                    GC2U = GC2U + w_i * (u_i + s_i)
                else:
                    if i == 0:
                        qind = np.array(range(len(quadlist)))
                        l_kj = 0
                        s_ij_1 = 0
                    else:
                        l_kj = l_i[(segindnp == segindnp[i]) & (qind < i)]
                        s_ij_1 = np.sum(l_kj)

                    p1 = Vector.fromPoint(quadgc2[iq0[segind[i]]][0])
                    s_ij_2 = (p1 - p_origin).dot(np.sign(E)*ahat) / 1000.0
                    # Above is GC2N, for GC2T use:
#                    s_ij_2 = (p1 - p_origin).dot(bhat) / 1000.0


                    s_ij = s_ij_1 + s_ij_2
                    GC2U = GC2U + w_i * (u_i + s_ij)
                s_i = s_i + l_i[i]

        # Collect distances from loop into the distance dict
        if 'rjb' in methods:
            minrjb = minrjb.reshape(oldshape)
            distdict['rjb'] = minrjb

        if ('rx' in methods) or ('ry' in methods) or \
           ('ry0' in methods) or ('U' in methods) or ('T' in methods):
            # Normalize by sum of quad weights
            GC2T = GC2T / totweight
            GC2U = GC2U / totweight
            distdict['T'] = copy.deepcopy(GC2T).reshape(oldshape)
            distdict['U'] = copy.deepcopy(GC2U).reshape(oldshape)

            # Take care of Rx
            Rx = copy.deepcopy(GC2T)  # preserve sign (no absolute value)
            Rx = Rx.reshape(oldshape)
            distdict['rx'] = Rx

            # Ry
            Ry = GC2U - s_i / 2.0
            Ry = Ry.reshape(oldshape)
            distdict['ry'] = Ry

            # Ry0
            Ry0 = np.zeros_like(GC2U)
            ix = GC2U < 0
            Ry0[ix] = np.abs(GC2U[ix])
            if nseg > 1:
                s_i = s_ij + l_i[-1]
            ix = GC2U > s_i
            Ry0[ix] = GC2U[ix] - s_i
            Ry0 = Ry0.reshape(oldshape)
            distdict['ry0'] = Ry0

        if 'rrup' in methods:
            minrrup = minrrup.reshape(oldshape)
            distdict['rrup'] = minrrup

    else:
        if 'rjb' in methods:
            if use_median_distance:
                warnings.warn(
                    'No fault; Replacing rjb with median rjb given M and repi.')
                cdir, tmp = os.path.split(__file__)

                # -------------------
                # Sort out file names
                # -------------------
                mech = source.getEventParam('mech')
                if not hasattr(source, '_tectonic_region'):
                    rf = os.path.join(
                        cdir, "data", "ps2ff", "Rjb_WC94_mechA_ar1p0_seis0_20_Ratios.csv")
                    vf = os.path.join(
                        cdir, "data", "ps2ff", "Rjb_WC94_mechA_ar1p0_seis0_20_Var.csv")
                elif source._tectonic_region == 'Active Shallow Crust':
                    if mech == 'ALL':
                        rf = os.path.join(
                            cdir, "data", "ps2ff", "Rjb_WC94_mechA_ar1p7_seis0_20_Ratios.csv")
                        vf = os.path.join(
                            cdir, "data", "ps2ff", "Rjb_WC94_mechA_ar1p7_seis0_20_Var.csv")
                    elif mech == 'RS':
                        rf = os.path.join(
                            cdir, "data", "ps2ff", "Rjb_WC94_mechR_ar1p7_seis0_20_Ratios.csv")
                        vf = os.path.join(
                            cdir, "data", "ps2ff", "Rjb_WC94_mechR_ar1p7_seis0_20_Var.csv")
                    elif mech == 'NM':
                        rf = os.path.join(
                            cdir, "data", "ps2ff", "Rjb_WC94_mechN_ar1p7_seis0_20_Ratios.csv")
                        vf = os.path.join(
                            cdir, "data", "ps2ff", "Rjb_WC94_mechN_ar1p7_seis0_20_Var.csv")
                    elif mech == 'SS':
                        rf = os.path.join(
                            cdir, "data", "ps2ff", "Rjb_WC94_mechSS_ar1p7_seis0_20_Ratios.csv")
                        vf = os.path.join(
                            cdir, "data", "ps2ff", "Rjb_WC94_mechSS_ar1p7_seis0_20_Var.csv")
                elif source._tectonic_region == 'Stable Shallow Crust':
                    if mech == 'ALL':
                        rf = os.path.join(
                            cdir, "data", "ps2ff", "Rjb_S14_mechA_ar1p0_seis0_15_Ratios.csv")
                        vf = os.path.join(
                            cdir, "data", "ps2ff", "Rjb_S14_mechA_ar1p0_seis0_15_Var.csv")
                    elif mech == 'RS':
                        rf = os.path.join(
                            cdir, "data", "ps2ff", "Rjb_S14_mechR_ar1p0_seis0_15_Ratios.csv")
                        vf = os.path.join(
                            cdir, "data", "ps2ff", "Rjb_S14_mechR_ar1p0_seis0_15_Var.csv")
                    elif mech == 'NM':
                        rf = os.path.join(
                            cdir, "data", "ps2ff", "Rjb_S14_mechN_ar1p0_seis0_15_Ratios.csv")
                        vf = os.path.join(
                            cdir, "data", "ps2ff", "Rjb_S14_mechN_ar1p0_seis0_15_Var.csv")
                    elif mech == 'SS':
                        rf = os.path.join(
                            cdir, "data", "ps2ff", "Rjb_S14_mechSS_ar1p0_seis0_15_Ratios.csv")
                        vf = os.path.join(
                            cdir, "data", "ps2ff", "Rjb_S14_mechSS_ar1p0_seis0_15_Var.csv")
                else:
                    warnings.warn(
                        'Unsupported tectonic region; using coefficients for unknown'
                        'tectonic region.')
                    rf = os.path.join(
                        cdir, "data", "ps2ff", "Rjb_WC94_mechA_ar1p0_seis0_20_Ratios.csv")
                    vf = os.path.join(
                        cdir, "data", "ps2ff", "Rjb_WC94_mechA_ar1p0_seis0_20_Var.csv")

                # -----------------
                # Start with ratios
                # -----------------
                repi2rjb_ratios_tbl = pd.read_csv(rf, comment='#')
                r2rrt_cols = repi2rjb_ratios_tbl.columns[1:]
                mag_list = []
                for column in (r2rrt_cols):
                    if re.search('R\d+\.*\d*', column):
                        magnitude = float(re.findall(
                            'R(\d+\.*\d*)', column)[0])
                        mag_list.append(magnitude)
                mag_list = np.array(mag_list)
                dist_list = np.log(np.array(repi2rjb_ratios_tbl['Repi_km']))
                repi2rjb_grid = repi2rjb_ratios_tbl.values[:, 1:]
                repi2rjb_obj = spint.RectBivariateSpline(
                    dist_list, mag_list, repi2rjb_grid, kx=1, ky=1)

                def repi2rjb_tbl(repi, M):
                    ratio = repi2rjb_obj.ev(np.log(repi), M)
                    rjb = repi * ratio
                    return rjb
                repis = distdict['repi']
                mags = np.ones_like(repis) * source.getEventParam('mag')
                rjb_hat = repi2rjb_tbl(repis, mags)
                distdict['rjb'] = rjb_hat
                # -------------------
                # Additional Variance
                # -------------------
                repi2rjbvar_ratios_tbl = pd.read_csv(vf, comment='#')
                repi2rjbvar_grid = repi2rjbvar_ratios_tbl.values[:, 1:]
                repi2rjbvar_obj = spint.RectBivariateSpline(
                    dist_list, mag_list, repi2rjbvar_grid, kx=1, ky=1)
                rjbvar = repi2rjbvar_obj.ev(np.log(repis), mags)
                distdict['rjbvar'] = rjbvar
            else:
                warnings.warn('No fault; Replacing rjb with repi')
                distdict['rjb'] = distdict['repi'].copy()
        if 'rrup' in methods:
            if use_median_distance:
                warnings.warn(
                    'No fault; Replacing rrup with median rrup given M and repi.')
                cdir, tmp = os.path.split(__file__)

                # -------------------
                # Sort out file names
                # -------------------
                rake = source._event_dict.get('rake')
                mech = rake_to_mech(rake)
                if not hasattr(source, '_tectonic_region'):
                    rf = os.path.join(
                        cdir, "data", "ps2ff", "Rrup_WC94_mechA_ar1p0_seis0-20_Ratios.csv")
                    vf = os.path.join(
                        cdir, "data", "ps2ff", "Rrup_WC94_mechA_ar1p0_seis0-20_Var.csv")
                elif source._tectonic_region == 'Active Shallow Crust':
                    if mech == 'ALL':
                        rf = os.path.join(
                            cdir, "data", "ps2ff", "Rrup_WC94_mechA_ar1p7_seis0-20_Ratios.csv")
                        vf = os.path.join(
                            cdir, "data", "ps2ff", "Rrup_WC94_mechA_ar1p7_seis0-20_Var.csv")
                    elif mech == 'RS':
                        rf = os.path.join(
                            cdir, "data", "ps2ff", "Rrup_WC94_mechR_ar1p7_seis0-20_Ratios.csv")
                        vf = os.path.join(
                            cdir, "data", "ps2ff", "Rrup_WC94_mechR_ar1p7_seis0-20_Var.csv")
                    elif mech == 'NM':
                        rf = os.path.join(
                            cdir, "data", "ps2ff", "Rrup_WC94_mechN_ar1p7_seis0-20_Ratios.csv")
                        vf = os.path.join(
                            cdir, "data", "ps2ff", "Rrup_WC94_mechN_ar1p7_seis0-20_Var.csv")
                    elif mech == 'SS':
                        rf = os.path.join(
                            cdir, "data", "ps2ff", "Rrup_WC94_mechSS_ar1p7_seis0-20_Ratios.csv")
                        vf = os.path.join(
                            cdir, "data", "ps2ff", "Rrup_WC94_mechSS_ar1p7_seis0-20_Var.csv")
                elif source._tectonic_region == 'Stable Shallow Crust':
                    if mech == 'ALL':
                        rf = os.path.join(
                            cdir, "data", "ps2ff", "Rrup_S14_mechA_ar1p0_seis0-15_Ratios.csv")
                        vf = os.path.join(
                            cdir, "data", "ps2ff", "Rrup_S14_mechA_ar1p0_seis0-15_Var.csv")
                    elif mech == 'RS':
                        rf = os.path.join(
                            cdir, "data", "ps2ff", "Rrup_S14_mechR_ar1p0_seis0-15_Ratios.csv")
                        vf = os.path.join(
                            cdir, "data", "ps2ff", "Rrup_S14_mechR_ar1p0_seis0-15_Var.csv")
                    elif mech == 'NM':
                        rf = os.path.join(
                            cdir, "data", "ps2ff", "Rrup_S14_mechN_ar1p0_seis0-15_Ratios.csv")
                        vf = os.path.join(
                            cdir, "data", "ps2ff", "Rrup_S14_mechN_ar1p0_seis0-15_Var.csv")
                    elif mech == 'SS':
                        rf = os.path.join(
                            cdir, "data", "ps2ff", "Rrup_S14_mechSS_ar1p0_seis0-15_Ratios.csv")
                        vf = os.path.join(
                            cdir, "data", "ps2ff", "Rrup_S14_mechSS_ar1p0_seis0-15_Var.csv")
                else:
                    warnings.warn(
                        'Unsupported tectonic region; using coefficients for unknown'
                        'tectonic region.')
                    rf = os.path.join(
                        cdir, "data", "ps2ff", "Rrup_WC94_mechA_ar1p0_seis0-20_Ratios.csv")
                    vf = os.path.join(
                        cdir, "data", "ps2ff", "Rrup_WC94_mechA_ar1p0_seis0-20_Var.csv")

                # -----------------
                # Start with ratios
                # -----------------
                repi2rrup_ratios_tbl = pd.read_csv(rf, comment='#')
                r2rrt_cols = repi2rrup_ratios_tbl.columns[1:]
                mag_list = []
                for column in (r2rrt_cols):
                    if re.search('R\d+\.*\d*', column):
                        magnitude = float(re.findall(
                            'R(\d+\.*\d*)', column)[0])
                        mag_list.append(magnitude)
                mag_list = np.array(mag_list)
                dist_list = np.log(np.array(repi2rrup_ratios_tbl['Repi_km']))
                repi2rrup_grid = repi2rrup_ratios_tbl.values[:, 1:]
                repi2rrup_obj = spint.RectBivariateSpline(
                    dist_list, mag_list, repi2rrup_grid, kx=1, ky=1)

                def repi2rrup_tbl(repi, M):
                    ratio = repi2rrup_obj.ev(np.log(repi), M)
                    rrup = repi * ratio
                    return rrup
                repis = distdict['repi']
                mags = np.ones_like(repis) * source.getEventParam('mag')
                rrup_hat = repi2rrup_tbl(repis, mags)
                distdict['rrup'] = rrup_hat

                # -------------------
                # Additional Variance
                # -------------------
                repi2rrupvar_ratios_tbl = pd.read_csv(vf, comment='#')
                repi2rrupvar_grid = repi2rrupvar_ratios_tbl.values[:, 1:]
                repi2rrupvar_obj = spint.RectBivariateSpline(
                    dist_list, mag_list, repi2rrupvar_grid, kx=1, ky=1)
                rrupvar = repi2rrupvar_obj.ev(np.log(repis), mags)
                distdict['rrupvar'] = rrupvar
            else:
                warnings.warn('No fault; Replacing rrup with rhypo')
                distdict['rrup'] = distdict['rhypo'].copy()
        if 'rx' in methods:
            warnings.warn('No fault; Setting Rx to zero.')
            distdict['rx'] = np.zeros_like(distdict['repi'])
        if 'ry0' in methods:
            warnings.warn('No fault; Setting ry0 to zero')
            distdict['ry0'] = np.zeros_like(distdict['repi'])
        if 'ry' in methods:
            warnings.warn('No fault; Setting ry to zero')
            distdict['ry'] = np.zeros_like(distdict['repi'])
        if 'U' in methods:
            warnings.warn('No fault; Setting U to zero')
            distdict['U'] = np.zeros_like(distdict['repi'])
        if 'T' in methods:
            warnings.warn('No fault; Setting T to zero')
            distdict['T'] = np.zeros_like(distdict['repi'])

    return distdict
Exemple #11
0
    def __computeThetaAndS(self, i):
        """
        :param i:
            Compute d for the i-th quad/segment.
        """
        # self.phyp is in ECEF
        tmp = ecef.ecef2latlon(self.phyp[i].x, self.phyp[i].y, self.phyp[i].z)
        epi_ecef = Vector.fromPoint(geo.point.Point(tmp[1], tmp[0], 0.0))
        epi_col = np.array([[epi_ecef.x], [epi_ecef.y], [epi_ecef.z]])

        # First compute along strike vector
        P0, P1, P2, P3 = self._flt.getQuadrilaterals()[i]
        p0 = Vector.fromPoint(P0)  # convert to ECEF
        p1 = Vector.fromPoint(P1)
        e01 = p1 - p0
        e01norm = e01.norm()
        hp0 = p0 - epi_ecef
        hp1 = p1 - epi_ecef
        strike_min = Vector.dot(hp0, e01norm) / 1000.0  # convert to km
        strike_max = Vector.dot(hp1, e01norm) / 1000.0  # convert to km
        strike_col = np.array([[e01norm.x], [e01norm.y],
                               [e01norm.z]])  # ECEF coords

        # Sites
        slat = self._lat
        slon = self._lon

        # Convert sites to ECEF:
        site_ecef_x = np.ones_like(slat)
        site_ecef_y = np.ones_like(slat)
        site_ecef_z = np.ones_like(slat)

        # Make a 3x(#number of sites) matrix of site locations
        # (rows are x, y, z) in ECEF
        site_ecef_x, site_ecef_y, site_ecef_z = ecef.latlon2ecef(
            slat, slon, np.zeros(slon.shape))
        site_mat = np.array([
            np.reshape(site_ecef_x, (-1, )),
            np.reshape(site_ecef_y, (-1, )),
            np.reshape(site_ecef_z, (-1, ))
        ])

        # Epicenter-to-site matrix
        e2s_mat = site_mat - epi_col  # in ECEF
        mag = np.sqrt(np.sum(e2s_mat * e2s_mat, axis=0))

        # Avoid division by zero
        mag[mag == 0] = 1e-12
        e2s_norm = e2s_mat / mag

        # Dot epicenter-to-site with along-strike vector
        s_raw = np.sum(e2s_mat * strike_col, axis=0) / 1000.0  # conver to km

        # Put back into a 2d array
        s_raw = np.reshape(s_raw, self._lat.shape)
        self.s = np.abs(s_raw.clip(min=strike_min,
                                   max=strike_max)).clip(min=np.exp(1))

        # Compute theta
        sdots = np.sum(e2s_norm * strike_col, axis=0)
        theta_raw = np.arccos(sdots)

        # But theta is defined to be the reference angle
        # (i.e., the equivalent angle between 0 and 90 deg)
        sintheta = np.abs(np.sin(theta_raw))
        costheta = np.abs(np.cos(theta_raw))
        theta = np.arctan2(sintheta, costheta)
        self.theta = np.reshape(theta, self._lat.shape)