Source code for openquake.pfd.primary_surf_displ.takao2013

# -*- 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.primary_surf_displ.takao2013` implements the
model of Takao et al. (2013) in :class:`Takao2013PrimaryFD`.
"""

import numpy as np
from scipy.stats import gamma, norm, beta
from openquake.pfd.params import check_choice, check_style
from openquake.pfd.primary_surf_displ.base import BasePrimarySurfDispl



[docs]class Takao2013PrimaryFD(BasePrimarySurfDispl): """Principal fault-displacement model of Takao et al. (2013). Model of principal (reverse-faulting) fault displacement as a function of magnitude and normalized along-strike position. Takao, M., et al. (2013). Application of probabilistic fault displacement hazard analysis in Japan. The conditional AD/MD log10-normal distribution is integrated over a truncation range of ``mean ± n_sigma·sigma`` (in log10 space). The truncation level ``n_sigma`` defaults to 3 and may be overridden from the logic tree via ``[Takao2013PrimaryFD] n_sigma = <value>``. Model contract: DISPLACEMENT_DEFINITION = "principal", DISPLACEMENT_COMPONENT = "net" -- Takao et al. (2013, JAEE 13) model displacement on the principal fault only (their nu_p1 term, distributed faulting handled by the separate nu_d2 chain), normalised by the Wells & Coppersmith AD/MD which are net (resultant) slip measures; component per the summary in Valentini et al. (2025, Rev. Geophys.) Table 4. """ DISPLACEMENT_DEFINITION = "principal" DISPLACEMENT_COMPONENT = "net" _N_INTEGRATION = 1000 def __init__(self, n_sigma=3.0, norm_disp_type=None, style=None): """ :param n_sigma: truncation half-width of the AD/MD distribution. :param norm_disp_type: optional normalization type pinned by the logic-tree branch ('AD' or 'MD'); ``None`` defers to the ``get_prob`` call. :param style: optional faulting style declared by the logic-tree branch. The Takao et al. (2013) regressions pool Japanese events in single equations, so the value does not change the numbers; it is stored (validated against the global style vocabulary) as a declaration of the branch context. """ self.n_sigma = float(n_sigma) if self.n_sigma <= 0.0: raise ValueError(f"n_sigma must be positive; got {self.n_sigma}") self.norm_disp_type = check_choice( type(self).__name__, "norm_disp_type", norm_disp_type, frozenset(["AD", "MD"]), canon=lambda v: str(v).upper()) self.style = check_style(type(self).__name__, style) def get_prob(self, d, X_L_ratio, mag, norm_disp_type=None): """ Model of Takao et al. (2013) for the probability of exceeding threshold values of primary displacement [m] :param d: target displacement (scalar or array-like, shape (n_displacements,)) :param X_L_ratio: Ratio of distance from the closest rupture end to the total rupture length (scalar or array-like, shape (n_sites,)) :param mag: Earthquake magnitude (scalar) :param srl: Surface rupture length in km :param norm_disp_type: Normalization displacement type. Valid options are "AD" or "MD". :returns: Probability of exceeding target displacement (m), shape (n_displacements, n_sites). """ # Define the accepted Normalization displacement types accepted_version = ["AD", "MD"] # Use Wells and Coppersmith 1994 law to estimate the surface rupture length (srl) in km srl = 10 ** (-2.86 + 0.63 * mag) # Validate the style if norm_disp_type is None: norm_disp_type = self.norm_disp_type if norm_disp_type is None: raise ValueError( f"{type(self).__name__}: norm_disp_type must be given either " f"in the logic-tree branch or at call time") if norm_disp_type not in accepted_version: raise ValueError( f"Invalid style '{norm_disp_type}'. Accepted values are: {', '.join(accepted_version)}" ) # Convert inputs to numpy arrays d = np.atleast_1d(d) # Shape (n_displacements,) X_L_ratio = np.atleast_1d(X_L_ratio) # Shape (n_sites,) # Fold the raw along-strike position x/L in [0, 1] to the normalized # distance from the *closest* rupture end in [0, 0.5], which is what the # Takao et al. (2013) regression coefficients in get_prob_D_AD / # get_prob_D_MD are defined against (see the X_L_ratio docstring). Without # this fold the gamma mean would grow monotonically toward x/L = 1, # producing an unphysical along-strike ramp instead of a symmetric, # centre-peaked displacement profile. Mirrors Youngs2003PrimaryFD. r = X_L_ratio - np.floor(X_L_ratio) X_L_ratio = 0.5 - np.abs(r - 0.5) # Following the approach in Youngs2003, we need to establish truncation bounds if norm_disp_type == "AD": # Based on Wells and Coppersmith (1994) for average displacement log_mean = -4.80 + 0.69 * mag sigma = 0.36 elif norm_disp_type == "MD": # Takao et al. (2013) Eq. 9: their refit of the Wells & Coppersmith # (1994) maximum-displacement relation (constant term 0.3 larger) log_mean = -5.16 + 0.82 * mag sigma = 0.42 d_truncation = self.n_sigma # ±n_sigma # Truncation bounds in log10 space lower = 10 ** (log_mean - d_truncation * sigma) upper = 10 ** (log_mean + d_truncation * sigma) # Use logspace values for numerical integration logspace_vals = np.logspace(np.log10(lower), np.log10(upper), self._N_INTEGRATION) # Initialize output array: (n_displacements, n_sites) n_displacements = len(d) n_sites = len(X_L_ratio) prob_exceeding_d = np.zeros((n_displacements, n_sites)) # Vectorized integration over displacement values for disp in logspace_vals: D_NormD = d / disp # Shape (n_displacements,) # Reshape for broadcasting: (n_displacements, 1) and (1, n_sites) D_NormD_reshaped = D_NormD[:, np.newaxis] # Shape (n_displacements, 1) X_L_reshaped = X_L_ratio[np.newaxis, :] # Shape (1, n_sites) if norm_disp_type == "AD": p3 = self.get_prob_D_AD(D_NormD_reshaped, X_L_reshaped, srl) * self.get_prob_avg_displacement(disp, mag) prob_exceeding_d += p3 elif norm_disp_type == "MD": p3 = self.get_prob_D_MD(D_NormD_reshaped, X_L_reshaped, srl) * self.get_prob_max_displacement(disp, mag) prob_exceeding_d += p3 return prob_exceeding_d def get_prob_D_AD(self, D_AD, x_L_ratio, srl): """ Model of Takao et al. (2013) for the probability of normalized displacement :param D_AD: Normalized displacement (D/Avg_D) :param x_L_ratio: Ratio of distance from the closest rupture end to the total rupture length :param srl: Surface rupture length in km :returns: Probability of exceeding 'norm_disp' """ if srl < 10: a = 1.53 b = 0.58 else: a = np.exp(0.7 + 0.34 * x_L_ratio) b = np.exp(-1.4 + 1.82 * x_L_ratio) return 1. - gamma.cdf(D_AD, a, loc=0, scale=b) def get_prob_avg_displacement(self, target_ad, mag: float) -> float: """ Calculate the normalized normal pdf of log10(average displacement) based on magnitude using Wells and Coppersmith (1994) for all faulting :param target_ad: Average displacement in meters :param magnitude: Earthquake magnitude :returns: Probability of average displacement """ # Based on Wells and Coppersmith (1994) for average displacement log_mean = -4.80 + 0.69 * mag sigma = 0.36 d_truncation = self.n_sigma # ±n_sigma # Truncation bounds in log10 space lower = 10 ** (log_mean - d_truncation * sigma) upper = 10 ** (log_mean + d_truncation * sigma) prob_avg_displacement = norm.pdf(np.log10(target_ad), loc=log_mean, scale=sigma) # Normalizing the distribution with the same truncation bounds logspace_vals = np.logspace(np.log10(lower), np.log10(upper), self._N_INTEGRATION) prob = norm.pdf(np.log10(logspace_vals), loc=log_mean, scale=sigma) normalization_factor = sum(prob) return prob_avg_displacement / normalization_factor def get_prob_D_MD(self, D_MD, x_L_ratio, srl): """ Model of Takao et al. (2013) for the probability of normalized displacement :param D_MD: Normalized displacement (D/Max_D) :param x_L_ratio: Ratio of distance from the closest rupture end to the total rupture length :param srl: Surface rupture length in km :returns: Probability of exceeding 'norm_disp' """ if srl < 10: a = 0.91 b = 1.9 else: a = np.exp(0.7 - 0.87 * x_L_ratio) b = np.exp(2.3 - 3.84 * x_L_ratio) return 1. - beta.cdf(D_MD, a, b) def get_prob_max_displacement(self, target_md, mag: float) -> float: """ Calculate the log-normal pdf of maximum displacement based on magnitude using Wells and Coppersmith (1994) for all style of faultings :param target_md: Maximum displacement in meters :param magnitude: Earthquake magnitude :returns: Probability of maximum displacement """ # Takao et al. (2013) Eq. 9: their refit of the Wells & Coppersmith # (1994) maximum-displacement relation (constant term 0.3 larger) log_mean = -5.16 + 0.82 * mag sigma = 0.42 d_truncation = self.n_sigma # ±n_sigma # Strike-slip #log_mean = -7.03 + 1.03 * mag #sigma = 0.34 # Truncation bounds in log10 space lower = 10 ** (log_mean - d_truncation * sigma) upper = 10 ** (log_mean + d_truncation * sigma) prob_max_displacement = norm.pdf(np.log10(target_md), loc=log_mean, scale=sigma) # Normalizing the distribution with the same truncation bounds logspace_vals = np.logspace(np.log10(lower), np.log10(upper), self._N_INTEGRATION) prob = norm.pdf(np.log10(logspace_vals), loc=log_mean, scale=sigma) normalization_factor = sum(prob) return prob_max_displacement / normalization_factor