Source code for openquake.hazardlib.contexts

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

import copy
import time
import logging
import warnings
import operator
import numpy
import shapely
from scipy.interpolate import interp1d

from openquake.baselib import config
from openquake.baselib.general import (
    DictArray, RecordBuilder, split_in_slices, block_splitter,
    getsizeof)
from openquake.baselib.performance import Monitor, split_array, compile
from openquake.hazardlib import valid, imt as imt_module
from openquake.hazardlib.const import IMC, StdDev, OK_COMPONENTS
from openquake.hazardlib.tom import NegativeBinomialTOM, PoissonTOM
from openquake.hazardlib.stats import ndtr, truncnorm_sf
from openquake.hazardlib.site import SiteCollection, site_param_dt
from openquake.hazardlib.calc.filters import (
    SourceFilter, IntegrationDistance, magdepdist,
    get_dparam, get_distances, getdefault, MINMAG, MAXMAG)
from openquake.hazardlib.map_array import MapArray
from openquake.hazardlib.geo import multiline
from openquake.hazardlib.geo.mesh import Mesh
from openquake.hazardlib.geo.surface.planar import (
    project, project_back, get_distances_planar)

U8 = numpy.uint8


I32 = numpy.int32


U32 = numpy.uint32


F16 = numpy.float16


F32 = numpy.float32


F64 = numpy.float64


TWO20 = 2**20


TWO16 = 2**16


TWO24 = 2**24


TWO32 = 2**32


STD_TYPES = (StdDev.TOTAL, StdDev.INTER_EVENT, StdDev.INTRA_EVENT)


KNOWN_DISTANCES = frozenset('''rrup rx_ry0 rx ry0 rjb rhypo repi rcdpp azimuth
azimuthcp rvolc clon_clat clon clat rtor x_l'''.split())


MEA = 0


STD = 1


EPS = 1E-3


#: How many (rupture, site) pairs are as expensive as a single surviving
#: context, i.e. how much a discarded pair costs with respect to a context
#: on which the GSIMs are actually evaluated. Determined heuristically
#: from the @slowtask in oq-risk-tests
PAIR_COST = 20.


bymag = operator.attrgetter('mag')


# These coordinates were provided by M Gerstenberger (personal
# communication, 10 August 2018)
cshm_polygon = shapely.geometry.Polygon([(171.6, -43.3), (173.2, -43.3),
                                         (173.2, -43.9), (171.6, -43.9)])


def _get(surfaces, param, dparam, mask=slice(None)):
    arr = numpy.array([dparam[sec.idx, param][mask] for sec in surfaces])
    return arr  # shape (S, N, ...)


def _get_tu(rup, dparam, mask):
    tor = rup.surface.tor
    arr = _get(rup.surface.surfaces, 'tuw', dparam, mask)
    S, N = arr.shape[:2]
    # keep the flipped values and then reorder the surface indices
    # arr has shape (S, N, 2, 3) where 2 refer to the flipping
    tuw = numpy.zeros((S, N, 3), F32)
    for s in range(S):
        idx = tor.soidx[s]
        flip = int(tor.flipped[idx])
        tuw[s] = arr[idx, :, flip, :]  # shape (N, 3)
    return multiline.get_tu(tor.shift, tuw)


