Source code for openquake.hazardlib.gsim.abrahamson_bhasin_2020

# -*- coding: utf-8 -*-
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright (C) 2025-2026 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

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
MAG_BINS = np.array([3.5, 4.5, 5.5, 6.5, 7.5, 8.5])


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, base_preds: dict, imt_key: str, ): f1 = _get_trilinear_magnitude_term(ctx, C) """ Returns (mean) conditioned PGV """ return ( C["a1"] + f1 * base_preds[imt_key]['mean'] + 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_sig( C: dict, ctx: np.recarray, base_preds: dict, imt_key: str): """ Returns sigma, tau and phi arrays for PGV """ f1 = _get_trilinear_magnitude_term(ctx, C) sigma_cond = base_preds[imt_key]['sig'] 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 = base_preds[imt_key]["tau"] phi_cond = base_preds[imt_key]["phi"] 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"} # GMPE not verified against an independent implementation non_verified = True # Conditional GMPE conditional = True kind = "general" REQUIRES_IMTS = [SA(0.2), SA(0.28), SA(0.40), SA(0.95), SA(1.4), SA(2.8)] # The conditioning periods are selected from the following (and few in # number) disrete periods based on the ctx's magnitude so we can just # specify these values here
[docs] def compute(self, ctx: np.recarray, base_preds: dict): """ Compute method for conditional GMPE applied within ModifiableGMPE. :param base_preds: Dictionary where each key is a string of an IMT and each value is a sub-dictionary. Each subdict has keys of "mean", "sigma", "tau" and "phi", with the values representing those computed using the underlying GMM (i.e. the values the conditional GMPE's values will be conditioned upon). This dictionary is built within the ModifiableGMPE's compute method. """ # Get conditioning IMT based on self.kind c = AB20_COEFFS[self.kind] if self.kind == "general": # Mag-dependent conditioning period Tref (Table 3.1) idx = np.searchsorted(MAG_BINS, ctx.mag[0], side="left") t_refs = np.array([imt.period for imt in self.REQUIRES_IMTS]) tref = t_refs[min(idx, len(t_refs) - 1)] cond_imt = SA(tref) elif self.kind == "pga_based": cond_imt = PGA() else: cond_imt = SA(1.0) lnpgv = get_mean_conditional_pgv(c, ctx, base_preds, str(cond_imt)) sigma_pgv, tau_pgv, phi_pgv = get_sig(c, ctx, base_preds, str(cond_imt)) return lnpgv, sigma_pgv, tau_pgv, phi_pgv
[docs]class AbrahamsonBhasin2020PGA(AbrahamsonBhasin2020): kind = "pga_based" REQUIRES_IMTS = [PGA()]
[docs]class AbrahamsonBhasin2020SA1(AbrahamsonBhasin2020): kind = "sa1_based" REQUIRES_IMTS = [SA(1.0)]