Source code for openquake.hazardlib.gsim.abrahamson_bhasin_2020

# -*- coding: utf-8 -*-
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright (C) 2025 GEM Foundation
#
# OpenQuake 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.
#
# OpenQuake 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 OpenQuake. If not, see <http://www.gnu.org/licenses/>.
"""
Module exports: :class:`AbrahamsonBhasin2020`,
                :class:`AbrahamsonBhasin2020PGA`,
                :class:`AbrahamsonBhasin2020SA1`,
"""

import numpy as np
from openquake.hazardlib import const
from openquake.hazardlib.imt import SA, PGV, PGA 
from openquake.hazardlib.gsim.base import GMPE, registry
from openquake.hazardlib.contexts import get_mean_stds

not_verified = True

# Abrahamson & Bhasin (2020) conditional PGV (horizontal component)
# coefficients (Table 3.2)
AB20_COEFFS = {
    "general": {
        "a1": 5.39, "a2": 0.799, "a3": 0.654, "a4": 0.479, "a5": -0.062,
        "a6": -0.359, "a7": -0.134, "a8": 0.023, "phi": 0.29, "tau": 0.16,
        "sigma": 0.33
    },
    "pga-based": {
        "a1": 4.77, "a2": 0.738, "a3": 0.484, "a4": 0.275, "a5": -0.036,
        "a6": -0.332, "a7": -0.44, "a8": 0.0, "phi_1": 0.32, "phi_2": 0.42,
        "tau_1": 0.12, "tau_2": 0.26, "sigma_1": 0.34, "sigma_2": 0.49,
    },
    "sa1_based": {
        "a1": 4.80, "a2": 0.82, "a3": 0.55, "a4": 0.27, "a5": 0.054,
        "a6": -0.382, "a7": -0.21, "a8": 0.0, "phi_1": 0.28, "phi_2": 0.38,
        "tau_1": 0.12, "tau_2": 0.17, "sigma_1": 0.30, "sigma_2": 0.42,
    }
}


M1 = 5.0
M2 = 7.0


def _get_tref(ctx):
    """
    Magnitude-dependent conditioning period Tref (Table 3.1).
    """
    mval = float(np.asarray(ctx.mag).flat[0])

    bins  = np.array([3.5, 4.5, 5.5, 6.5, 7.5, 8.5], dtype=float)
    trefs = np.array([0.20, 0.28, 0.40, 0.95, 1.40, 2.80], dtype=float)

    idx = np.searchsorted(bins, mval, side="left")
    return float(trefs[min(idx, len(trefs) - 1)])


def _get_trilinear_magnitude_term(ctx: np.recarray, C: dict):
    """Magnitude-dependent slope term f1(M) of ln(PSA(tref)). 
    (see eq. 3.8)

    """
    f1 = np.empty_like(ctx.mag, dtype=float)
    m1 = (ctx.mag < 5.0)
    m2 = (ctx.mag >= 5.0) & (ctx.mag <= 7.5)
    m3 = ~(m1 | m2)
    f1[m1] = C["a2"]
    f1[m2] = C["a2"] + (C["a3"] - C["a2"]) * (ctx.mag[m2] - 5.0) / 2.5
    f1[m3] = C["a3"]
    return f1


def _tri_linear_stdev_term(M: np.ndarray, stdev1: float, stdev2: float):
    """Tri-linear interpolation between used in pga- and sa1-based
    models. 
    (see eq. 3.9)

    """
    out = np.empty_like(M, dtype=float)
    m_lo = (M < M1)
    m_md = (M >= M1) & (M <= M2)
    m_hi = (M > M2)
    out[m_lo] = stdev1
    out[m_md] = stdev1 + (stdev2 - stdev1) * (M[m_md] - M1) / (M2 - M1)
    out[m_hi] = stdev2
    return out


