Source code for openquake.hazardlib.calc.gmf

# -*- coding: utf-8 -*-
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright (C) 2012-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 :mod:`~openquake.hazardlib.calc.gmf` exports
:func:`ground_motion_fields`.
"""
import logging

import numpy as np
import pandas
from scipy import special

from openquake.baselib import config
from openquake.baselib.general import AccumDict, humansize
from openquake.baselib.performance import Monitor, compile
from openquake.hazardlib.const import StdDev
from openquake.hazardlib.source.rupture import EBRupture, get_eid_rlz
from openquake.hazardlib.correlation_models.cross_imt.no_cross_correlation \
    import NoCrossCorrelation
from openquake.hazardlib.correlation_models.base import (
    CorrelationContext, CrossIMTCorrelationModel, ResidualComponent,
    SpatialCorrelationModel)
from openquake.hazardlib.correlation_models.circulant_embedding import (
    CirculantEmbeddingFactor, RegularGridLayout)
from openquake.hazardlib.contexts import ContextMaker, FarAwayRupture
from openquake.hazardlib.imt import from_string

U8 = np.uint8
U16 = np.uint16
U32 = np.uint32
I64 = np.int64
F32 = np.float32
TRUNCATION_THRESHOLD = 1E-9
CE_MIN_SITES = 1_000


def _correlation_budget():
    """Return the configured per-worker correlation workspace budget."""
    return int(float(config.memory.correlated_gmf_gb) * 1024 ** 3)


def _dense_correlation_bytes(model, sites, num_imts):
    """Estimate peak bytes required by the existing dense factorization."""
    if isinstance(model, CrossIMTCorrelationModel):
        return 3 * num_imts ** 2 * 8
    if isinstance(model, SpatialCorrelationModel):
        num_sites = len(sites.complete)
        matrices = num_imts + 2
        return matrices * num_sites ** 2 * 8
    dimension = num_imts * len(sites)
    return 3 * dimension ** 2 * 8


def _site_positions(complete, selected):
    """Return positions of selected site IDs in the complete collection."""
    complete_sids = np.asarray(complete.sids)
    selected_sids = np.asarray(selected.sids)
    order = np.argsort(complete_sids)
    sorted_sids = complete_sids[order]
    positions = np.searchsorted(sorted_sids, selected_sids)
    if (np.any(positions == len(sorted_sids)) or
            np.any(sorted_sids[positions] != selected_sids)):
        raise ValueError('Affected sites are absent from the complete grid')
    return order[positions]


def _truncated_normals(shape, level, rng):
    """Draw truncated standard normals with one in-place work array."""
    samples = rng.random(shape)
    lower = special.ndtr(-level)
    samples *= special.ndtr(level) - lower
    samples += lower
    special.ndtri(samples, out=samples)
    return samples


[docs]class CorrelationButNoInterIntraStdDevs(Exception): def __init__(self, corr, gsim): self.corr = corr self.gsim = gsim def __str__(self): return '''\ You cannot use the correlation model %s with the GSIM %s, \ that defines only the total standard deviation. If you want to use a \ correlation model you have to select a GMPE that provides the inter and \ intra event standard deviations.''' % ( self.corr.__class__.__name__, self.gsim.__class__.__name__)
[docs]@compile(["(float32[:,:], boolean)", "(float32[:], boolean)", "(float64, boolean)"]) def exp(vals, notMMI): """ Exponentiate the values unless the IMT is MMI """ if notMMI: return np.exp(vals) return vals
[docs]@compile("(float32[:,:,:],float32[:,:],float64[:],float64[:],int64)") def set_max_min(array, mean, max_iml, min_iml, mmi_index): N, M, E = array.shape # manage max_iml for m in range(M): iml = max_iml[m] for n in range(N): # capping the gmv at the median value if val > max_iml[m] maxval = exp(mean[m, n], m != mmi_index) for e in range(E): val = array[n, m, e] if val > iml: array[n, m, e] = maxval # manage min_iml for n in range(N): for e in range(E): # set to zero only if all IMTs are below the thresholds if (array[n, :, e] < min_iml).all(): array[n, :, e] = 0
[docs]@compile("(uint32[:],uint32[:],uint32[:],uint32[:])") def build_eid_sid_rlz(allrlzs, sids, eids, rlzs): eid_sid_rlz = np.zeros((3, len(sids) * len(eids)), U32) idx = 0 for rlz in allrlzs: for eid in eids[rlzs == rlz]: for sid in sids: eid_sid_rlz[0, idx] = eid eid_sid_rlz[1, idx] = sid eid_sid_rlz[2, idx] = rlz idx += 1 return eid_sid_rlz
[docs]def calc_gmf_simplified(ebrupture, sitecol, cmaker): """ A simplified version of the GmfComputer for event based calculations. Used only for pedagogical purposes. Here is an example of usage: from unittest.mock import Mock import numpy from openquake.hazardlib import valid, contexts, site, geo from openquake.hazardlib.source.rupture import EBRupture, build_planar from openquake.hazardlib.calc.gmf import calc_gmf_simplified, GmfComputer imts = ['PGA'] rlzs = np.arange(3, dtype=np.uint32) rlzs_by_gsim = {valid.gsim('BooreAtkinson2008'): rlzs} lons = [0., 0.] lats = [0., 1.] siteparams = Mock(reference_vs30_value=760.) sitecol = site.SiteCollection.from_points(lons, lats, sitemodel=siteparams) hypo = geo.point.Point(0, .5, 20) rup = build_planar(hypo, mag=7., rake=0.) cmaker = contexts.simple_cmaker(rlzs_by_gsim, imts, truncation_level=3.) ebr = EBRupture(rup, 0, 0, n_occ=2, id=1) ebr.seed = 42 print(cmaker) print(sitecol.array) print(ebr) gmfa = calc_gmf_simplified(ebr, sitecol, cmaker) print(gmfa) # numbers considering the full site collection sites = site.SiteCollection.from_points([0], [1], sitemodel=siteparams) gmfa = calc_gmf_simplified(ebr, sites, cmaker) print(gmfa) # different numbers considering half of the site collection """ N = len(sitecol) M = len(cmaker.imtls) [ctx] = cmaker.get_ctxs([ebrupture.rupture], sitecol) mean, _sig, tau, phi = cmaker.get_mean_stds([ctx]) # shapes (G, M, N) rlzs = np.concatenate(list(cmaker.gsims.values())) _eid, rlz = get_eid_rlz(vars(ebrupture), rlzs, False) rng = np.random.default_rng(ebrupture.seed) between_correl = NoCrossCorrelation(cmaker.truncation_level_between) within_dist = NoCrossCorrelation( cmaker.truncation_level_within).distribution gmfs = [] for g, (gs, rlzs) in enumerate(cmaker.gsims.items()): idxs, = np.where(np.isin(rlz, rlzs)) E = len(idxs) # build arrays of random numbers of shape (M, N, E) and (M, E) within_eps = [within_dist.rvs((N, E), rng).astype(F32) for _ in range(M)] eps = np.zeros((E, M), F32) eps[idxs] = between_correl.get_inter_eps(cmaker.imtls, E, rng).T gmf = np.zeros((M, N, E)) for m, imt in enumerate(cmaker.imtls): within_res = phi[g, m, :, None] * within_eps # shape (N, E) between_res = tau[g, m, :, None] * eps[idxs, m] # shape (N, E) gmf[m] = np.exp(mean[g, m, :, None] + within_res + between_res) gmfs.append(gmf) return np.concatenate(gmfs) # shape (M, N, E)
[docs]class GmfComputer(object): """ Given an earthquake rupture, the GmfComputer computes ground shaking over a set of sites, by randomly sampling a ground shaking intensity model. :param rupture: EBRupture to calculate ground motion fields radiated from. :param :class:`openquake.hazardlib.site.SiteCollection` sitecol: a complete SiteCollection :param cmaker: a :class:`openquake.hazardlib.gsim.base.ContextMaker` instance :param within_event_model: Instance of a within-event correlation model object. See :mod:`openquake.hazardlib.correlation_models`. Can be ``None``, in which case non-correlated ground motion fields are calculated. Correlation model is not used if ``truncation_level`` is zero. :param between_event_model: Instance of a between-event correlation model object. See :mod:`openquake.hazardlib.correlation_models`. Can be ``None``, in which case non-cross-correlated ground motion fields are calculated. :param amplifier: None or an instance of Amplifier :param sec_perils: Tuple of secondary perils. See :mod:`openquake.hazardlib.sep`. Can be ``None``, in which case no secondary perils need to be evaluated. """ mtp_dt = np.dtype([('rup_id', I64), ('site_id', U32), ('gsim_id', U16), ('imt_id', U8), ('mea', F32), ('tau', F32), ('phi', F32)]) # The GmfComputer is called from the OpenQuake Engine. In that case # the rupture is an EBRupture instance containing a # :class:`openquake.hazardlib.source.rupture.Rupture` instance as an # attribute. Then the `.compute(gsim, num_events, ms)` method is called and # a matrix of size (M, N, E) is returned, where M is the number of # IMTs, N the number of affected sites and E the number of events. The # seed is extracted from the underlying rupture. def __init__(self, rupture, sitecol, cmaker, within_event_model=None, between_event_model=None, amplifier=None, sec_perils=(), **legacy): if 'correlation_model' in legacy: if within_event_model is not None: raise TypeError('Pass only within_event_model') within_event_model = legacy.pop('correlation_model') if 'cross_correl' in legacy: if between_event_model is not None: raise TypeError('Pass only between_event_model') between_event_model = legacy.pop('cross_correl') if legacy: raise TypeError('Unknown arguments: %s' % sorted(legacy)) if len(sitecol) == 0: raise ValueError('No sites') elif len(cmaker.imtls) == 0: raise ValueError('No IMTs') elif len(cmaker.gsims) == 0: raise ValueError('No GSIMs') self.cmaker = cmaker self.imts = [from_string(imt) for imt in cmaker.imtls] self.cmaker = cmaker self.gsims = sorted(cmaker.gsims) self.within_event_model = within_event_model self.amplifier = amplifier self.sec_perils = sec_perils self.ebrupture = rupture self.rup_id = rupture.id self.seed = rupture.seed rupture = rupture.rupture # the underlying rupture ctxs = list(cmaker.get_ctxs([rupture], sitecol)) if not ctxs: raise FarAwayRupture [self.ctx] = ctxs self.N = len(self.ctx) if within_event_model: # store the filtered sitecol self.sites = sitecol.complete.filtered(self.ctx.sids) within_event_model.validate_imts(self.imts) self.between_event_model = between_event_model or NoCrossCorrelation( cmaker.truncation_level_between) self.between_event_model.validate_imts(self.imts) self.correlation_context = CorrelationContext( mag=rupture.mag, rake=getattr(rupture, 'rake', None), trt=cmaker.trt) self._within_event_factor = None self._ce_factor = None self._ce_checked = False self.within_dist = NoCrossCorrelation( cmaker.truncation_level_within).distribution self.mea_tau_phi = [] self.gmv_fields = [str(imt) for imt in cmaker.imts] self.mmi_index = -1 for m, imt in enumerate(cmaker.imtls): if imt == 'MMI': self.mmi_index = m @property def correlation_model(self): """Compatibility alias for :attr:`within_event_model`.""" return self.within_event_model @correlation_model.setter def correlation_model(self, model): self.within_event_model = model @property def cross_correl(self): """Compatibility alias for :attr:`between_event_model`.""" return self.between_event_model @cross_correl.setter def cross_correl(self, model): self.between_event_model = model
[docs] def init_eid_rlz_sig_eps(self): """ Initialize the attributes eid, rlz, sig, eps with shapes E, E, EM, EM """ self.rng = np.random.default_rng(self.seed) self.rlzs = np.concatenate(list(self.cmaker.gsims.values())) self.eid, self.rlz = get_eid_rlz( vars(self.ebrupture), self.rlzs, self.cmaker.scenario) self.E = E = len(self.eid) self.M = M = len(self.gmv_fields) self.sig = np.zeros((E, M), F32) # same for all events self.between_eps = np.zeros((E, M), F32) # not the same
[docs] def build_sig_eps(self, se_dt, event_indices=None): """ :returns: a structured array of size E with fields (eid, rlz_id, sig_inter_IMT, eps_inter_IMT) """ if event_indices is None: event_indices = np.arange(self.E) sig_eps = np.zeros(len(event_indices), se_dt) sig_eps['eid'] = self.eid[event_indices] sig_eps['rlz_id'] = self.rlz[event_indices] for m, imt in enumerate(self.cmaker.imtls): sig_eps[f'sig_inter_{imt}'] = self.sig[event_indices, m] sig_eps[f'eps_inter_{imt}'] = \ self.between_eps[event_indices, m] return sig_eps
[docs] def update(self, data, array, rlzs, mean, max_iml=None, event_indices=None): """ Updates the data dictionary with the values coming from the array of GMVs. Also indirectly updates the arrays .sig and .eps. """ min_iml = self.cmaker.min_iml mag = self.ebrupture.rupture.mag if max_iml is None: max_iml = np.full(self.M, np.inf, float) set_max_min(array, mean, max_iml, min_iml, self.mmi_index) data['gmv'].append(array) if self.sec_perils and event_indices is not None: for e in range(len(event_indices)): gmfa = array[:, :, e].T # shape (M, N) self._update_secondary(data, gmfa, mag) elif self.sec_perils: n = 0 for rlz in rlzs: eids = self.eid[self.rlz == rlz] E = len(eids) for e, _eid in enumerate(eids): gmfa = array[:, :, n + e].T # shape (M, N) self._update_secondary(data, gmfa, mag) n += E
def _update_secondary(self, data, gmfa, mag): """Append secondary-peril outputs for one event.""" for sp in self.sec_perils: outputs = sp.compute(mag, zip(self.imts, gmfa), self.ctx) for outkey, outarr in zip(sp.outputs, outputs): key = f'{sp.__class__.__name__}_{outkey}' if outkey == 'Disp': # Catarina says to ignore small displacements outarr[outarr < 1e-4] = 0 data[key].append(outarr)
[docs] def strip_zeros(self, data, event_indices=None): """ :returns: a DataFrame with the nonzero GMVs """ # building an array of shape (3, NE) if event_indices is None: eid_sid_rlz = build_eid_sid_rlz( self.rlzs, self.ctx.sids, self.eid, self.rlz) else: num_sites = len(self.ctx.sids) eids = self.eid[event_indices] rlzs = self.rlz[event_indices] eid_sid_rlz = np.array([ np.repeat(eids, num_sites), np.tile(self.ctx.sids, len(event_indices)), np.repeat(rlzs, num_sites)], dtype=U32) for key, val in sorted(data.items()): data[key] = np.concatenate(data[key], axis=-1, dtype=F32) gmv = data.pop('gmv') # shape (N, M, E) ok = gmv.sum(axis=1).T.reshape(-1) > 0 for m, gmv_field in enumerate(self.gmv_fields): data[gmv_field] = gmv[:, m].T.reshape(-1) # build dataframe df = pandas.DataFrame(data) df['eid'] = eid_sid_rlz[0] df['sid'] = eid_sid_rlz[1] df['rlz'] = eid_sid_rlz[2] # remove the rows with all zero values df = df[ok] # remove the rows with low intensity secondary perils to save # storage space (i.e. the computed seismic risk will be wrong) minimum = self.cmaker.oq.minimum_intensity for sec_imt in self.cmaker.oq.sec_imts: _col, imt = sec_imt.split('_') if imt in minimum: df = df[df[sec_imt] >= minimum[imt]] return df
[docs] @staticmethod def get_symmetric_bounds(cov_matrix, level): """ Calculates the lower and upper bound vectors for symmetric truncation based on the marginal standard deviations of the covariance matrix. """ # Extract marginal standard deviations from the diagonal sigmas = np.sqrt(np.diag(cov_matrix)) upper = level * sigmas return -upper, upper
@property def tlb(self): return self.cmaker.truncation_level_between @property def tlw(self): return self.cmaker.truncation_level_within
[docs] def compute_all(self, MNE=None, cmon=Monitor(), umon=Monitor()): """ :returns: DataFrame with fields eid, rlz, sid, gmv_X, ... """ max_iml = self.cmaker.oq.get_max_iml() self.init_eid_rlz_sig_eps() data = AccumDict(accum=[]) conditioned = MNE is not None for g, (gs, rlzs) in enumerate(self.cmaker.gsims.items()): if not conditioned: with self.cmaker.gmf_mon: mean_stds = self.cmaker.get_4MN([self.ctx], gs).astype(F32) gs.gid = self.cmaker.gid[g] idxs, = np.where(np.isin(self.rlz, rlzs)) E = len(idxs) if E == 0: # crucial for performance continue with cmon: E = len(idxs) result = np.zeros((len(self.imts), len(self.ctx.sids), E), F32) # arrays of random numbers of shape (M, N, E) and (M, E) within_eps = self._draw_within_eps( E, correlate=not conditioned) # between_eps are used in _compute if self.tlb <= TRUNCATION_THRESHOLD: self.between_eps[idxs] = 0. else: self.between_eps[idxs] = \ self.between_event_model.get_inter_eps( self.imts, E, self.rng).T mean = [] for m, imt in enumerate(self.imts): if conditioned: result[m] = exp(MNE[g][m, :, :E], imt != 'MMI') if self.amplifier: self.amplifier.amplify_gmfs( self.ctx.ampcode, result, m, imt, self.rng) mean.append(MNE[g][m, :, E]) else: ms = mean_stds[:, m] mean.append(ms[0]) self._compute_update( result, m, imt, gs, ms, idxs, within_eps) with umon: result = result.transpose(1, 0, 2) # shape (N, M, E) self.update(data, result, rlzs, np.array(mean), max_iml) with umon: return self.strip_zeros(data)
[docs] def tabulate_conditioned(self, fields, mean, g, indices, rng=None): """Convert one batch of conditioned log fields to a GMF table.""" if rng is None: rng = self.rng gsim, rlzs = list(self.cmaker.gsims.items())[g] gsim.gid = self.cmaker.gid[g] num_events = len(indices) expected = (self.M, self.N, num_events) if fields.shape != expected: raise ValueError( f'Expected conditioned fields with shape {expected}, got ' f'{fields.shape}') result = np.empty(expected, dtype=F32) for m, imt in enumerate(self.imts): result[m] = (np.exp(fields[m]) if imt.string != 'MMI' else fields[m]) if self.amplifier: self.amplifier.amplify_gmfs( self.ctx.ampcode, result, m, imt, rng) data = AccumDict(accum=[]) self.update( data, result.transpose(1, 0, 2), rlzs, np.asarray(mean, dtype=F32), self.cmaker.oq.get_max_iml(), indices) return self.strip_zeros(data, indices)
[docs] def compute_all_batches(self, cmon=Monitor(), umon=Monitor()): """Yield bounded GMF tables and their global event indices.""" self.init_eid_rlz_sig_eps() if (self.within_event_model is not None and self.tlw > TRUNCATION_THRESHOLD): factor = self._get_ce_factor() else: factor = None if factor is None: indices = np.arange(self.E) yield self.compute_all(None, cmon, umon), indices, True return yield from self._compute_ce_batches(factor, cmon, umon)
def _compute_ce_batches(self, factor, cmon, umon): """Yield unconditioned CE fields without forming the full cube.""" max_iml = self.cmaker.oq.get_max_iml() batch_size = self._ce_batch_size(factor) streams = np.random.SeedSequence(self.seed).spawn(3) within_rng, between_rng, amplifier_rng = ( np.random.default_rng(stream) for stream in streams) batches = [] for g, (gs, rlzs) in enumerate(self.cmaker.gsims.items()): idxs, = np.where(np.isin(self.rlz, rlzs)) if not len(idxs): continue if self.tlb > TRUNCATION_THRESHOLD: self.between_eps[idxs] = \ self.between_event_model.get_inter_eps( self.imts, len(idxs), between_rng).T for start in range(0, len(idxs), batch_size): batches.append((g, gs, rlzs, idxs[start:start + batch_size])) logging.info( 'Streaming %d correlated fields in %d batches of at most %d', self.E, len(batches), batch_size) recorded_gsims = set() mean_stds_by_gsim = {} for number, (g, gs, rlzs, idxs) in enumerate(batches, 1): if g not in mean_stds_by_gsim: with self.cmaker.gmf_mon: mean_stds_by_gsim[g] = self.cmaker.get_4MN( [self.ctx], gs).astype(F32) gs.gid = self.cmaker.gid[g] record_stats = g not in recorded_gsims df = self._compute_ce_batch( factor, gs, rlzs, mean_stds_by_gsim[g], idxs, max_iml, within_rng, amplifier_rng, record_stats, cmon, umon) recorded_gsims.add(g) yield df, idxs, number == len(batches) def _ce_batch_size(self, factor): """Bound a batch by both FFT workspace and returned GMF rows.""" fft_events = factor.batch_size(_correlation_budget()) max_rows = int(config.memory.max_gmvs_chunk) output_events = max(1, max_rows // self.N) return min(fft_events, output_events) def _compute_ce_batch(self, factor, gs, rlzs, mean_stds, idxs, max_iml, within_rng, amplifier_rng, record_stats, cmon, umon): """Compute and tabulate one bounded group of CE realizations.""" num_events = len(idxs) data = AccumDict(accum=[]) with cmon: result = np.zeros((self.M, self.N, num_events), F32) within_eps = self._draw_ce_eps( factor, num_events, within_rng) mean = [] for m, imt in enumerate(self.imts): ms = mean_stds[:, m] mean.append(ms[0]) self._compute_update( result, m, imt, gs, ms, idxs, within_eps, amplifier_rng, record_stats) with umon: result = result.transpose(1, 0, 2) self.update( data, result, rlzs, np.array(mean), max_iml, idxs) return self.strip_zeros(data, idxs) def _get_ce_factor(self): """Return a cached CE factor when the large-grid path is eligible.""" if self._ce_checked: return self._ce_factor self._ce_checked = True model = self.within_event_model dense_bytes = _dense_correlation_bytes(model, self.sites, self.M) dense_sites = (len(self.sites.complete) if isinstance(model, SpatialCorrelationModel) else len(self.sites)) compatible = model.SUPPORTS_CIRCULANT_EMBEDDING if dense_sites < CE_MIN_SITES or not compatible: if dense_bytes > _correlation_budget(): qualifier = ('too small for automatic circulant embedding' if compatible else 'not enabled for circulant embedding') raise ValueError( f'{model.__class__.__name__} is {qualifier}; its dense ' f'factorization requires about ' f'{humansize(dense_bytes)}') return None try: complete = self.sites.complete layout = RegularGridLayout.from_sites(complete) positions = _site_positions(complete, self.sites) site_indices = layout.site_indices[positions] self._ce_factor = CirculantEmbeddingFactor.build( model, self.imts, layout.grid_shape, layout.spacing, ResidualComponent.WITHIN_EVENT, self.correlation_context, site_indices) except ValueError as exc: if dense_bytes > _correlation_budget(): raise ValueError( f'Cannot sample {model.__class__.__name__} within the ' f'correlation memory budget: {exc}') from exc logging.warning( 'Falling back to dense %s correlation: %s', model.__class__.__name__, exc) return None factor = self._ce_factor logging.info( 'Using circulant embedding for %s: grid=%sx%s, ' 'occupancy=%.1f%%, embedding=%sx%s, factor=%s', model.__class__.__name__, *layout.grid_shape, 100 * layout.occupancy, *factor.embedded_shape, humansize(factor.spectral_root.nbytes)) return factor def _draw_ce_eps(self, factor, num_events, rng=None): """Draw correlated residual fields in bounded FFT batches.""" if rng is None: rng = self.rng batch_size = min( num_events, factor.batch_size(_correlation_budget())) correlated = np.empty( (factor.output_size, num_events), dtype=F32) for start in range(0, num_events, batch_size): stop = min(start + batch_size, num_events) samples = _truncated_normals( (stop - start, factor.input_size), self.tlw, rng) correlated[:, start:stop] = factor.apply(samples.T) return correlated.reshape(self.M, self.N, num_events) def _draw_within_eps(self, num_events, correlate=True): if self.tlw <= TRUNCATION_THRESHOLD: return np.zeros((self.M, self.N, num_events), F32) model = self.within_event_model if correlate and model is not None: factor = self._get_ce_factor() if factor is not None: return self._draw_ce_eps(factor, num_events) samples = np.asarray([ self.within_dist.rvs((self.N, num_events), self.rng).astype(F32) for _ in range(self.M)]) if (not correlate or model is None or isinstance(model, SpatialCorrelationModel)): return samples if self._within_event_factor is None: self._within_event_factor = model.factor( self.sites, self.imts, ResidualComponent.WITHIN_EVENT, self.correlation_context) flattened = samples.reshape(-1, num_events) correlated = self._within_event_factor.apply(flattened) return correlated.reshape(samples.shape).astype(F32) def _compute_update(self, result, m, imt, gs, ms, idxs, within_eps, rng=None, record_stats=True): try: result[m] = self._compute( ms, m, imt, gs, within_eps[m], idxs, record_stats) except Exception as exc: if exc.__class__ is RuntimeError: msg = str(exc) else: msg = f'{exc.__class__.__name__}:{exc}' raise RuntimeError( '(%s, %s): %s' % (gs, imt, msg) ).with_traceback(exc.__traceback__) if self.amplifier: self.amplifier.amplify_gmfs( self.ctx.ampcode, result, m, imt, self.rng if rng is None else rng) def _compute(self, mean_stds, m, imt, gsim, within_eps, idxs, record_stats=True): # regular case, sets self.sig, returns gmf im = imt.string mean, sig, tau, phi = mean_stds # shapes N if self.cmaker.oq.mea_tau_phi and record_stats: min_iml = self.cmaker.min_iml[m] gmv = np.exp(mean) for s, sid in enumerate(self.ctx.sids): if gmv[s] > min_iml: self.mea_tau_phi.append( (self.rup_id, sid, gsim.gid, m, mean[s], tau[s], phi[s])) if (self.tlw <= TRUNCATION_THRESHOLD and self.tlb <= TRUNCATION_THRESHOLD): # for zero between/within truncation there is only mean, no stds if self.within_event_model: raise ValueError('truncation_level_within=0 requires ' 'no correlation model') gmf = exp(mean, im != 'MMI')[:, np.newaxis].repeat( len(idxs), axis=1) elif gsim.DEFINED_FOR_STANDARD_DEVIATION_TYPES == {StdDev.TOTAL}: # If the GSIM provides only total standard deviation, we need # to compute mean and total standard deviation at the sites # of interest. # In this case, we also assume no correlation model is used. if self.within_event_model: raise CorrelationButNoInterIntraStdDevs( self.within_event_model, gsim) gmf = exp(mean[:, np.newaxis] + sig[:, np.newaxis] * within_eps, im != 'MMI') self.sig[idxs, m] = np.nan else: # NB: [:, newaxis] is used to implement multiplication by row; # for instance, if a = [1 2], b = [[1 2] [3 4]], then # a[:, newaxis] * b = [[1 2] [6 8]] which is the expected result; # otherwise one would get multiplication by column [[1 4] [3 8]] within_res = phi[:, np.newaxis] * within_eps # shape (N, E) if (isinstance( self.within_event_model, SpatialCorrelationModel) and self._ce_factor is None): within_res = self.within_event_model.apply_correlation( self.sites, imt, within_res, phi).astype(F32) between_res = tau[:, np.newaxis] * self.between_eps[idxs, m] # shape (N, 1) * E => (N, E) gmf = exp(mean[:, np.newaxis] + within_res + between_res, im != 'MMI') self.sig[idxs, m] = tau.max() # from shape (N, 1) => scalar return gmf # shapes (N, E)
# this is not used in the engine; it is still useful for usage in IPython # when demonstrating hazardlib capabilities
[docs]def ground_motion_fields(rupture, sites, imts, gsim, truncation_level, realizations, correlation_model=None, seed=0): """ Given an earthquake rupture, the ground motion field calculator computes ground shaking over a set of sites, by randomly sampling a ground shaking intensity model. A ground motion field represents a possible 'realization' of the ground shaking due to an earthquake rupture. .. note:: This calculator is using random numbers. In order to reproduce the same results numpy random numbers generator needs to be seeded. :param openquake.hazardlib.source.rupture.Rupture rupture: Rupture to calculate ground motion fields radiated from. :param openquake.hazardlib.site.SiteCollection sites: Sites of interest to calculate GMFs. :param imts: List of intensity measure type objects (see :mod:`openquake.hazardlib.imt`). :param gsim: Ground-shaking intensity model, instance of subclass of either :class:`~openquake.hazardlib.gsim.base.GMPE` or :class:`~openquake.hazardlib.gsim.base.IPE`. :param truncation_level: Float, number of standard deviations for truncation of the intensity distribution :param realizations: Integer number of GMF simulations to compute. :param correlation_model: Instance of correlation model object. See :mod:`openquake.hazardlib.correlation`. Can be ``None``, in which case non-correlated ground motion fields are calculated. Correlation model is not used if ``truncation_level`` is zero. :param int seed: The seed used in the numpy random number generator :returns: Dictionary mapping intensity measure type objects (same as in parameter ``imts``) to 2d numpy arrays of floats, representing different simulations of ground shaking intensity for all sites in the collection. First dimension represents sites and second one is for simulations. """ cmaker = ContextMaker(rupture.tectonic_region_type, {gsim: U32([0])}, dict(truncation_level=truncation_level, imtls={str(imt): np.array([0.]) for imt in imts})) cmaker.oq.calculation_mode = 'scenario' ebr = EBRupture( rupture, source_id=0, trt_smr=0, n_occ=realizations, id=0, e0=0) ebr.seed = seed N, E = len(sites), realizations gc = GmfComputer(ebr, sites, cmaker, correlation_model) df = gc.compute_all() res = {} for m, imt in enumerate(gc.imts): res[imt] = arr = np.zeros((N, E), F32) for sid, eid, gmv in zip(df.sid, df.eid, df[str(imt)]): arr[sid, eid] = gmv return res