[docs]def set_distances(ctx, rup, r_sites, param, dparam, mask, tu): """ Set the distance attributes on the context; also manages paired attributes like clon_lat and rx_ry0. """ if not dparam: # no multifault dists = get_distances(rup, r_sites, param) if param in ('clon_clat', 'rx_ry0'): p0, p1 = param.split('_') # paired attributes setattr(ctx, p0, dists[:, 0]) setattr(ctx, p1, dists[:, 1]) else: setattr(ctx, param, dists) else: # use the MultiLine object u_max = rup.surface.msparam['u_max'] if param in ('rx', 'ry0'): tut, uut = tu ''' # sanity check with the right parameters t, u t, u = rup.surface.tor.get_tu(r_sites) numpy.testing.assert_allclose(tut, t) numpy.testing.assert_allclose(uut, u) ''' if param == 'rx': ctx.rx = tut elif param == 'ry0': neg = uut < 0 ctx.ry0[neg] = numpy.abs(uut[neg]) big = uut > u_max ctx.ry0[big] = uut[big] - u_max elif param == 'rjb': rjbs = _get(rup.surface.surfaces, 'rjb', dparam, mask) ctx['rjb'] = numpy.min(rjbs, axis=0) ''' # sanity check with the right rjb rjb = rup.surface.get_joyner_boore_distance(r_sites) numpy.testing.assert_allclose(ctx.rjb, rjb) ''' elif param == 'clon_clat': coos = _get(rup.surface.surfaces, 'clon_clat', dparam, mask) # shape (numsections, numsites, 3) m = Mesh(coos[:, :, 0], coos[:, :, 1]).get_closest_points(r_sites) # shape (numsites, 3) ctx['clon'] = m.lons ctx['clat'] = m.lats elif param in ('rtor', 'x_l'): # PFD metrics are not in msparam: use the surface methods, which # for a MultiSurface implement the 'segments' semantics setattr(ctx, param, get_distances(rup, r_sites, param))
[docs]def is_modifiable(gsim): """ :returns: True if it is a ModifiableGMPE """ return hasattr(gsim, 'gmpe') and hasattr(gsim, 'params')
[docs]def concat(ctxs): """ Concatenate context arrays """ if not ctxs: return [] ctx = ctxs[0] # if ctx has probs_occur, it is assumed to be non-poissonian if hasattr(ctx, 'probs_occur') and ctx.probs_occur.shape[1] >= 1: # case 27, 29, 62, 65, 75, 78, 80 [shp] = list(set(ctx.probs_occur.shape[1] for ctx in ctxs)) p_array = [p for p in ctxs if p.probs_occur.shape[1] == shp] out = numpy.concatenate(p_array).view(numpy.recarray) else: out = numpy.concatenate(ctxs).view(numpy.recarray) return out
[docs]def size(imtls): """ :returns: size of the dictionary of arrays imtls """ imls = imtls[next(iter(imtls))] return len(imls) * len(imtls)
[docs]def trivial(ctx, name): """ :param ctx: a recarray :param name: name of a parameter :returns: True if the parameter is missing or single valued """ if name not in ctx.dtype.names: return True return len(numpy.unique(numpy.float32(ctx[name]))) == 1
[docs]class Oq(object): """ A mock for OqParam """ af = None impact = False total_residual_correlation_model = None correlation_strategy = 'cholesky_decomposition' mea_tau_phi = False split_sources = True keep_rupdata = True use_rates = False with_betw_ratio = None infer_occur_rates = False minimum_intensity = {} sec_imts = [] inputs = () def __init__(self, **hparams): vars(self).update(hparams) @property def min_iml(self): try: imtls = self.imtls except AttributeError: imtls = self.hazard_imtls return numpy.array([1E-10 for imt in imtls])
[docs] def get_max_iml(self): return numpy.full(len(self.imtls), numpy.nan)
[docs] def get_reqv(self): if 'reqv' not in self.inputs: return return {key: valid.RjbEquivalent(value) for key, value in self.inputs['reqv'].items()}
[docs]class FarAwayRupture(Exception): """Raised if the rupture is outside the maximum distance for all sites"""
[docs]def get_num_distances(gsims): """ :returns: the number of distances required for the given GSIMs """ dists = set() for gsim in gsims: dists.update(gsim.REQUIRES_DISTANCES) return len(dists)
# NB: minimum_magnitude is ignored def _interp(param, name, trt): try: mdd = param[name] except KeyError: return magdepdist([(MINMAG, 1000), (MAXMAG, 1000)]) if isinstance(mdd, IntegrationDistance): return mdd(trt) elif isinstance(mdd, dict): if mdd: magdist = getdefault(mdd, trt) else: magdist = [(MINMAG, 1000), (MAXMAG, 1000)] return magdepdist(magdist) return mdd
[docs]def simple_cmaker(gsims, imts, **params): """ :returns: a simplified ContextMaker for use in the tests """ dic = dict(imtls={imt: [0] for imt in imts}) dic.update(**params) return ContextMaker('*', gsims, dic)
# generator of quintets (rup_index, mag, magdist, planars, sites) # called first in preclassical with a reduced sitecol and then in classical def _quintets(cmaker, src, sitecol): with cmaker.ir_mon: # building planar geometries planardict = src.get_planar(cmaker.shift_hypo) magdist = {mag: cmaker.maximum_distance(mag) for mag in planardict} # cmaker.maximum_distance(mag) can be 0 if outside the mag range, and # it is 0 for all the magnitudes if the source has no magnitude inside # the range, which can happen since for the sources modified by the # uncertainties the magnitude filtering is performed in the classical # workers and not in the preclassical (see filter_mag) valid = {mag: dist for mag, dist in magdist.items() if dist > 0} if not valid: # the source is irrelevant, no site is in range return maxmag = max(valid) maxdist = valid[maxmag] cdist = sitecol.get_cdist(src.location) # Reuse the rupture radii while handling this source; get_psdist is # called once per rupture and recomputing the whole vector is expensive. radii = src._get_rupture_projection_radii() mask = cdist <= maxdist + src.max_radius(maxdist, radii) sites = sitecol.filter(mask) if sites is None: return minmag = cmaker.maximum_distance.x[0] maxmag = cmaker.maximum_distance.x[-1] # splitting by magnitude if src.count_nphc() == 1: # one rupture per magnitude for m, (mag, pla) in enumerate(planardict.items()): if minmag <= mag <= maxmag: yield m, mag, magdist[mag], pla, sites else: for m, rup in enumerate(src.iruptures()): mag = rup.mag if mag > maxmag or mag < minmag: continue mdist = magdist[mag] # far sites use the mean rupture from src.iruptures(), # close sites use all the nodal planes/hypocenters meanpla = [rup.surface.array.reshape(-1, 3)] allpla = planardict[mag] # NB: having a good psdist is essential for performance! psdist = src.get_psdist(m, mag, cmaker.pointsource_distance, magdist, radii) close = sites.filter(cdist[mask] <= psdist) far = sites.filter(cdist[mask] > psdist) if cmaker.fewsites: if close is None: # all is far, common for small mag yield m, mag, mdist, meanpla, sites else: # something is close yield m, mag, mdist, allpla, sites else: # many sites if close is None: # all is far yield m, mag, mdist, meanpla, far elif far is None: # all is close yield m, mag, mdist, allpla, close else: # some sites are far, some are close yield m, mag, mdist, meanpla, far yield m, mag, mdist, allpla, close # helper used to populate contexts for planar ruptures def _get_ctx_planar(cmaker, builder, mag, mrate, magi, planar, sites, src_id, src_offset, tom): zeroctx = builder.zeros((len(planar), len(sites))) # shape (N, U) if cmaker.fewsites: offset = src_offset + magi * len(planar) rup_ids = zeroctx['rup_id'].T # numpy trick, shape (U, N) rup_ids[:] = numpy.arange(offset, offset+len(planar)) # computing distances rrup, xx, yy = project(planar, sites.xyz) # (3, U, N) # get the closest points on the surface if cmaker.fewsites or 'clon' in cmaker.REQUIRES_DISTANCES: closest = project_back(planar, xx, yy) # (3, U, N) # set distances zeroctx['rrup'] = rrup for par in cmaker.REQUIRES_DISTANCES - {'rrup'}: zeroctx[par] = get_distances_planar(planar, sites, par) for par in cmaker.REQUIRES_DISTANCES: dst = zeroctx[par] if cmaker.minimum_distance: dst[dst < cmaker.minimum_distance] = cmaker.minimum_distance # ctx has shape (U, N), ctxt (N, U) ctxt = zeroctx.T # smart trick taking advantage of numpy magic ctxt['src_id'] = src_id # setting rupture parameters for par in cmaker.ruptparams: if par == 'mag': ctxt[par] = mag elif par == 'occurrence_rate': ctxt[par] = mrate * planar.wlr[:, 2] # shape U-> (N, U) elif par == 'width': ctxt[par] = planar.wlr[:, 0] elif par == 'strike': ctxt[par] = planar.sdr[:, 0] elif par == 'dip': ctxt[par] = planar.sdr[:, 1] elif par == 'rake': ctxt[par] = planar.sdr[:, 2] elif par == 'ztor': # top edge depth ctxt[par] = planar.corners[:, 2, 0] elif par == 'zbot': # bottom edge depth ctxt[par] = planar.corners[:, 2, 3] elif par == 'hypo_lon': ctxt[par] = planar.hypo[:, 0] elif par == 'hypo_lat': ctxt[par] = planar.hypo[:, 1] elif par == 'hypo_depth': ctxt[par] = planar.hypo[:, 2] if cmaker.fewsites: zeroctx['clon'] = closest[0] zeroctx['clat'] = closest[1] # setting site parameters for par in cmaker.siteparams: zeroctx[par] = sites.array[par] # shape N-> (U, N) if hasattr(tom, 'get_pmf'): # NegativeBinomialTOM # read Probability Mass Function from model and reshape it # into predetermined shape of probs_occur # NB: in case_79 zeroctx['probs_occur'] has shape (2, 1, 10) # where (2, 1) = (len(planar), len(sites)) pmf = tom.get_pmf(planar.wlr[:, 2] * mrate) zeroctx['probs_occur'] = pmf[:, numpy.newaxis, :] return zeroctx.flatten() # shape N*U
[docs]def genctxs_Pp(src, sitecol, cmaker): """ Context generator for point sources and collapsed point sources """ dd = cmaker.defaultdict.copy() tom = getattr(src, 'temporal_occurrence_model', None) if tom and isinstance(tom, NegativeBinomialTOM): dd['probs_occur'] = numpy.zeros_like(NegativeBinomialTOM.x) else: dd['probs_occur'] = numpy.zeros(0) builder = RecordBuilder(**dd) cmaker.siteparams = [par for par in sitecol.array.dtype.names if par in dd] cmaker.ruptparams = cmaker.REQUIRES_RUPTURE_PARAMETERS | {'occurrence_rate'} mrate = dict(src.get_annual_occurrence_rates()) for magi, mag, magdist, planars, sites in _quintets(cmaker, src, sitecol): if not planars: continue elif len(planars) > 1: # when using ps_grid_spacing, case_43 # CollapsedPointSource.get_planar() has already normalized each # source block by its own magnitude rate. pla = numpy.concatenate(planars).view(numpy.recarray) else: pla = planars[0] # building contexts with cmaker.ctx_mon: ctx = _get_ctx_planar(cmaker, builder, mag, mrate[mag], magi, pla, sites, src.id, src.offset, tom) ctxt = ctx[ctx.rrup < magdist] if len(ctxt): yield ctxt
# this is the critical function for the performance of the classical calculator # the performance is dominated by the CPU cache, i.e. large arrays are slow # the only way to speedup is to reduce the maximum_distance, then the array # will become shorter in the N dimension (number of affected sites), or to # collapse the ruptures, then truncnorm_sf will be called less times @compile("(float64[:,:,:], float64[:,:], float64, float32[:,:])") def _set_poes(mean_std, loglevels, phi_b, out): L1 = loglevels.size // len(loglevels) for m, levels in enumerate(loglevels): mL1 = m * L1 mea, std = mean_std[:, m] # shape N for lvl, iml in enumerate(levels): out[mL1 + lvl] = truncnorm_sf(phi_b, (iml - mea) / std) def _fix(gsimdict, betw): if betw: out = {} for gsim, uints in gsimdict.items(): if len(gsim.DEFINED_FOR_STANDARD_DEVIATION_TYPES) == 1: out[valid.modified_gsim(gsim, add_between_within_stds=betw)] \ = uints else: out[gsim] = uints return out return gsimdict
[docs]class ContextMaker(object): """ A class to manage the creation of contexts and to compute mean/stddevs and possibly PoEs. :param trt: tectonic region type string :param gsims: list of GSIMs or a dictionary gsim -> rlz indices :param oq: dictionary of parameters like the maximum_distance, the IMTLs, the investigation time, etc, or an OqParam instance :param extraparams: additional site parameters to consider, used only in the tests NB: the trt can be different from the tectonic region type for which the underlying GSIMs are defined. This is intentional. """ REQUIRES = ['DISTANCES', 'SITES_PARAMETERS', 'RUPTURE_PARAMETERS'] deltagetter = None fewsites = False ilabel = None tom = None cluster = None # set in RmapMaker source_mb = 0 # set in build_dparam dt = 0 @property def cross_correl(self): """Compatibility alias for the total-residual correlation model.""" return self.total_residual_correlation_model @cross_correl.setter def cross_correl(self, model): self.total_residual_correlation_model = model def __init__(self, trt, gsims, oq, monitor=Monitor(), extraparams=()): self.trt = trt if isinstance(oq, dict): # this happens when instantiating RuptureData in extract.py param = oq oq = Oq(**param) self.mags = param.get('mags', ()) # list of strings %.2f self.total_residual_correlation_model = param.get( 'total_residual_correlation_model', param.get('cross_correl')) else: # OqParam param = vars(oq) param['reqv'] = oq.get_reqv() param['af'] = getattr(oq, 'af', None) try: resolver = oq.get_total_residual_correlation_model except AttributeError: self.total_residual_correlation_model = getattr( oq, 'total_residual_correlation_model', None) if self.total_residual_correlation_model is None: self.total_residual_correlation_model = getattr( oq, 'cross_correl', None) else: self.total_residual_correlation_model = resolver() self.imtls = oq.imtls try: self.mags = oq.mags_by_trt[trt] except AttributeError: self.mags = () except KeyError: # missing TRT but there is only one [(_, self.mags)] = oq.mags_by_trt.items() if oq.with_betw_ratio: betw_ratio = {'with_betw_ratio': oq.with_betw_ratio} elif oq.impact: betw_ratio = {'with_betw_ratio': 1.7} # same as in GEESE else: betw_ratio = {} if isinstance(gsims, dict): self.gsims = _fix(gsims, betw_ratio) else: self.gsims = _fix({gsim: U32([i]) for i, gsim in enumerate(gsims)}, betw_ratio) # NB: gsim_idx is the index of each gsim in the list, i.e. the id # identifying a gsim of the group; it is used to label the GMFs # (see calc.gmf) and the contexts (see the gid column of the rup # dataframe) self.gsim_idx = numpy.arange(len(gsims), dtype=numpy.uint16) self._gid = None # set by restrict_trt_smrs, see the gid property self.oq = oq self.monitor = monitor self._init1(param) self._init2(param, extraparams) self.set_imts_conv() self.init_monitoring(self.monitor) def _init1(self, param): self.dparam = {} # multifault cache if 'poes' in param: self.poes = param['poes'] if 'imtls' in param: for imt in param['imtls']: if not isinstance(imt, str): raise TypeError('Expected string, got %s' % type(imt)) self.imtls = param['imtls'] elif 'hazard_imtls' in param: self.imtls = imt_module.dictarray(param['hazard_imtls']) elif not hasattr(self, 'imtls'): raise KeyError('Missing imtls in ContextMaker!') self.max_sites_disagg = param.get('max_sites_disagg', 10) self.collapse_level = int(param.get('collapse_level', -1)) self.disagg_by_src = param.get('disagg_by_src', False) self.horiz_comp = param.get('horiz_comp_to_geom_mean', False) self.maximum_distance = _interp(param, 'maximum_distance', self.trt) if 'pointsource_distance' not in param: self.pointsource_distance = float( config.performance.pointsource_distance) else: self.pointsource_distance = getdefault( param['pointsource_distance'], self.trt) self.minimum_distance = param.get('minimum_distance', 0) self.investigation_time = param.get('investigation_time') self.ses_seed = param.get('ses_seed', 42) self.ses_per_logic_tree_path = param.get('ses_per_logic_tree_path', 1) self.truncation_level = param.get('truncation_level', 99.) self.truncation_level_between = param.get('truncation_level_between') if self.truncation_level_between is None: self.truncation_level_between = self.truncation_level self.truncation_level_within = param.get('truncation_level_within') if self.truncation_level_within is None: self.truncation_level_within = self.truncation_level self.phi_b = ndtr(self.truncation_level) self.num_epsilon_bins = param.get('num_epsilon_bins', 1) self.disagg_bin_edges = param.get('disagg_bin_edges', {}) self.ps_grid_spacing = param.get('ps_grid_spacing') self.split_sources = self.oq.split_sources def _init2(self, param, extraparams): for req in self.REQUIRES: reqset = set() for gsim in self.gsims: reqset.update(getattr(gsim, 'REQUIRES_' + req)) if getattr(self.oq, 'af', None) and req == 'SITES_PARAMETERS': reqset.add('ampcode') if is_modifiable(gsim) and req == 'SITES_PARAMETERS': reqset.add('vs30') # required by the ModifiableGMPE reqset.update(gsim.gmpe.REQUIRES_SITES_PARAMETERS) if 'apply_swiss_amplification' in gsim.params: reqset.add('amplfactor') if ('apply_swiss_amplification_sa' in gsim.params): reqset.add('ch_ampl03') reqset.add('ch_ampl06') reqset.add('ch_phis2s03') reqset.add('ch_phis2s06') reqset.add('ch_phiss03') reqset.add('ch_phiss06') setattr(self, 'REQUIRES_' + req, reqset) self.min_iml = self.oq.min_iml self.reqv = param.get('reqv') if self.reqv is not None: self.REQUIRES_DISTANCES.add('repi') # NB: REQUIRES_DISTANCES is empty when gsims = [FromFile] REQUIRES_DISTANCES = self.REQUIRES_DISTANCES | {'rrup'} reqs = (sorted(self.REQUIRES_RUPTURE_PARAMETERS) + sorted(self.REQUIRES_SITES_PARAMETERS | set(extraparams)) + sorted(REQUIRES_DISTANCES)) dic = {} for req in reqs: if req in site_param_dt: dt = site_param_dt[req] if isinstance(dt, tuple): # (string_, size) dic[req] = b'X' * dt[1] else: dic[req] = dt(0) else: dic[req] = 0. dic['src_id'] = I32(0) dic['rup_id'] = U32(0) dic['sids'] = U32(0) dic['rrup'] = F64(0) dic['occurrence_rate'] = F64(0) self.defaultdict = dic self.shift_hypo = param.get('shift_hypo')
[docs] def init_monitoring(self, monitor): # instantiating child monitors, may be called in the workers self.gmf_mon = monitor('computing mean_std', measuremem=False) self.poe_mon = monitor('get_poes', measuremem=False) self.ir_mon = monitor('iter_ruptures', measuremem=False) self.sec_mon = monitor('building dparam', measuremem=False) self.delta_mon = monitor('getting delta_rates', measuremem=False) # NB: building the contexts (distances + rupture parameters + # recarray) is by far the most expensive operation of the classical # calculator, so it is monitored even if the monitor is entered # once per rupture; the counts tell how many ruptures were # considered (the ruptures beyond the integration time included) self.ctx_mon = monitor('building contexts', measuremem=False) self.task_no = getattr(monitor, 'task_no', 0) self.out_no = getattr(monitor, 'out_no', self.task_no) self.cfactor = numpy.zeros(2)
[docs] def copy(self, **kw): """ :returns: a copy of the ContextMaker with modified attributes """ new = copy.copy(self) for k, v in kw.items(): setattr(new, k, v) if 'imtls' in kw: new.set_imts_conv() return new
[docs] def restrict(self, imts): """ :param imts: a list of IMT strings subset of the full list :returns: a new ContextMaker involving less IMTs """ new = copy.copy(self) new.imtls = DictArray({imt: self.imtls[imt] for imt in imts}) new.set_imts_conv() return new
[docs] def set_imts_conv(self): """ Set the .imts list and .conv dictionary for the horizontal component conversion (if any). Also set the .loglevels. """ self.loglevels = DictArray(self.imtls) if self.imtls else {} with warnings.catch_warnings(): # avoid RuntimeWarning: divide by zero encountered in log warnings.simplefilter("ignore") for imt, imls in self.imtls.items(): if imt != 'MMI': self.loglevels[imt] = numpy.log(imls) self.imts = tuple(imt_module.from_string(im) for im in self.imtls) self.conv = {} # gsim -> imt -> (conv_median, conv_sigma, rstd) if not self.horiz_comp: return # do not convert for gsim in self.gsims: self.conv[gsim] = {} imc = gsim.DEFINED_FOR_INTENSITY_MEASURE_COMPONENT if not imc: # for GMPETables continue elif not isinstance(imc, IMC): raise ValueError( f"{gsim.__class__.__name__}." f"DEFINED_FOR_INTENSITY_MEASURE_COMPONENT " f"is not an instance of const.IMC") elif imc.name == 'GEOMETRIC_MEAN': pass # nothing to do elif imc.name in OK_COMPONENTS: dic = {imt: imc.apply_conversion(imt) for imt in self.imts} self.conv[gsim].update(dic) else: logging.info(f'Conversion from {imc.name} not applicable to' f' {gsim.__class__.__name__}')
@property def gid(self): """ :returns: the ids of the columns of the rates generated by this cmaker, i.e. the gids of an index of rate attribution if the cmaker has been restricted (see restrict_trt_smrs), otherwise the gsim_idx, since for a whole group the ids of the rates and the indexes of the gsims coincide """ return self.gsim_idx if self._gid is None else self._gid @gid.setter def gid(self, gids): self._gid = gids
[docs] def split(self, blocksize): """ Split the ContextMaker by blocks of GSIMs """ for gsim_idx, wei, gsims in zip(block_splitter(self.gsim_idx, blocksize), block_splitter(self.wei, blocksize), block_splitter(self.gsims, blocksize)): new = copy.copy(self) new.gsims = gsims new.gsim_idx = gsim_idx new.wei = wei yield new
[docs] def restrict_trt_smrs(self, trt_smrs, gid, wei): """ :param trt_smrs: the trt_smrs of the index of rate attribution :param gid: the gids identifying the index of rate attribution, i.e. the columns of the rates generated by the new cmaker; NB: the gsim_idx are not changed, since the gsims are not restricted either :param wei: the weights of the gids :returns: a shallow copy of the cmaker restricted to the realizations trt_smrs, since a group has the trt_smrs of all the realizations but the sources can have different uncertainties in different ones NB: the gsims are not restricted, since the number of gsims of an index of rate attribution is the number of gsims of its TRT, see get_rlzs_by_gsim, and must match the number of gids, see get_gids """ new = copy.copy(self) new.trt_smrs = numpy.array(trt_smrs, U32) new.gid = gid new.wei = wei assert len(new.gsims) == len(gid), (len(new.gsims), len(gid)) return new
[docs] def horiz_comp_to_geom_mean(self, mean_stds, gsim): """ This function converts ground-motion obtained for a given description of horizontal component into ground-motion values for geometric_mean. The conversion equations used are from: - Beyer and Bommer (2006): for arithmetic mean, GMRot and random - Boore and Kishida (2017): for RotD50 """ if not self.conv[gsim]: return for m, imt in enumerate(self.imts): me, si, _ta, _ph = mean_stds[:, m] conv_median, conv_sigma, rstd = self.conv[gsim][imt] me[:] = numpy.log(numpy.exp(me) / conv_median) si[:] = ((si**2 - conv_sigma**2) / rstd**2)**0.5
@property def scenario(self): """ True if scenario """ return 'scenario' in self.oq.calculation_mode @property def Z(self): """ :returns: the number of realizations associated to self """ return sum(len(rlzs) for rlzs in self.gsims.values())
[docs] def new_ctx(self, size): """ :returns: a recarray of the given size full of zeros """ return RecordBuilder(**self.defaultdict).zeros(size)
[docs] def recarray(self, ctxs): """ :params ctxs: a non-empty list of homogeneous contexts :returns: a recarray, possibly collapsed """ assert ctxs dd = self.defaultdict.copy() if not hasattr(ctxs[0], 'probs_occur'): for ctx in ctxs: ctx.probs_occur = numpy.zeros(0) np = 0 else: shps = [ctx.probs_occur.shape for ctx in ctxs] np = max(i[1] if len(i) > 1 else i[0] for i in shps) dd['probs_occur'] = numpy.zeros(np) C = sum(len(ctx) for ctx in ctxs) ra = RecordBuilder(**dd).zeros(C) start = 0 for ctx in ctxs: if self.minimum_distance: for name in self.REQUIRES_DISTANCES: array = ctx[name] small_distances = array < self.minimum_distance if small_distances.any(): array = numpy.array(array) # make a copy first array[small_distances] = self.minimum_distance ctx[name] = array slc = slice(start, start + len(ctx)) for par in dd: if par == 'rup_id': val = getattr(ctx, par) else: val = getattr(ctx, par, numpy.nan) if par == 'clon_clat': ra['clon'][slc] = ctx.clon ra['clat'][slc] = ctx.clat else: getattr(ra, par)[slc] = val ra.sids[slc] = ctx.sids start = slc.stop return ra
[docs] def get_ctx_params(self): """ :returns: the interesting attributes of the context """ params = {'occurrence_rate', 'sids', 'src_id', 'probs_occur', 'clon', 'clat', 'rrup'} params.update(self.REQUIRES_RUPTURE_PARAMETERS) for dparam in self.REQUIRES_DISTANCES: params.add(dparam + '_') return params
[docs] def from_planar(self, rup, hdist, step, point='TC', toward_azimuth=90., direction='positive'): """ :param rup: a BaseRupture instance with a PlanarSurface and site parameters :returns: a context array for the sites around the rupture """ sitecol = SiteCollection.from_planar( rup, point='TC', toward_azimuth=toward_azimuth, direction=direction, hdist=hdist, step=step, req_site_params=self.REQUIRES_SITES_PARAMETERS) ctxs = list(self.genctxs([rup], sitecol, src_id=0)) return self.recarray(ctxs)
[docs] def from_srcs(self, srcs, sitecol): # used in disagg.disaggregation """ :param srcs: a list of Source objects with consistent TOM :param sitecol: a SiteCollection instance :returns: a context array """ ctxs = [] srcfilter = SourceFilter(sitecol, self.maximum_distance) for i, src in enumerate(srcs): if src.id == -1: # not set yet src.id = i sites = srcfilter.get_close_sites(src) if sites is not None: ctxs.extend(self.get_ctxs(src, sites)) return concat(ctxs)
@staticmethod def _get_zbot(rup, msparam): """ Bottom-edge depth; needed for width estimation in CampbellBozorgnia2014. """ if msparam: return msparam['zbot'] elif rup.surface and hasattr(rup, 'surfaces'): return rup.surface.zbot elif rup.surface: return rup.surface.mesh.depths.max() else: return rup.hypocenter.depth @staticmethod def _get_in_cshm(rup): """ Whether the rupture intersects the CSHM polygon; used in the McVerry and Bradley GMPEs. This is really expensive. """ if not rup.surface: return False lons = rup.surface.mesh.lons.flatten() lats = rup.surface.mesh.lats.flatten() points_in_polygon = ( shapely.geometry.Point(lon, lat).within(cshm_polygon) for lon, lat in zip(lons, lats)) return any(points_in_polygon) def _get_rup_param(self, rup, param, msparam): """ :param rup: a rupture :param param: a rupture parameter name :param msparam: multi-fault section parameters, or None :returns: the value of the given rupture parameter """ surface = rup.surface if param == 'mag': return numpy.round(rup.mag, 3) elif param == 'strike': return msparam['strike'] if msparam else surface.get_strike() elif param == 'dip': if msparam: return msparam['dip'] dip = getattr(surface, 'original_dip', None) return dip if dip is not None else surface.get_dip() elif param == 'rake': return rup.rake elif param == 'ztor': return (msparam['ztor'] if msparam else surface.get_top_edge_depth()) elif param == 'width': return msparam['width'] if msparam else surface.get_width() elif param == 'zbot': return self._get_zbot(rup, msparam) elif param == 'length': # top rupture trace length (PFDHA); scalar per rupture return surface.get_tor_length() if surface else 0.0 elif param == 'in_cshm': return self._get_in_cshm(rup) elif param == 'hypo_lon': return rup.hypocenter.longitude elif param == 'hypo_lat': return rup.hypocenter.latitude elif param == 'hypo_depth': return rup.hypocenter.depth elif param == 'is_aftershock': # Set to True later in RmapMaker for ruptures with a non-zero # delta_rate. Without delta_rates it stays False return False elif param == 'crjb': # Centroid Joyner-Boore distance between an aftershock rup and # its main shock. Can be provided from the delta_rates CSV return numpy.nan else: raise ValueError('%s requires unknown rupture parameter %r' % (type(self).__name__, param))
[docs] def get_rparams(self, rup): """ :returns: a dictionary with the rupture parameters """ msparam = rup.surface.msparam if self.dparam else None dic = {param: self._get_rup_param(rup, param, msparam) for param in self.REQUIRES_RUPTURE_PARAMETERS} dic['occurrence_rate'] = getattr(rup, 'occurrence_rate', numpy.nan) if hasattr(rup, 'temporal_occurrence_model'): if isinstance(rup.temporal_occurrence_model, NegativeBinomialTOM): dic['probs_occur'] = ( rup.temporal_occurrence_model.get_pmf( rup.occurrence_rate)) elif hasattr(rup, 'probs_occur'): dic['probs_occur'] = rup.probs_occur return dic
[docs] def genctxs(self, same_mag_rups, sites, src_id): """ :params same_mag_rups: a list of ruptures :param sites: a (filtered) site collection :param src_id: source index :yields: a context array for each rupture """ magdist = self.maximum_distance(same_mag_rups[0].mag) for rup in same_mag_rups: # NB: the monitor is entered once per rupture, since the # building of the contexts is the critical section with self.ctx_mon: yield from self._genctx(rup, sites, src_id, magdist)
def _genctx(self, rup, sites, src_id, magdist): """ :param rup: a rupture :param sites: a (filtered) site collection :param src_id: source index :param magdist: integration time, i.e. the maximum distance :yields: a context array for the given rupture """ dparam = self.dparam if dparam: rrups = _get(rup.surface.surfaces, 'rrup', dparam) rrup = numpy.min(rrups, axis=0) else: rrup = get_distances(rup, sites, 'rrup') mask = rrup <= magdist if not mask.any(): return r_sites = sites.filter(mask) # to debug you can insert here # print(rup.surface.tor.get_tuw_df(r_sites)) # import pdb; pdb.set_trace() ''' # sanity check true_rrup = rup.surface.get_min_distance(r_sites) numpy.testing.assert_allclose(true_rrup, rrup[mask]) ''' rparams = self.get_rparams(rup) ctx = self._make_ctx(rparams, r_sites, rrup, mask) params = self.REQUIRES_DISTANCES - {'rrup'} if self.fewsites or 'clon' in params or 'clat' in params: params.add('clon_clat') # compute tu only once if dparam and ('rx' in params or 'ry0' in params): tu = _get_tu(rup, dparam, mask) else: tu = None for param in params - {'clon', 'clat'}: set_distances(ctx, rup, r_sites, param, dparam, mask, tu) # PFD multi-fault reference-line metrics: fill only the methods # the configured PFD models declare (cmaker.pfd_methods); single # surfaces fall back to the canonical trace-based metrics for method in getattr(self, 'pfd_methods', ()): if method == 'segments': continue if rup.surface is not None and hasattr( rup.surface, 'get_ref_metrics'): r_km, x_l, l_km = rup.surface.get_ref_metrics( method, r_sites) else: r_km, x_l, l_km = ctx.rtor, ctx.x_l, ctx.length ctx['rtor_' + method] = r_km ctx['x_l_' + method] = x_l ctx['length_' + method] = l_km # Equivalent distances reqv_obj = (self.reqv.get(self.trt) if self.reqv else None) if reqv_obj and not rup.surface: # PointRuptures have no surface reqv = reqv_obj.get(ctx.repi, rup.mag) if 'rjb' in self.REQUIRES_DISTANCES: ctx.rjb = reqv if 'rrup' in self.REQUIRES_DISTANCES: ctx.rrup = numpy.sqrt(reqv**2 + rup.hypocenter.depth**2) for name in r_sites.array.dtype.names: setattr(ctx, name, r_sites[name]) ctx.src_id = src_id if src_id >= 0: ctx.rup_id = rup.rup_id yield ctx def _make_ctx(self, rparams, r_sites, rrup, mask): """ :param rparams: a dictionary of rupture parameters :param r_sites: a (filtered) site collection :param rrup: rupture distances, one per site :param mask: boolean mask of the sites within the integration time :returns: a context recarray with shape (len(r_sites),) """ dd = self.defaultdict.copy() try: po = rparams['probs_occur'] except KeyError: dd['probs_occur'] = numpy.zeros(0) else: L = len(po) if len(po.shape) == 1 else po.shape[1] dd['probs_occur'] = numpy.zeros(L) ctx = RecordBuilder(**dd).zeros(len(r_sites)) for par, val in rparams.items(): ctx[par] = val ctx.rrup = rrup[mask] ctx.sids = r_sites.sids return ctx # this is called for non-point sources (or point sources in preclassical)
[docs] def gen_contexts(self, rups_sites, src_id): """ :yields: the old-style RuptureContexts generated by the source """ for rups, sites in rups_sites: # ruptures with the same magnitude yield from self.genctxs(rups, sites, src_id)
def _build_dparam(self, src, sitecol): dparams = {'rjb', 'tuw'} if self.fewsites: dparams |= {'clon_clat'} for sec in src.get_sections(src.get_unique_idxs()): if (sec.idx, 'rrup') in self.dparam: continue self.dparam[sec.idx, 'rrup'] = get_dparam(sec, sitecol, 'rrup') for param in dparams: self.dparam[sec.idx, param] = get_dparam(sec, sitecol, param) self.source_mb += getsizeof(src) / TWO20
[docs] def get_ctxs(self, src, sitecol, src_id=0, step=1): """ :param src: a source object (already split) or a list of ruptures :param sitecol: a (filtered) SiteCollection :param src_id: integer source ID used where src is actually a list :param step: > 1 only in preclassical :returns: list of recarrays """ self.fewsites = len(sitecol.complete) <= self.max_sites_disagg if self.fewsites or 'clon' in self.REQUIRES_DISTANCES: self.defaultdict['clon'] = F64(0.) self.defaultdict['clat'] = F64(0.) if getattr(src, 'location', None): return genctxs_Pp(src, sitecol, self) elif hasattr(src, 'source_id'): # other source if src.code == b'F' and step == 1: with self.sec_mon: self._build_dparam(src, sitecol) minmag = self.maximum_distance.x[0] maxmag = self.maximum_distance.x[-1] with self.ir_mon: allrups = list(src.iter_ruptures( shift_hypo=self.shift_hypo, step=step)) for i, rup in enumerate(allrups): rup.rup_id = src.offset + i allrups = sorted([rup for rup in allrups if minmag <= rup.mag <= maxmag], key=bymag) if not allrups: return iter([]) # sorted by mag by construction u32mags = U32([rup.mag * 100 for rup in allrups]) rups_sites = [(rups, sitecol) for rups in split_array( numpy.array(allrups), u32mags)] src_id = src.id else: # in event based we get a list with a single rupture rups_sites = [(src, sitecol)] src_id = -1 ctxs = self.gen_contexts(rups_sites, src_id) if len(rups_sites) == 1 and not self.minimum_distance: # fast lane for event based with no minimum_distance return ctxs return (self.recarray([c]) for c in ctxs)
[docs] def get_occ_rates(self, ctxt): """ :param ctxt: context array generated by this ContextMaker :returns: occurrence rates, possibly from probs_occur[0] """ # thanks to split_by_tom we can assume ctx to be homogeneous if numpy.isfinite(ctxt[0].occurrence_rate): return ctxt.occurrence_rate else: probs = [rec.probs_occur[0] for rec in ctxt] return -numpy.log(probs) / self.investigation_time
# not used by the engine, it is meant for notebooks
[docs] def get_poes(self, srcs, sitecol, tom=None, rup_mutex={}, collapse_level=-1): """ :param srcs: a list of sources with the same TRT :param sitecol: a SiteCollection instance with N sites :returns: an array of PoEs of shape (N, L, G) """ ctx = self.from_srcs(srcs, sitecol) return self.get_pmap(ctx, tom, rup_mutex).array
def _gen_poes(self, ctx): # NB: by construction ctx.mag contains a single magnitude from openquake.hazardlib.site_amplification import get_poes_site (M, L1), G = self.loglevels.array.shape, len(self.gsims) # split large context arrays to avoid filling the CPU cache with self.gmf_mon: mean_stdt = self.get_mean_stds([ctx], split_by_mag=False) if len(ctx) < 100: # do not split in slices to make debugging easier slices = [slice(0, len(ctx))] else: # making plenty of slices so that the array `poes` is small slices = split_in_slices(len(ctx), 2*L1) for slc in slices: with self.poe_mon: # this is allocating at most a few MB of RAM poes = numpy.zeros((slc.stop-slc.start, M*L1, G), F32) # NB: using .empty would break the MixtureModelGMPETestCase for g, gsim in enumerate(self.gsims): ms = mean_stdt[:2, g, :, slc] # builds poes of shape (n, L, G) if self.oq.af: # amplification method poes[:, :, g] = get_poes_site(ms, self, ctx[slc]) else: # regular case set_poes(gsim, ms, self, ctx, poes[:, :, g], slc) yield (poes, mean_stdt[0, :, :, slc], mean_stdt[1, :, :, slc], mean_stdt[2, :, :, slc], slc) #cs,ms,ps = ctx.nbytes/TWO20, mean_stdt.nbytes/TWO20, poes.nbytes/TWO20 #print('C=%.1fM, mean_stds=%.1fM, poes=%.1fM, G=%d' % (cs, ms, ps, G))
[docs] def gen_poes(self, ctx): """ :param ctx: a vectorized context (recarray) of size N :param rup_indep: rupture flag (false for mutex ruptures) :yields: poes, mea_sig, ctxt with poes of shape (N, L, G) """ ctx.mag = numpy.round(ctx.mag, 3) for mag in numpy.unique(ctx.mag): ctxt = ctx[ctx.mag == mag] self.cfactor += [len(ctxt), 1] for poes, mea, sig, tau, slc in self._gen_poes(ctxt): # NB: using directly 64 bit poes would be slower without reason # since with astype(F64) the numbers are identical yield poes.astype(F64), mea, sig, tau, ctxt[slc]
# called by get_rmap, also documented in the manual
[docs] def get_pmap(self, ctx, tom=None, rup_mutex={}): """ :param ctx: a context array :param tom: temporal occurrence model (default PoissonTom) :param rup_mutex: dictionary of weights (default empty) :returns: a MapArray """ rup_indep = not rup_mutex sids = numpy.unique(ctx.sids) pmap = MapArray(sids, size(self.imtls), len(self.gsims)).fill(rup_indep) self.tom = tom or PoissonTOM(self.investigation_time) self.update(pmap, ctx, rup_mutex) return ~pmap if rup_indep else pmap
[docs] def get_rmap(self, ctx): """ Used for debugging :param ctx: a context array :returns: a MapArray of annual rates of shape (N, L, G) """ rmap = (~self.get_pmap(ctx)).to_rates() rmap.gid = self.gid rmap.wei = self.wei return rmap
[docs] def update(self, pmap, ctx, rup_mutex=None): """ :param pmap: probability map to update :param ctx: a context array :param rup_mutex: dictionary (src_id, rup_id) -> weight """ for poes, mea, sig, tau, ctxt in self.gen_poes(ctx): # ctxt contains an unique magnitude if rup_mutex: pmap.update_mutex(poes, ctxt, self.tom.time_span, rup_mutex) elif self.cluster: # in classical/case_35 for poe, sidx in zip(poes, pmap.sidx[ctxt.sids]): pmap.array[sidx] *= 1. - poe else: pmap.update_indep(poes, ctxt, self.tom.time_span)
# called by gen_poes and by the GmfComputer
[docs] def get_mean_stds(self, ctxs, split_by_mag=True): """ :param ctxs: a list of contexts with N=sum(len(ctx) for ctx in ctxs) :param split_by_mag: where to split by magnitude :returns: an array of shape (4, G, M, N) with mean and stddevs """ N = sum(len(ctx) for ctx in ctxs) M = len(self.imts) G = len(self.gsims) if all(isinstance(ctx, numpy.recarray) for ctx in ctxs): # contexts already vectorized recarrays = ctxs elif all(isinstance(ctx, numpy.ndarray) for ctx in ctxs): recarrays = [concat(ctxs)] else: # vectorize the contexts recarrays = [self.recarray(ctxs)] if split_by_mag: recarr = numpy.concatenate( recarrays, dtype=recarrays[0].dtype).view(numpy.recarray) recarrays = split_array(recarr, U32(numpy.round(recarr.mag*100))) out = numpy.empty((4, G, M, N)) for g, gsim in enumerate(self.gsims): if gsim.conditional and not gsim.from_mgmpe: url = ('https://docs.openquake.org/oq-engine/master/manual/' 'contributing/implementing-new-gsim.html') raise NotImplementedError( f'{gsim} was not instantiated with a ' f'ModifiableGMPE, please read {url}') out[:, g] = self.get_4MN(recarrays, gsim) return out
[docs] def get_4MN(self, ctxs, gsim): """ Called by the GmfComputer """ N = sum(len(ctx) for ctx in ctxs) M = len(self.imts) out = numpy.zeros((4, M, N)) gsim.adj = [] # NSHM2014P adjustments compute = gsim.__class__.compute start = 0 for ctx in ctxs: slc = slice(start, start + len(ctx)) adj = compute(gsim, ctx, self.imts, *out[:, :, slc]) if adj is not None: gsim.adj.append(adj) start = slc.stop if self.truncation_level not in (0, 1E-9, 99.) and (out[1] == 0.).any(): raise ValueError('Total StdDev is zero for %s' % gsim) if gsim.adj: gsim.adj = numpy.concatenate(gsim.adj) if self.conv: # apply horizontal component conversion self.horiz_comp_to_geom_mean(out, gsim) return out
# not used right now
[docs] def get_att_curves(self, site, msr, mag, aratio=1., strike=0., dip=45., rake=-90): """ :returns: 4 attenuation curves mea, sig, tau, phi (up to 500 km from the site at steps of 5 km) """ from openquake.hazardlib.source import rupture rup = rupture.get_planar( site, msr, mag, aratio, strike, dip, rake, self.trt) ctx = self.from_planar(rup, hdist=500, step=5) mea, sig, tau, phi = self.get_mean_stds([ctx]) return (interp1d(ctx.rrup, mea), interp1d(ctx.rrup, sig), interp1d(ctx.rrup, tau), interp1d(ctx.rrup, phi))
# tested in test_collapse_small
[docs] def estimate_weight(self, src, srcfilter): """ :param src: a source object :param srcfilter: a SourceFilter instance :returns: an estimate of the CPU cost of processing the source """ t0 = time.time() if src.nsites == 0: # was discarded by the prefiltering return EPS sites = srcfilter.get_close_sites(src) if sites is None: # may happen for CollapsedPointSources return EPS src.nsites = len(sites) if src.bysrc_unc: # NB: the sources modified by the uncertainties are not split # here (see filter_weight) but in classical, after modifying # them; generating all their contexts now would be expensive # (the area sources go through the rupture-by-rupture path of # gen_contexts) and useless, since the estimate is used only # to split the work in tasks. Since each source is processed # once per set of realizations with different uncertainties, # the cost is proportional to the number of sets; NB: the # import is local to avoid a circular dependency from openquake.hazardlib.lt import get_ts_sets C = src.num_ruptures * src.nsites * len(get_ts_sets(src)) else: step = (1 if src.code in b'pP' else 20 if src.num_ruptures >= 400 else 4) C = sum(len(ctx) for ctx in self.get_ctxs(src, sites, step=step)) # the corrections compensate the subsampling of the ruptures if src.code in b'SFN': C *= step**2 elif src.code in b'CKX': C *= step src.dt = time.time() - t0 if not C: return EPS N = len(srcfilter.sitecol.complete) src.nctxs = C * srcfilter.multiplier # The surviving contexts are not the whole story: the contexts of a # pointlike source are generated for *all* the (rupture, site) pairs # and then filtered by rrup < magdist, so a collapsed point source # with thousands of ruptures and a couple of surviving contexts is # much more expensive than its nctxs suggests. The number of pairs # is the relevant quantity, discounted by PAIR_COST pairs = src.num_ruptures * src.nsites * srcfilter.multiplier return (src.nctxs + pairs / PAIR_COST) / N
[docs] def set_weight(self, sources, srcfilter): """ Set the weight attribute on each prefiltered source """ if srcfilter.sitecol is None: for src in sources: src.weight = EPS else: for src in sources: src.weight = self.estimate_weight(src, srcfilter)
def __repr__(self): rlzs = sum(len(rlzs) for rlzs in self.gsims.values()) return f'<{self.__class__.__name__} {self.trt} {rlzs=}>'
# see contexts_tests.py for examples of collapse # probs_occur = functools.reduce(combine_pmf, (r.probs_occur for r in rups))
[docs]def combine_pmf(o1, o2): """ Combine probabilities of occurrence; used to collapse nonparametric ruptures. :param o1: probability distribution of length n1 :param o2: probability distribution of length n2 :returns: probability distribution of length n1 + n2 - 1 >>> combine_pmf([.99, .01], [.98, .02]) array([9.702e-01, 2.960e-02, 2.000e-04]) """ n1 = len(o1) n2 = len(o2) o = numpy.zeros(n1 + n2 - 1) for i in range(n1): for j in range(n2): o[i + j] += o1[i] * o2[j] return o
def _get_poes(mean_std, loglevels, phi_b): # returns a matrix of shape (N, L) N = mean_std.shape[2] # shape (2, M, N) out = numpy.empty((loglevels.size, N), F32) # shape (L, N) _set_poes(mean_std, loglevels, phi_b, out) return out.T
[docs]def set_poes(gsim, mean_std, cmaker, ctx, out, slc): """ Calculate and return probabilities of exceedance (PoEs) of one or more intensity measure levels (IMLs) of one intensity measure type (IMT) for one or more pairs "site -- rupture". :param gsim: A GMPE instance :param mean_std: An array of shape (2, M, N) with mean and standard deviations for the sites and intensity measure types :param cmaker: A ContextMaker instance, used only in nhsm_2014 :param ctx: A context array used only in avg_poe_gmpe :param out: An array of PoEs of shape (N, L) to be filled :param slc: A slice object used only in avg_poe_gmpe :raises ValueError: If truncation level is not ``None`` and neither non-negative float number, and if ``imts`` dictionary contain wrong or unsupported IMTs (see :attr:`DEFINED_FOR_INTENSITY_MEASURE_TYPES`). """ loglevels = cmaker.loglevels.array phi_b = cmaker.phi_b _M, L1 = loglevels.shape if hasattr(gsim, 'weights_signs'): # for nshmp_2014, case_72 adj = gsim.adj[slc] outs = [] weights, signs = zip(*gsim.weights_signs) for s in signs: ms = numpy.array(mean_std) # make a copy for m in range(len(loglevels)): ms[0, m] += s * adj outs.append(_get_poes(ms, loglevels, phi_b)) out[:] = numpy.average(outs, weights=weights, axis=0) elif hasattr(gsim, 'mixture_model'): for f, w in zip(gsim.mixture_model["factors"], gsim.mixture_model["weights"]): mean_stdi = mean_std.copy() mean_stdi[1] *= f # multiply stddev by factor out[:] += w * _get_poes(mean_stdi, loglevels, phi_b) elif hasattr(gsim, 'weights'): # avg_poe_gmpe cm = copy.copy(cmaker) cm.poe_mon = Monitor() # avoid double counts cm.gsims = gsim.gsims avgs = [] for poes, _mea, _sig, _tau, _ctx in cm.gen_poes(ctx[slc]): # poes has shape N, L, G avgs.append(poes @ gsim.weights) out[:] = numpy.concatenate(avgs) else: # regular case _set_poes(mean_std, loglevels, phi_b, out.T) imtweight = getattr(gsim, 'weight', None) # ImtWeight or None for m, imt in enumerate(cmaker.imtls): mL1 = m * L1 if imtweight and imtweight.dic.get(imt) == 0: # set by the engine when parsing the gsim logictree # when 0 ignore the contribution: see _build_branches out[:, mL1:mL1 + L1] = 0
# used in boore_atkinson_2008
[docs]def get_dists(ctx): """ Extract the distance parameters from a context. :returns: a dictionary dist_name -> distances """ return {par: dist for par, dist in vars(ctx).items() if par in KNOWN_DISTANCES}
# used in a few hazardlib tests, more or less replaces # gsim.get_mean_and_stddevs(rctx, rctx, rctx, imt, stddev_types)
[docs]def mean_stds(rctx, gsim, imt, idx=[0, 1, 2, 3]): """ :param rctx: a RuptureContext with site and distance information :param gsim: a GSIM instance :param imt: an IMT string :param idx: index (or list of indices) in the range 0..3 (default [0,1,2,3]) :returns: array of shape N or (len(idx), N) """ cmaker = simple_cmaker([gsim], [imt]) ctx = cmaker.recarray([rctx]) out = cmaker.get_mean_stds([ctx]) return out[idx, 0, 0]
[docs]def get_mean_stds(gsim, ctx, imts, return_dicts=False, **kw): """ :param gsim: a single GSIM or a a list of GSIMs :param ctx: a RuptureContext or a recarray of size N with same magnitude :param imts: a list of M IMT objects :param return_dicts: if True, returns 4 dictionaries keyed by IMT strings :param kw: additional keyword arguments :returns: an array of shape (4, M, N) obtained by applying the given GSIM, ctx amd imts, or an array of shape (G, 4, M, N) """ single = hasattr(gsim, 'compute') kw['imtls'] = {imt.string: [0] for imt in imts} cmaker = ContextMaker('*', [gsim] if single else gsim, kw) out = cmaker.get_mean_stds([ctx], split_by_mag=False) # (4, G, M, N) out = out[:, 0] if single else out if return_dicts: assert single return [{imt.string: out[o, m] for m, imt in enumerate(imts)} for o in range(4)] return out
# mock of a rupture used in the tests and in the SMT module of the OQ-MBTK
[docs]class RuptureContext: """ Rupture calculation context for ground shaking intensity models. Instances of this class represent relevant features of a single rupture. Every GSIM class is required to declare what :attr:`rupture parameters <GroundShakingIntensityModel.REQUIRES_RUPTURE_PARAMETERS>` does it need. Only those required parameters are made available in a result context object. """ src_id = 0 rup_id = 0 _slots_ = ( 'mag', 'strike', 'dip', 'rake', 'ztor', 'hypo_lon', 'hypo_lat', 'hypo_depth', 'width', 'hypo_loc', 'length', 'src_id', 'rup_id') def __init__(self, param_pairs=()): for param, value in param_pairs: setattr(self, param, value)
[docs] def size(self): """ If the context is a multi rupture context, i.e. it contains an array of magnitudes and it refers to a single site, returns the size of the array, otherwise returns 1. """ nsites = len(self.sids) if nsites == 1 and isinstance(self.mag, numpy.ndarray): return len(self.mag) return nsites
# used in acme_2019 def __len__(self): return len(self.sids) def __eq__(self, other): """ Return True if ``other`` has same attributes with same values. """ if isinstance(other, self.__class__): if self._slots_ == other._slots_: oks = [] for s in self._slots_: a, b = getattr(self, s, None), getattr(other, s, None) if a is None and b is None: ok = True elif a is None and b is not None: ok = False elif a is not None and b is None: ok = False elif hasattr(a, 'shape') and hasattr(b, 'shape'): if a.shape == b.shape: ok = numpy.allclose(a, b) else: ok = False else: ok = a == b oks.append(ok) return numpy.all(oks) return False