Source code for openquake.hazardlib.gsim.karimzadeh_azores_islands_2023

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

import os
import csv
import gzip
import numpy as np

from openquake.baselib.onnx import PicklableInferenceSession
from openquake.hazardlib.gsim.base import GMPE
from openquake.hazardlib import const
from openquake.hazardlib.imt import PGA, PGV, SA

_DATA_DIR = os.path.join(
    os.path.dirname(__file__),
    "karimzadeh_azores_2023_data",
)

# Standard deviations file
_STDS_FILE = os.path.join(_DATA_DIR, "stds.csv")
# ONNX model handling (Single bundled model)
_BUNDLED_MODEL_NAME = "karimzadeh_azores_islands_2023_bundled.onnx.gz"

# Period to column index mapping, as learned from models.predict().
# Index 0: PGA, Index 1: PGV, Index 2+: SA periods
_PERIOD_TO_INDEX = {
    0.03: 2, 0.05: 3, 0.075: 4, 0.1: 5, 0.15: 6, 0.2: 7, 0.25: 8,
    0.3: 9, 0.4: 10, 0.5: 11, 0.75: 12, 1.0: 13, 1.5: 14, 2.0: 15
}

def _predict_im(sess: PicklableInferenceSession, X: np.ndarray) -> np.ndarray:
    """
    Evaluate the bundled ONNX model and return outputs for all IMTs
    (shape: N x 16).
    """
    input_name = sess.get_inputs()[0].name
    # Access underlying session to get outputs
    out_name = sess.inference_session.get_outputs()[0].name
    out = sess.run([out_name], {input_name: X.astype(np.float32)})[0]
    return np.asarray(out, dtype=float)

def _mean_and_std_from_ctx(gsim, ctx, imt):
    Mw = np.asarray(ctx.mag, dtype=float)
    RJB = np.asarray(ctx.rjb, dtype=float)
    hypo_depth = np.asarray(ctx.hypo_depth, dtype=float)

    # Order expected by the model: [Mw, RJB, Focal Depth]
    X = np.column_stack([Mw, RJB, hypo_depth])

    if imt.string == "PGA":
        kind = "PGA"
        key = "ln(PGA)"
        index = 0
    elif imt.string == "PGV":
        kind = "PGV"
        key = "ln(PGV)"
        index = 1
    elif imt.period:
        period = float(imt.period)
        kind = "SA"
        key = f"ln(PSA={period})"
        if period not in _PERIOD_TO_INDEX:
            raise ValueError(f"Period {period} not supported in Karimzadeh2023Azores.")
        index = _PERIOD_TO_INDEX[period]
    else:
        raise ValueError(f"IMT {imt} not supported in Karimzadeh2023Azores.")

    # Predict all IMTs at once (N, 16).
    all_outputs = _predict_im(gsim.session, X)
    
    # Extract the requested IMT
    ln_im_train = all_outputs[:, index]

    # Outputs are already in ln(cm/s^2) and ln(cm/s).
    if kind in ("PGA", "SA"):
        # Convert ln(cm/s^2) to ln(g)
        ln_im = ln_im_train - np.log(981.0)
    else:
        # PGV remains in ln(cm/s)
        ln_im = ln_im_train

    # Stds from stds.csv
    sigma_val = gsim.sigma_intra[key]
    tau_val   = gsim.tau_inter[key]
    phi_val   = gsim.phi_total[key]

    sigma_intra = np.full_like(ln_im, sigma_val)
    tau_inter   = np.full_like(ln_im, tau_val)
    phi_total   = np.full_like(ln_im, phi_val)

    return ln_im, sigma_intra, tau_inter, phi_total

[docs]class Karimzadeh2023Azores(GMPE): """ Machine-learning Ground-Motion Model for Azores Islands (Karimzadeh et al., 2023). Reference --------- Karimzadeh S, Mohammadi A, Salahuddin U, Carvalho A, Lourenço PB. Backbone ground motion model through simulated records and XGBoost machine learning algorithm: An application for the Azores plateau (Portugal). Earthquake Engineering & Structural Dynamics. 2024 Feb; 53(2):668-93. https://doi.org/10.1002/eqe.4040 """ DEFINED_FOR_TECTONIC_REGION_TYPE = const.TRT.ACTIVE_SHALLOW_CRUST DEFINED_FOR_INTENSITY_MEASURE_TYPES = {PGA, PGV, SA} DEFINED_FOR_INTENSITY_MEASURE_COMPONENT = const.IMC.GEOMETRIC_MEAN DEFINED_FOR_STANDARD_DEVIATION_TYPES = { const.StdDev.INTRA_EVENT, const.StdDev.INTER_EVENT, const.StdDev.TOTAL, } DEFINED_FOR_REFERENCE_VELOCITY = 760.0 REQUIRES_RUPTURE_PARAMETERS = {"mag", "hypo_depth"} REQUIRES_DISTANCES = {"rjb"} REQUIRES_SITES_PARAMETERS = set() def __init__(self, **kwargs): super().__init__(**kwargs) self.sigma_intra = {} self.tau_inter = {} self.phi_total = {} if not os.path.exists(_STDS_FILE): raise IOError(f"Cannot find stds.csv at {_STDS_FILE}") with open(_STDS_FILE, newline="") as f: reader = csv.DictReader(f) for row in reader: key = row["ID"] self.sigma_intra[key] = float(row["Sigma"]) self.tau_inter[key] = float(row["Tau"]) self.phi_total[key] = float(row["Phi"]) model_path = os.path.join(_DATA_DIR, _BUNDLED_MODEL_NAME) with gzip.open(model_path, "rb") as f: model_bytes = f.read() self.session = PicklableInferenceSession(model_bytes)
[docs] def compute(self, ctx: np.recarray, imts, mean, sig, tau, phi): """ Compute method for Karimzadeh2023Azores GSIM. """ for m, imt in enumerate(imts): ln_im, sigma_intra, tau_inter, phi_total = _mean_and_std_from_ctx( self, ctx, imt ) mean[m, :] = ln_im sig[m, :] = phi_total # TOTAL tau[m, :] = tau_inter # INTER-EVENT phi[m, :] = sigma_intra # INTRA-EVENT