Source code for openquake.hazardlib.gsim.sgobba_2020

# -*- coding: utf-8 -*-
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright (C) 2020, 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 :mod:`openquake.hazardlib.gsim.sgobba_2020` implements
:class:`~openquake.hazardlib.gsim.sgobba_2020.SgobbaEtAl2020`
"""

import os
import copy
import numpy as np
import pandas as pd
from scipy.constants import g as gravity_acc
from scipy.spatial import cKDTree
from openquake.hazardlib.geo import Point, Polygon
from openquake.hazardlib import const
from openquake.hazardlib.geo.mesh import Mesh
from openquake.hazardlib.imt import PGA, SA
from openquake.hazardlib.gsim.base import GMPE, CoeffsTable

# From http://www.csgnetwork.com/degreelenllavcalc.html
LEN_1_DEG_LAT_AT_43pt5 = 111.10245
LEN_1_DEG_LON_AT_43pt5 = 80.87665

DATA_FOLDER = os.path.join(os.path.dirname(__file__), 'sgobba_2020')

REGIONS = {
    '1': [[13.37, 42.13], [13.60, 42.24], [13.48, 42.51], [13.19, 42.36]],
    '4': [[13.26, 42.41], [13.43, 42.49], [13.27, 43.02], [12.96, 42.86]],
    '5': [[13.03, 42.90], [13.21, 42.99], [13.10, 43.13], [12.90, 43.06]]}

CONSTS = {'Mh': 5.0,
          'Rref': 1.0,
          'PseudoDepth': 6.0}


def _get_cluster_correction(dat, C, ctx, imt):
    """
    Get cluster correction. The use can specify various options through
    the cluster parameter. The available options are:
    - cluster = None
        In this case the code finds the most appropriate correction using
        the rupture position
    - cluster = 0
        No cluster correction
    - cluser = 1 or 4 or 5
        The code uses the correction for the given cluster
    """
    cluster = dat.cluster
    shape = ctx.sids.shape
    correction = np.zeros_like(shape)
    # st.dev.
    tau_L2L = np.zeros(shape)
    Bp_model = np.zeros(shape)
    phi_P2P = np.zeros(shape)

    # No cluster correction
    if cluster == 0:
        tau_L2L = C['tau_L2L']
        phi_P2P = C['phi_P2P']
        return correction, tau_L2L, Bp_model, phi_P2P
    # the code finds the most appropriate correction
    if cluster is None:
        mesh = Mesh(np.array([ctx.hypo_lon]), np.array([ctx.hypo_lat]))
        # midp = ctx.surface.get_middle_point()
        # mesh = Mesh(np.array([midp.longitude]),np.array([midp.latitude]))

        for key in REGIONS:
            coo = np.array(REGIONS[key])
            pnts = [Point(lo, la) for lo, la in zip(coo[:, 0], coo[:, 1])]
            poly = Polygon(pnts)
            within = poly.intersects(mesh)
            if within.all():
                cluster = int(key)
                break
    # if OUT clusters do not apply corrections
    if cluster is None:
        tau_L2L = C['tau_L2L']
        phi_P2P = C['phi_P2P']
        return correction, tau_L2L, Bp_model, phi_P2P
    else:
        # if IN clusters apply corrections
        # Cluster coefficients
        fname = 'P_model_cluster{:d}.csv'.format(cluster)
        fname = os.path.join(DATA_FOLDER, fname)
        data = np.loadtxt(fname, delimiter=",", skiprows=1)
        # for st.dev.
        fname2 = 'beta_dP2P_cluster{:d}.csv'.format(cluster)
        fname2 = os.path.join(DATA_FOLDER, fname2)
        data2 = np.loadtxt(fname2, delimiter=",", skiprows=1)
        # Compute the coefficients
        correction = np.zeros(shape)
        per = imt.period
        for idx in np.unique(dat.idxs):
            tmp = data[int(idx)]
            correction[dat.idxs == idx] = np.interp(
                per, dat.PERIODS, tmp[0:5])
        # Adding L2L correction
        label = "dL2L_cluster{:d}".format(cluster)
        correction += C[label]
        # compute st.dev.
        for idx in np.unique(dat.idxs):
            tmp2 = data2[int(idx)]
            Bp_model[dat.idxs == idx] = np.interp(per, dat.PERIODS, tmp2[0:5])
        return correction, tau_L2L, Bp_model, phi_P2P


def _get_distance_term(C, mag, ctx):
    """
    Eq.3 - page 3
    """
    term1 = C['c1'] * (mag - C['mref']) + C['c2']
    tmp = np.sqrt(ctx.rjb**2 + CONSTS['PseudoDepth']**2)
    term2 = np.log10(tmp / CONSTS['Rref'])
    term3 = C['c3'] * (tmp - CONSTS['Rref'])
    return term1 * term2 + term3


def _get_magnitude_term(C, mag):
    """
    Eq.2 - page 3
    """
    return np.where(mag <= CONSTS['Mh'],
                    C['b1'] * (mag - CONSTS['Mh']),
                    C['b2'] * (mag - CONSTS['Mh']))


def _get_site_correction(data, shape, imt):
    """
    Get site correction
    """
    correction = np.zeros_like(shape)
    # Compute the coefficients
    correction = np.zeros(shape)
    # stand.dev.
    Bs_model = np.zeros(shape)
    phi_S2Sref = np.zeros(shape)
    per = imt.period
    for idx in np.unique(data.idxs):
        tmp = data.Smodel[int(idx)]
        correction[data.idxs == idx] = np.interp(
            per, data.PERIODS, tmp[0:5])
        tmp2 = data.betaS2S[int(idx)]
        Bs_model[data.idxs == idx] = np.interp(
            per, data.PERIODS, tmp2[0:5])
    return correction, Bs_model, phi_S2Sref


[docs]class Data(object): """Helper class""" def __init__(self, smodel, cluster, periods, betaS2S, idxs): self.Smodel = smodel self.cluster = copy.copy(cluster) self.PERIODS = periods self.betaS2S = betaS2S self.idxs = idxs
# NB: the implementation here is HORRIBLE performance-wise, # because it is using KDTree and pandas!
[docs]class SgobbaEtAl2020(GMPE): """ Implements the GMM proposed by Sgobba et al. (2020). Warning: This GMM is not meant for national models where it would be too slow, it is meant for scenario calculations. :param event_id: A string identifying an event amongst the ones comprised in the list available in the file `event.csv` :param directionality: A boolean :param cluster: If set to 'None', the OQ Engine finds the corresponding cluster using the rupture epicentral location. If cluster=0, no cluster correction applied. Otherwise, if an integer ID is provided, that corresponds to the cluster id (available cluster indexes are 1, 4 and 5), the corresponding correction id applied. """ #: Supported tectonic region type is 'active shallow crust' DEFINED_FOR_TECTONIC_REGION_TYPE = const.TRT.ACTIVE_SHALLOW_CRUST #: Set of :mod:`intensity measure types <openquake.hazardlib.imt>` #: this GSIM can calculate. A set should contain classes from module #: :mod:`openquake.hazardlib.imt`. DEFINED_FOR_INTENSITY_MEASURE_TYPES = {PGA, SA} #: Supported intensity measure component is the geometric mean of two #: horizontal components DEFINED_FOR_INTENSITY_MEASURE_COMPONENT = const.IMC.GEOMETRIC_MEAN #: Supported standard deviation types are inter-event, intra-event #: and total DEFINED_FOR_STANDARD_DEVIATION_TYPES = { const.StdDev.TOTAL, const.StdDev.INTER_EVENT, const.StdDev.INTRA_EVENT} #: Required site parameters are lon and lat REQUIRES_SITES_PARAMETERS = {'lon', 'lat'} #: Required rupture parameters is magnitude, hypo_lat, hypo_lon REQUIRES_RUPTURE_PARAMETERS = {'mag', 'hypo_lon', 'hypo_lat'} #: Required distance measure is Rjb REQUIRES_DISTANCES = {'rjb'} PERIODS = np.array([0, 0.2, 0.50251256281407, 1.0, 2.0]) def __init__(self, event_id=None, directionality=False, cluster=None, site=False, bedrock=False, **kwargs): super().__init__(event_id=event_id, directionality=directionality, cluster=cluster, **kwargs) self.event_id = event_id self.directionality = directionality self.cluster = cluster self.site = site self.bedrock = bedrock # Get site indexes. They are used for the site correction and the # cluster (path) correction # Load the coordinate of the grid fname = os.path.join(DATA_FOLDER, 'grid.csv') coo = np.fliplr(np.loadtxt(fname, delimiter=";")) self.kdt = cKDTree(coo) # Load the table with the between event coefficients if event_id is not None: self.event_id = event_id fname = os.path.join(DATA_FOLDER, 'event.csv') # Create dataframe with events df = pd.read_csv(fname, dtype={'id': str}) df.set_index('id', inplace=True) self.df = df assert event_id in df.index.values # Site coefficients fname = os.path.join(DATA_FOLDER, "S_model.csv") self.Smodel = np.loadtxt(fname, delimiter=",", skiprows=1) fname = os.path.join(DATA_FOLDER, "beta_dS2S.csv") self.betaS2S = np.loadtxt(fname, delimiter=",", skiprows=1)
[docs] def compute(self, ctx: np.recarray, imts, mean, sig, tau, phi): """ Eq.1 - page 2 """ for m, imt in enumerate(imts): # Ergodic coeffs C = self.COEFFS[imt] # between-event if self.event_id is not None: label = "dBe_{:s}".format(str(imt)) self.be = self.df.loc[self.event_id][label] self.be_std = 0.0 else: self.be_std = C['tau_ev'] self.be = 0.0 # Site correction points = np.array([ctx.lon, ctx.lat]).T # shape (N, 2) dsts, idxs = self.kdt.query(points) dat = Data(self.Smodel, self.cluster, self.PERIODS, self.betaS2S, idxs) sc = 0 phi_S2Sref = C['phi_S2S_ref'] Bs_model = np.zeros(ctx.sids.shape) if self.site and self.bedrock is False: sc, Bs_model, phi_S2Sref = _get_site_correction( dat, ctx.sids.shape, imt) cc, tau_L2L, Bp_model, phi_P2P = _get_cluster_correction( dat, C, ctx, imt) # Get mean mean[m] = (C['a'] + _get_magnitude_term(C, ctx.mag) + _get_distance_term(C, ctx.mag, ctx) + sc + cc + self.be) # To natural logarithm and fraction of g mean[m] = np.log(10.0**mean[m] / (gravity_acc*100)) # Get stds std = np.sqrt(C['sigma_0'] ** 2 + self.be_std ** 2 + tau_L2L ** 2 + Bs_model + phi_S2Sref ** 2 + Bp_model + phi_P2P ** 2) sig[m] = np.log(10.0 ** std) tau[m] = np.log(10.0 ** self.be_std) phi[m] = np.log(10.0 ** np.sqrt((std ** 2 - self.be_std ** 2)))
COEFFS = CoeffsTable(sa_damping=5., table="""\ IMT a b1 b2 c1 c2 c3 mref tau_ev tau_L2L phi_S2S_ref phi_S2S phi_P2P sigma_0 dL2L_cluster1 dL2L_cluster4 dL2L_cluster5 pga 2.92178299969904 0.549352522898805 0.195787600661646 0.182324348626393 -1.56833817017883 -0.00277072348000775 3.81278167434967 0.148487352282019 0.0244729216674648 0.2011 0.249446934633114 0.129067143622624 0.194457660872416 -0.0144001959308384 -0.0141006684390188 -0.0814912207894038 0.2 3.23371734550753 0.718110435825521 0.330819511910566 0.101391376086178 -1.47499081134392 -0.00235944669544279 3.52085298608413 0.142257128473462 0.0142903160948886 0.2049 0.25356556670542 0.108002263739343 0.211634711188527 -0.0295313493684611 -0.0242995747709838 -0.0779761977475732 0.50251256281407 3.16050217205595 0.838494386998919 0.466787811642044 0.105723089676 -1.48056328666322 0 4.87194107479204 0.118125529818761 0.0131546256192079 0.1419 0.230442366433058 0.0959080118602377 0.191012833298469 0.00929098048437728 -0.00995372305434456 -0.00828100722167989 1 2.58227846237728 0.85911311807545 0.519131261495525 0.146088352194266 -1.28019118368202 0 5.42555199253122 0.124229747688977 2.39299038437967e-08 0.1153 0.212408309867869 0.118568732468557 0.176037658051544 0.000737173026444824 -0.00123578210338215 0.000181351036566464 2 1.88792168738756 0.727248116061721 0.47362977053987 0.244695132922949 -1.19816952711971 0 5.26896508895249 0.127711124129548 8.69064652723658e-09 0.1078 0.189154038588083 0.119572905421336 0.183045950697286 5.60984632803441e-15 -1.18288330352055e-14 9.31778101896791e-15 """)