[docs]def get_mean_conditional_pgv( C: dict, ctx: np.recarray, mean_gms: dict, imt_key: str ): lnY = mean_gms[imt_key] f1 = _get_trilinear_magnitude_term(ctx, C) return ( C["a1"] + f1 * lnY + C["a4"] * (ctx.mag - 6.0) + C["a5"] * (8.5 - ctx.mag) ** 2 + C["a6"] * np.log(ctx.rrup + 5.0 * np.exp(0.4 * (ctx.mag - 6.0))) + (C["a7"] + C["a8"] * (ctx.mag - 5.0)) * np.log(ctx.vs30 / 425.0) )
[docs]def get_standard_deviations( C: dict, ctx: np.recarray, sigma_gms: dict, tau_gms: dict, phi_gms: dict, imt_key: str): f1 = _get_trilinear_magnitude_term(ctx, C) sigma_cond = sigma_gms[imt_key] if "sigma" in C: # general model sigma_pgv = np.full_like(sigma_cond, C["sigma"], dtype=float) tau_pgv = np.full_like(sigma_cond, C["tau"], dtype=float) phi_pgv = np.full_like(sigma_cond, C["phi"], dtype=float) else: # fixed-IMT variants M = np.asarray(ctx.mag, dtype=float) sigma_pgv = _tri_linear_stdev_term(M, C["sigma_1"], C["sigma_2"]) tau_pgv = _tri_linear_stdev_term(M, C["tau_1"], C["tau_2"]) phi_pgv = _tri_linear_stdev_term(M, C["phi_1"], C["phi_2"]) tau_cond = tau_gms.get(imt_key) phi_cond = phi_gms.get(imt_key) sigma = np.sqrt((f1 * sigma_cond) ** 2 + sigma_pgv ** 2) tau = np.sqrt((f1 * tau_cond) ** 2 + tau_pgv ** 2) if (tau_cond is not None and np.size(tau_cond) > 0) else tau_pgv phi = np.sqrt((f1 * phi_cond) ** 2 + phi_pgv ** 2) if (phi_cond is not None and np.size(phi_cond) > 0) else phi_pgv return sigma, tau, phi
[docs]class AbrahamsonBhasin2020(GMPE): """Implementation of a conditional GMPE of Abrahamson & Bhasin (2020) for Peak Ground Velocity (PGV) applicable to active shallow crustal earthquakes. This requires characterisation of the SA at reference period (which is magnitude-dependent), in addition to magnitude and vs30, to define PGV and propagate the associated uncertainty. It also includes single-period SA variants (e.g., PGA and SA(1.0)), designed for use with seismic design maps that typically provide SA values at only a few spectral periods. Abrahamson N, Bhasin S (2020) "Conditional Ground-Motion Model for Peak Ground Velocity for Active Crustal Regions.", PEER Report 2020/05, Pacific Earthquake Engineering Research Center Headquarters, University of California at Berkeley. (October 2010) """ DEFINED_FOR_TECTONIC_REGION_TYPE = const.TRT.ACTIVE_SHALLOW_CRUST DEFINED_FOR_INTENSITY_MEASURE_TYPES = {PGV} DEFINED_FOR_INTENSITY_MEASURE_COMPONENT = const.IMC.RotD50 DEFINED_FOR_STANDARD_DEVIATION_TYPES = { const.StdDev.TOTAL, const.StdDev.INTER_EVENT, const.StdDev.INTRA_EVENT } REQUIRES_SITES_PARAMETERS = {"vs30"} REQUIRES_RUPTURE_PARAMETERS = {"mag"} REQUIRES_DISTANCES = {"rrup"} non_verified = True def __init__(self, kind: str = "general", **gmpe_dict): if kind not in AB20_COEFFS: raise ValueError(f"Unknown AB20 kind '{kind}'. Choose from {list(AB20_COEFFS)}") [(gmpe_name, kw)] = gmpe_dict.pop("gmpe").items() self.gmpe = registry[gmpe_name](**kw) self.c = AB20_COEFFS[kind] self.kind = kind self.last_tref = None for attr in ("REQUIRES_SITES_PARAMETERS", "REQUIRES_RUPTURE_PARAMETERS", "REQUIRES_DISTANCES"): setattr(self, attr, set(getattr(self, attr, ())) | set(getattr(self.gmpe, attr, ())))
[docs] def compute(self, ctx: np.recarray, imts: list, mean: np.ndarray, sig: np.ndarray, tau: np.ndarray, phi: np.ndarray): non_pgv = [(i, imt) for i, imt in enumerate(imts) if str(imt) != "PGV"] if non_pgv: non_pgv_imts = [imt for _, imt in non_pgv] base_mean, base_sig, base_tau, base_phi = get_mean_stds( self.gmpe, ctx, non_pgv_imts ) for j, (i, _) in enumerate(non_pgv): mean[i] = base_mean[j] sig[i] = base_sig[j] tau[i] = base_tau[j] phi[i] = base_phi[j] for i, imt in enumerate(imts): if str(imt) != "PGV": continue if self.kind == "general": tref = _get_tref(ctx) self.last_tref = tref cond_imt = SA(tref) elif self.kind == "pga-based": cond_imt = PGA() else: cond_imt = SA(1.0) key = str(cond_imt) mean_gms, sigma_gms, tau_gms, phi_gms = get_mean_stds( self.gmpe, ctx, {cond_imt}, return_dicts=True ) lnpgv = get_mean_conditional_pgv(self.c, ctx, mean_gms, key) sigma_pgv, tau_pgv, phi_pgv = get_standard_deviations( self.c, ctx, sigma_gms=sigma_gms, tau_gms=tau_gms, phi_gms=phi_gms, imt_key=key, ) mean[i] = lnpgv sig[i] = sigma_pgv tau[i] = tau_pgv phi[i] = phi_pgv
[docs]class AbrahamsonBhasin2020PGA(AbrahamsonBhasin2020): DEFINED_FOR_TECTONIC_REGION_TYPE = const.TRT.ACTIVE_SHALLOW_CRUST DEFINED_FOR_INTENSITY_MEASURE_TYPES = {PGV} def __init__(self, **gmpe_dict): super().__init__(kind="pga-based", **gmpe_dict)
[docs]class AbrahamsonBhasin2020SA1(AbrahamsonBhasin2020): DEFINED_FOR_TECTONIC_REGION_TYPE = const.TRT.ACTIVE_SHALLOW_CRUST DEFINED_FOR_INTENSITY_MEASURE_TYPES = {PGV} def __init__(self, **gmpe_dict): super().__init__(kind="sa1_based", **gmpe_dict)