# -*- coding: utf-8 -*-
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright (C) 2024-2026 Yen-Shin Chen, OGS
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
Module :mod:`openquake.pfd.secondary_surf_rup.moss2022` implements
Moss et al. (2022) distributed (secondary) surface rupture probability.
References
----------
Moss, R., Thompson, S., Kuo, C.-H., Younesi, K., and Baumont, D. (2022).
Reverse Fault PFDHA. Report GIRS-2022-05 (Revised 1/17/2024).
DOI: 10.34948/N3F595
"""
import numpy as np
from openquake.pfd.secondary_surf_rup.base import BaseSecondarySurfRup
# ── Table 5.3: simple exponential coefficients (85th pct, 500 m grid) ───
# Equation 5.5: P(d>0) = min(exp(−a·r_km + b), 1)
_COEFF_SIMPLE = {
'hw': {'a': 2.2, 'b': 0.5},
'fw': {'a': 2.4, 'b': 0.4},
}
# ── Tables 5.4–5.5: biexponential frequency-CDF coefficients ────────────
# Equation 5.7: F(x) = a·exp(b·x) + c·exp(d·x), x in metres
# Equation 5.6: P = max(1 − F(x), 0)
# Default rows use "exp+random" where available.
_COEFF_BIEXP = {
'hw': {
'M7': {'a': 0.6998, 'b': 2.75e-5, 'c': -0.6931, 'd': -0.001219},
'M6': {'a': 0.8858, 'b': 6.203e-6, 'c': -0.8957, 'd': -0.001959},
'M5': {'a': 98.45, 'b': 0.0023, 'c': -98.53, 'd': -0.0142},
},
'fw': {
'M7': {'a': 0.1959, 'b': 0.0001, 'c': -0.2020, 'd': -0.0026},
'M6': {'a': 0.9297, 'b': 2.51e-5, 'c': -0.9233, 'd': -0.002},
},
}
def _mag_bin(mag):
"""Map magnitude to the bin key used by the biexponential CDF tables."""
if mag >= 7.0:
return 'M7'
if mag >= 6.0:
return 'M6'
if mag >= 5.0:
return 'M5'
return None
[docs]class Moss2022SecondarySR(BaseSecondarySurfRup):
"""
P(d > 0) for distributed displacement on reverse faults.
Two methods are available:
* ``'simple'`` (default) - Eq. 5.5 / Table 5.3 (85th-percentile,
500 m-grid calibration).
* ``'biexp'`` - Eqs 5.6–5.7 / Tables 5.4–5.5 (magnitude-binned
biexponential CDF).
Distance ``r`` is received in **km** (adapter convention, matching
``ctx.r``). Eq. 5.5 operates in km directly; the biexponential
CDF (Eq. 5.7) converts to metres internally.
"""
def get_prob(self, r, rx, mag=None, method="simple", **kwargs):
"""
Parameters
----------
r : float or array-like
Distance from the principal fault trace in **km**.
rx : float or array-like
Signed cross-fault distance in km.
Positive = hanging wall, negative = footwall.
mag : float, optional
Magnitude (required for ``method='biexp'``).
method : str
``'simple'`` or ``'biexp'``.
Returns
-------
numpy.ndarray or float
P(d > 0) per site.
"""
r_arr = np.atleast_1d(np.asarray(r, dtype=float))
rx_arr = np.atleast_1d(np.asarray(rx, dtype=float))
hw_mask = rx_arr >= 0
method_l = method.lower()
if method_l == 'simple':
prob = np.where(
hw_mask,
np.exp(-_COEFF_SIMPLE['hw']['a'] * r_arr
+ _COEFF_SIMPLE['hw']['b']),
np.exp(-_COEFF_SIMPLE['fw']['a'] * r_arr
+ _COEFF_SIMPLE['fw']['b']),
)
prob = np.minimum(prob, 1.0)
elif method_l == 'biexp':
if mag is None:
raise ValueError("mag is required for method='biexp'")
mbin = _mag_bin(mag)
if mbin is None:
return np.zeros_like(r_arr)
prob = np.zeros_like(r_arr)
for side_label, mask in [('hw', hw_mask), ('fw', ~hw_mask)]:
if not np.any(mask):
continue
tbl = _COEFF_BIEXP[side_label]
if mbin not in tbl:
continue
c = tbl[mbin]
x = r_arr[mask] * 1000.0 # km → metres for Eq. 5.7
Fx = (c['a'] * np.exp(c['b'] * x)
+ c['c'] * np.exp(c['d'] * x))
prob[mask] = np.clip(1.0 - Fx, 0.0, 1.0)
else:
raise ValueError(
f"Invalid method '{method}'. Accept: simple, biexp")
return prob.squeeze()