# relMT - Program to compute relative earthquake moment tensors
# Copyright (C) 2024 Wasja Bloch, Doriane Drolet, Michael Bostock
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""Functions to set up and solve the linear systems"""
import numpy as np
import math
from collections import defaultdict
from relmt import core, ls
logger = core.register_logger(__name__)
[docs]
def mt_array(mt: core.MT | np.ndarray) -> np.ndarray:
"""Return 3x3 array representation of moment tensor"""
return np.array(
[[mt[0], mt[3], mt[4]], [mt[3], mt[1], mt[5]], [mt[4], mt[5], mt[2]]]
)
[docs]
def mt_tuple(mt_arr: np.ndarray) -> core.MT:
"""Return tuple representation of 3x3 moment tensor array"""
return core.MT(
mt_arr[0, 0],
mt_arr[1, 1],
mt_arr[2, 2],
mt_arr[0, 1],
mt_arr[0, 2],
mt_arr[1, 2],
)
[docs]
def mt_tuples(mt_vec: np.ndarray, mt_constraint: str) -> list[core.MT]:
"""Return tuple representations of moment tensor vector."""
mt_elements = ls.mt_elements(mt_constraint)
if len(mt_vec) % mt_elements != 0:
msg = f"length of mt_vec ({len(mt_vec)} not devisiable by "
msg += f"'mt_elements' ({mt_elements})"
raise IndexError(msg)
mts = []
for iev in range(int(len(mt_vec) / mt_elements)):
# First index of MT corresoponding to iev
i0 = iev * mt_elements
if mt_elements == 6:
mt = core.MT(*mt_vec[i0 : i0 + mt_elements])
elif mt_elements == 5:
mt = core.MT(
mt_vec[i0],
mt_vec[i0 + 1],
-mt_vec[i0] - mt_vec[i0 + 1],
mt_vec[i0 + 2],
mt_vec[i0 + 3],
mt_vec[i0 + 4],
)
mts.append(mt)
return mts
[docs]
def moment_of_tensor(mt_arr: np.ndarray) -> float:
"""Seismic moment of a moment tensor"""
return np.linalg.norm(mt_arr) / np.sqrt(2)
[docs]
def moment_of_vector(m: np.ndarray | core.MT) -> float | np.ndarray:
"""Seismic moment of tensor in 6-element vector notation
For arrays, sum moment along last dimension and return array of moments
"""
try:
return np.sqrt(
m[..., 0] ** 2
+ m[..., 1] ** 2
+ m[..., 2] ** 2
+ 2 * m[..., 3] ** 2
+ 2 * m[..., 4] ** 2
+ 2 * m[..., 5] ** 2
) / np.sqrt(2)
except TypeError:
return np.sqrt(
m[0] ** 2
+ m[1] ** 2
+ m[2] ** 2
+ 2 * m[3] ** 2
+ 2 * m[4] ** 2
+ 2 * m[5] ** 2
) / np.sqrt(2)
[docs]
def moment_of_magnitude(magnitude: float) -> float:
"""Seismic moment of a magnitude"""
return 10 ** (1.5 * (magnitude + 10.7)) * 1e-7 # in Nm
[docs]
def magnitude_of_moment(moment: float) -> float:
"""Magnitude of a seismic moment"""
return np.log10(moment * 1e7) / 1.5 - 10.7
[docs]
def magnitude_of_vector(vector: core.MT | tuple) -> float:
"""Magnitude of a moment tensor in 6-element vector notation"""
return magnitude_of_moment(moment_of_vector(vector))
[docs]
def mean_moment(mts: list[core.MT]) -> float:
"""Mean seismic moment of list of moment tensors"""
return sum((moment_of_tensor(mt_array(mt)) for mt in mts)) / len(mts)
[docs]
def p_radiation(
M: np.ndarray,
azi: float,
plu: float,
dist: float,
rho: float,
alpha: float,
only_first: bool = False,
) -> np.ndarray:
"""P radiation pattern of a moment tensor M
Parameters
----------
M:
``(3,3)`` moment tensor (Nm)
azi:
Source to receiver azimuth in degree E of N
inc:
Source to receiver ray plunge in degreen down from horizontal
dist:
Source to Receiver distance (m)
rho:
Density of the medium (kg m^-3)
alpha:
P-wave velocity of the medim (m/s)
only_first:
If True, only return the first component of the displacement vector
Returns
-------
``(3,)`` (or ``(1,)`` when `only_first=True`) displacement vector at receiver
"""
g = ls.gamma(azi, plu)
gMg = g @ M @ g
u = gMg * g / (4.0 * np.pi * rho * alpha**3.0 * dist)
return u
[docs]
def s_radiation(
M: np.ndarray, azi: float, plu: float, dist: float, rho: float, beta: float
) -> np.ndarray:
"""
S radiation pattern of a moment tensor M
Parameters
----------
M:
``(3, 3)`` moment tensor (Nm)
azi:
Source to receiver azimuth in degree E of N
inc:
Source to receiver ray plunge in degreen down from horizontal
dist:
Source to Receiver distance (m)
rho:
Density of the medium (kg m^-3)
beta:
S-wave velocity of the medim (m/s)
Returns
-------
``(3,)`` displacement vector at receiver
"""
g = ls.gamma(azi, plu)
gMg = g @ M @ g
u = ((M @ g) - gMg * g) / (4.0 * np.pi * rho * beta**3.0 * dist)
return u
[docs]
def norm_rms(
mt_list: list[core.MT] | dict[int, list[core.MT]], mt_true: core.MT | None = None
) -> float | dict[int, float]:
"""Normalized RMS misfit of MTs
Parameters
----------
mt_list:
List of MT observations to compare, or dict of lists
mt_true:
A true MT from which to compute deviation. If None, use mean of `mt_list`
Returns
-------
normalized root-mean-square misfit
"""
if type(mt_list) == dict:
if type(mt_true) != dict:
# Reutrn the mt_true value on any input
mt_true = defaultdict(lambda _: mt_true)
return {evn: norm_rms(mtl, mt_true[evn]) for evn, mtl in mt_list.items()}
mt_array = np.array(mt_list)
mean = mt_array.mean(axis=0)
m0 = mean_moment(mt_list)
if mt_true is not None:
mean = np.array(mt_true)
m0 = moment_of_vector(mt_true)
return np.sqrt(np.sum((mt_array - mean) ** 2) / mt_array.size) / m0
[docs]
def kagan_rms(
mt_list: list[core.MT] | dict[int, list[core.MT]],
mt_true: core.MT | dict[int, core.MT] | None = None,
) -> float | dict[int, float]:
"""RMS of the kagan angles between MTs
Parameters
----------
mt_list:
List of MT observations to compare, or dict of lists
mt_true:
A true MT from which to compute deviation. If None, use mean of `mt_list`
Returns
-------
RMS of the kagan angles
"""
if type(mt_list) == dict:
if type(mt_true) != dict:
# Reutrn the mt_true value on any input
mt_true = defaultdict(lambda _: mt_true)
return {evn: kagan_rms(mtl, mt_true[evn]) for evn, mtl in mt_list.items()}
mt_array = np.array(mt_list)
mean = mt_array.mean(axis=0)
if mt_true is not None:
mean = np.array(mt_true)
kagans = np.array([kagan_angle(mean, this) for this in mt_array])
return np.sqrt(np.sum(kagans**2) / mt_array.size)
_pbt2tpb = np.array(((0.0, 0.0, 1.0), (1.0, 0.0, 0.0), (0.0, 1.0, 0.0)))
def _tpb2q(t, p, b):
"""Quaternion rotating P, B, T axes into alignment
Parameters
----------
t, p, b:
Three orthogonal unit vectors representing the T, P and B axes
Returns
-------
:class:`numpy.ndarray`
``(4,)`` quaternion ``[w, x, y, z]`` describing the rotation
"""
eps = 0.001
tqw = 1.0 + t[0] + p[1] + b[2]
tqx = 1.0 + t[0] - p[1] - b[2]
tqy = 1.0 - t[0] + p[1] - b[2]
tqz = 1.0 - t[0] - p[1] + b[2]
q = np.zeros(4)
if tqw > eps:
q[0] = 0.5 * math.sqrt(tqw)
q[1:] = p[2] - b[1], b[0] - t[2], t[1] - p[0]
elif tqx > eps:
q[0] = 0.5 * math.sqrt(tqx)
q[1:] = p[2] - b[1], p[0] + t[1], b[0] + t[2]
elif tqy > eps:
q[0] = 0.5 * math.sqrt(tqy)
q[1:] = b[0] - t[2], p[0] + t[1], b[1] + p[2]
elif tqz > eps:
q[0] = 0.5 * math.sqrt(tqz)
q[1:] = t[1] - p[0], b[0] + t[2], b[1] + p[2]
else:
raise Exception("should not reach this line, check theory!")
# q[0] = max(0.5 * math.sqrt(tqx), eps)
# q[1:] = p[2] - b[1], p[0] + t[1], b[0] + t[2]
q[1:] /= 4.0 * q[0]
q /= math.sqrt(np.sum(q**2))
return q
[docs]
def kagan_angle(mt1: core.MT, mt2: core.MT) -> float:
"""
Given two moment tensors, return the Kagan angle in degrees.
..note: Implementation curtesy of :func:`pyrocko.moment_tensor.kagan_angle`
After Kagan (1991) and Tape & Tape (2012).
"""
mta1 = mt_array(mt1)
mta2 = mt_array(mt2)
eve1 = np.linalg.eigh(mta1)[1]
eve2 = np.linalg.eigh(mta2)[1]
if np.linalg.det(eve1) < 0.0:
eve1 *= -1.0
if np.linalg.det(eve2) < 0.0:
eve2 *= -1.0
ai = np.dot(_pbt2tpb, eve1.T)
aj = np.dot(_pbt2tpb, eve2.T)
u = np.dot(ai, aj.T)
tk, pk, bk = u
qk = _tpb2q(tk, pk, bk)
return 2.0 * math.acos(np.max(np.abs(qk))) * 180 / np.pi
[docs]
def rtf2ned(
mrr: float, mtt: float, mff: float, mrt: float, mrf: float, mtf: float
) -> tuple[float, float, float, float, float, float]:
"""
Convert moment tensor in `r` (up), `t` (south), `p` (east) coordinates to
north-east-down
"""
# mnn, mee, mdd, mne, mnd, med
return core.MT(mtt, mff, mrr, -mtf, mrt, -mrf)
[docs]
def ned2rtf(
mnn: float, mee: float, mdd: float, mne: float, mnd: float, med: float
) -> tuple[float, float, float, float, float, float]:
"""
Convert moment tensor in north-east-down coordinates to
`r` (up), `t` (south), `f` (east)
"""
# mrr, mtt, mff, mrt, mrf, mtf
return mdd, mnn, mee, mnd, -med, -mne
[docs]
def correlation(M1: core.MT, M2: core.MT) -> tuple[float, float]:
"""
P and S correlation coefficients between two moment tensors
Implements Equation 5 of Kuge and Kawakatsu (1993, PEPI)
Parameters
----------
M1, M2:
The two moment tensors to compare
Returns
-------
Correlation coefficients for the P-SV and SH system
"""
m1 = ned2rtf(*M1)
m2 = ned2rtf(*M2)
# M = Mrr Mtt Mff Mrt Mrf Mtf
# 0 1 2 3 4 5
def I(M):
return (M[0] + M[1] + M[2]) / 3
def C(M):
return (M[1] + M[2] - 2 * M[0]) / 3
def D(M):
return (M[1] - M[2]) / 2
def A(M, l, m):
if l == 0:
return 2 * np.pi * I(M)
if l == 2:
if m == 0:
return -2 * np.sqrt(np.pi / 5) * C(M)
if m == 1:
return -2 * np.sqrt(2 * np.pi / 15) + M[3] + 1j * M[4]
if m == -1:
return -2 * np.sqrt(2 * np.pi / 15) - M[3] + 1j * M[4]
if m == 2:
return 2 * np.sqrt(2 * np.pi / 15) * (D(M) + 1j * M[5])
if m == -2:
return 2 * np.sqrt(2 * np.pi / 15) * (D(M) - 1j * M[5])
def B(M, l, m):
if l == 0 or m == 0:
return 0.0
if l == 2:
return A(M, l, m)
def doublesum(first, second, coefficient):
return np.sum(
[
coefficient(first, l, m) * np.conjugate(coefficient(second, l, m))
for l in [0, 2] # Not evaluated at l=1
for m in range(-l, l + 1)
]
)
def plusminus_one(number):
return min(1, max(-1, number))
eta_p = np.real(
doublesum(m1, m2, A)
/ np.sqrt(doublesum(m1, m1, A))
/ np.sqrt(doublesum(m2, m2, A))
)
eta_h = np.real(
doublesum(m1, m2, B)
/ np.sqrt(doublesum(m1, m1, B))
/ np.sqrt(doublesum(m2, m2, B))
)
return plusminus_one(eta_p), plusminus_one(eta_h)
[docs]
def norm_scalar_product(M1: core.MT, M2: core.MT) -> float:
"""
Return the normalized scalar product of two moment tensors
See Michael (1987, JGR) Eq. 1 for motivation
"""
m1 = mt_array(M1)
m2 = mt_array(M2)
return np.sum(m1 * m2) / (np.sqrt(np.sum(m1 * m1)) * np.sqrt(np.sum(m2 * m2)))