# -*- coding: utf-8 -*-
"""
Mohammadi et al. (2023) Turkiye ML-based Ground-Motion Model
"""
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
# MinMaxScaler parameters (from scx.pkl)
_DATA_MIN = np.array([4.1, 131.0, 0.1, 0.0, 0.0, 0.0], dtype=float)
_DATA_MAX = np.array([7.6, 1380.0, 199.0, 1.0, 1.0, 1.0], dtype=float)
_SCALE = np.array(
[
0.17142857142857146,
0.00048038430744595686,
0.00301659125188537,
0.6000000000000001,
0.6000000000000001,
0.6000000000000001,
],
dtype=float,
)
_MIN = np.array(
[-0.5028571428571429, 0.13706965572457966, 0.19969834087481148, 0.2, 0.2, 0.2],
dtype=float,
)
# Data directory
_DATA_DIR = os.path.join(
os.path.dirname(__file__),
"mohammadi_turkiye_2023_data",
)
# Standard deviations file
_STDS_FILE = os.path.join(_DATA_DIR, "stds.csv")
# ONNX model handling (Single bundled model)
_BUNDLED_MODEL_NAME = "mohammadi_turkiye_2023_bundled.onnx.gz"
def _scale_features(X: np.ndarray) -> np.ndarray:
"""
Apply the *same* feature scaling that was used in training (scx.pkl).
Parameters
----------
X : np.ndarray, shape (N, 6)
Columns in the following order:
[Mw, Vs30, RJB, normal, reverse, strike_slip]
Returns
-------
X_scaled : np.ndarray, shape (N, 6)
MinMax-scaled features, consistent with the original scx.pkl:
X_scaled = X * SCALE + MIN
Notes
-----
- All 6 features are scaled simultaneously using the pre-extracted
SCALE and MIN parameters.
- This matches the behaviour of the original MinMaxScaler used
when training the XGBoost models.
"""
return X * _SCALE + _MIN
def _predict_ln_im(output_name: str, X_scaled: np.ndarray,
sess: PicklableInferenceSession) -> np.ndarray:
"""
Evaluate the bundled ONNX model and return ln(IM) in training units.
"""
# Input name is shared and fixed by bundling script
# It was 'float_input' in all original models.
input_name = sess.get_inputs()[0].name # Should be 'float_input'
# Run inference only for the requested output
out = sess.run([output_name], {input_name: X_scaled})[0]
return np.asarray(out, dtype=float).reshape(-1)
def _mean_and_std_from_ctx(gsim, ctx, imt):
"""
Core prediction logic for a single IMT, using the vectorized `ctx`
(numpy.recarray) used by modern OpenQuake.
Parameters
----------
gsim : Mohammadi2023Turkiye instance
ctx : numpy.recarray
Must provide fields: mag, rake, rjb, vs30
imt : IMT instance (PGA, PGV, SA(...))
Returns
-------
ln_im : np.ndarray, shape (N,)
ln(IM) in OQ units:
- ln(PGA), ln(SA) in g
- ln(PGV) in cm/s
sigma_intra : np.ndarray, shape (N,)
Intra-event stddev (σ_intra)
tau_inter : np.ndarray, shape (N,)
Inter-event stddev (τ)
phi_total : np.ndarray, shape (N,)
Total stddev (σ_total)
"""
N = len(ctx)
Mw = np.asarray(ctx.mag, dtype=float)
RJB = np.asarray(ctx.rjb, dtype=float)
Vs30 = np.asarray(ctx.vs30, dtype=float)
rake = np.asarray(ctx.rake, dtype=float)
# Fault mechanism flags (vectorized)
normal = np.zeros(N, dtype=float)
reverse = np.zeros(N, dtype=float)
strike_slip = np.zeros(N, dtype=float)
normal[rake < -30.0] = 1.0
reverse[rake > 30.0] = 1.0
strike_slip[(rake >= -30.0) & (rake <= 30.0)] = 1.0
# Order: Mw, Vs30, RJB, normal, reverse, strike_slip
X = np.column_stack([Mw, Vs30, RJB, normal, reverse, strike_slip])
X_scaled = _scale_features(X).astype(np.float32)
# IMT identification
if imt.string == "PGA":
kind, period = "PGA", None
key = "ln(PGA)"
elif imt.string == "PGV":
kind, period = "PGV", None
key = "ln(PGV)"
elif imt.period:
period = float(imt.period)
kind = "SA"
key = f"ln(PSA={period})"
else:
try:
imt_str = str(imt)
except Exception:
imt_str = repr(imt)
raise ValueError(f"IMT {imt_str} not supported in Mohammadi2023Turkiye")
# Prediction in training units, then convert to OQ units
ln_im_train = _predict_ln_im(key, X_scaled, gsim.session)
if kind in ("PGA", "SA"):
# cm/s^2 -> g (still in ln)
ln_im = ln_im_train - np.log(981.0)
else:
# PGV already in cm/s
ln_im = ln_im_train
# Standard deviations from stds.csv (all in ln-units)
# Sigma -> intra-event
# Tau -> inter-event
# Phi -> total
sigma_val = gsim.sigma_intra[key] # intra-event
tau_val = gsim.tau_inter[key] # inter-event
phi_val = gsim.phi_total[key] # total
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 Mohammadi2023Turkiye(GMPE):
"""
Machine-learning Ground-Motion Model for Turkiye (Mohammadi et al., 2023).
Reference
---------
Mohammadi A, Karimzadeh S, Banimahd SA, Ozsarac V, Lourenço PB (2023).
"The potential of region-specific machine-learning-based ground motion models:
application to Turkiye."
Soil Dynamics and Earthquake Engineering, 172:108008.
https://doi.org/10.1016/j.soildyn.2023.108008
"""
# Tectonic region
DEFINED_FOR_TECTONIC_REGION_TYPE = const.TRT.ACTIVE_SHALLOW_CRUST
# Intensity measure types
DEFINED_FOR_INTENSITY_MEASURE_TYPES = {PGA, PGV, SA}
# Component: geometric mean or closest available
DEFINED_FOR_INTENSITY_MEASURE_COMPONENT = const.IMC.GEOMETRIC_MEAN
# Std dev types – order doesn't matter; mapping is defined in code.
DEFINED_FOR_STANDARD_DEVIATION_TYPES = {
const.StdDev.INTRA_EVENT,
const.StdDev.INTER_EVENT,
const.StdDev.TOTAL,
}
# Reference velocity (not really used but required by some tools)
DEFINED_FOR_REFERENCE_VELOCITY = 760.0
# Required contexts
REQUIRES_RUPTURE_PARAMETERS = {"mag", "rake"}
REQUIRES_DISTANCES = {"rjb"}
REQUIRES_SITES_PARAMETERS = {"vs30"}
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 Mohammadi et al. 2023 GSIM.
"""
for m, imt in enumerate(imts):
ln_im, sigma_intra, tau_inter, phi_total = _mean_and_std_from_ctx(
self, ctx, imt
)
# Fill rows for IMT m
mean[m, :] = ln_im
sig[m, :] = phi_total # TOTAL
tau[m, :] = tau_inter # INTER-EVENT
phi[m, :] = sigma_intra # INTRA-EVENT