Source code for openquake.hazardlib.source_reader

# -*- coding: utf-8 -*-
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright (C) 2019, 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 time
import copy
import hashlib
import json
import os.path
import pickle
import operator
import logging
import numpy

from openquake.baselib import parallel, performance, general, hdf5
from openquake.hazardlib import (
    geo, nrml, source, sourceconverter, InvalidFile, calc)
from openquake.hazardlib.source_group import (
    CompositeSourceModel, SourceGroup, get_unique)
from openquake.hazardlib.source.multi_fault import save_and_split
from openquake.hazardlib.lt import (
    apply_uncertainties, check_correlated, get_bset_value,
    restrict_ts_samples, ts_samples_dt, get_ts_sets)
from openquake.hazardlib.rmap_maker import get_unique_inverse
from openquake.hazardlib.valid import basename

TWO24 = 2**24
TWO32 = 2**32

CLASSICAL_MODES = ('classical', 'classical_risk', 'classical_damage',
                   'classical_bcr', 'disaggregation', 'preclassical')
U16 = numpy.uint16
U32 = numpy.uint32
F32 = numpy.float32
bybranch = operator.attrgetter('branch')
checksum = operator.attrgetter('checksum')

source_info_dt = numpy.dtype([
    ('source_id', hdf5.vstr),          # 0
    ('grp_id', U16),                   # 1
    ('code', (numpy.bytes_, 1)),       # 2
    ('calc_time', F32),                # 3
    ('num_ctxs', numpy.uint64),        # 4
    ('est_ctxs', numpy.uint64),        # 5
    ('num_ruptures', U32),             # 6
    ('weight', F32),                   # 7
    ('mul', U16),                      # 8
])


[docs]def ts_samples(samples, trt_smr): """ :returns: a structured array (trt_smr, samples) of length 1 """ return numpy.array([(trt_smr, samples, 0)], ts_samples_dt)
def _copy_uncertainty_attrs(src, segment): # the segments of a split source must keep the ts_samples and the # flag bysrc_unc, otherwise the uncertainties are applied to the # base realization only, see get_ts_sets segment.ts_samples = src.ts_samples segment.bysrc_unc = src.bysrc_unc # NB: blocksize is chosen so that event_based/case_35 works
[docs]def splitMF(sources, disagg_by_src, blocksize=1000): """ Split the MultiPointSources to avoid oceanic sources with 160M ruptures hanging during rupture sampling """ splits = [] for src in sources: if src.code == b'M' and len(src) > blocksize: for i, slc in enumerate(general.gen_slices(0, len(src), blocksize)): segment = source.MultiPointSource( source_id=f'{src.source_id}:{i}', name=src.name, tectonic_region_type=src.tectonic_region_type, mfd=src.mfd[slc], magnitude_scaling_relationship=( src.magnitude_scaling_relationship), rupture_aspect_ratio=src.rupture_aspect_ratio, upper_seismogenic_depth=src.upper_seismogenic_depth, lower_seismogenic_depth=src.lower_seismogenic_depth, nodal_plane_distribution=src.nodal_plane_distribution, hypocenter_distribution=src.hypocenter_distribution, mesh=geo.Mesh(src.mesh.lons[slc], src.mesh.lats[slc]), temporal_occurrence_model=src.temporal_occurrence_model) _copy_uncertainty_attrs(src, segment) splits.append(segment) elif src.code == b'F' and not disagg_by_src: # use the colon convention only in absence of kendra-splitting for segment in src: _copy_uncertainty_attrs(src, segment) splits.append(segment) else: splits.append(src) sources[:] = splits
[docs]def check_unique(ids, msg='', strict=True): """ Raise a DuplicatedID exception if there are duplicated IDs """ if isinstance(ids, dict): # ids by key all_ids = sum(ids.values(), []) unique, counts = numpy.unique(all_ids, return_counts=True) for dupl in unique[counts > 1]: keys = [k for k in ids if dupl in ids[k]] if keys: errmsg = '%r appears in %s %s' % (dupl, keys, msg) if strict: raise nrml.DuplicatedID(errmsg) else: logging.info('*' * 60 + ' DuplicatedID:\n' + errmsg) return unique, counts = numpy.unique(ids, return_counts=True) for u, c in zip(unique, counts): if c > 1: errmsg = '%r appears %d times %s' % (u, c, msg) if strict: raise nrml.DuplicatedID(errmsg) else: logging.info('*' * 60 + ' DuplicatedID:\n' + errmsg)
[docs]def create_source_info(csm, h5): """ Creates source_info, trt_smrs, toms """ csm.source_info = csm.get_source_info() num_srcs = len(csm.source_info) # avoid hdf5 damned bug by creating source_info in advance h5.create_dataset('source_info', (num_srcs,), source_info_dt)
[docs]def trt_smrs(src): return tuple(src.trt_smrs)
def _sample_atomic_groups(src_groups, sample, applied): # Atomic groups are sampled as units; applied sources are always retained. atomic = [sg for sg in src_groups if sg.atomic] if not atomic: return src_groups selected = general.random_filter(atomic, float(sample)) selected.extend(sg for sg in atomic if any( src.source_id in applied for src in sg)) if not selected: selected = [atomic[0]] selected_ids = {id(sg) for sg in selected} return [sg for sg in src_groups if not sg.atomic or id(sg) in selected_ids]
[docs]def read_source_model(fname, branch, converter, applied, sample, monitor): """ :param fname: path to a source model XML file :param branch: source model logic tree branch ID :param converter: SourceConverter :param applied: list of source IDs within applyToSources :param sample: a string with the sampling factor (if any) :param monitor: a Monitor instance :returns: a SourceModel instance """ t0 = time.time() [sm] = nrml.read_source_models([fname], converter) sm.branch = branch if sample: sm.src_groups = _sample_atomic_groups(sm.src_groups, sample, applied) for sg in sm.src_groups: if sample and not sg.atomic: srcs = [] for src in sg: if src.source_id in applied: srcs.append(src) else: srcs.extend(calc.filters.split_source(src)) if srcs: kept = [src for src in srcs if src.source_id in applied] rand = general.random_filter(srcs, float(sample)) sg.sources = (kept + rand) or [srcs[0]] else: sg.sources = [] sm.rtime = time.time() - t0 # save the read time return {fname: sm}
# NB:this is called after reduce_sources, so ";" is not added # if the same source appears multiple times, i.e. len(srcs) == 1
[docs]def add_semicolons(src_groups): """ Add semicolons to differentiate different sources with the same source_id and then sort the sources in each group by extended source_id """ sources = general.AccumDict(accum=[]) for sg in src_groups: for src in sg: sources[src.source_id].append(src) for src_id, srcs in sources.items(): srcs = get_unique(srcs) if len(srcs) > 1: # happens in logictree/case_01/rup.ini for i, src in enumerate(srcs): src.source_id = '%s;%d' % (src.source_id, i) for sg in src_groups: sg.sources.sort(key=operator.attrgetter('source_id'))
# tested in logictree/case_05 with 9 variations of # AreaSource 1 and SimpleFaultSource 2
[docs]def check_branchID(branchID): """ Forbids invalid characters .:; used in fragmentno """ if '.' in branchID: raise InvalidFile('branchID %r contains an invalid "."' % branchID) elif ':' in branchID: raise InvalidFile('branchID %r contains an invalid ":"' % branchID) elif ';' in branchID: raise InvalidFile('branchID %r contains an invalid ";"' % branchID)
[docs]def check_duplicates(smdict, strict): # check_duplicates in the same file for sm in smdict.values(): srcids = [] for sg in sm.src_groups: srcids.extend(src.source_id for src in sg) if sg.src_interdep == 'mutex': # mutex sources in the same group must have all the same # basename, i.e. the colon convention must be used basenames = set(map(basename, sg)) assert len(basenames) == 1, basenames check_unique(srcids, 'in ' + sm.fname, strict) # check duplicates in different files but in the same branch # the problem was discovered in the DOM model for branch, sms in general.groupby(smdict.values(), bybranch).items(): srcids = general.AccumDict(accum=[]) fnames = [] for sm in sms: if isinstance(sm, nrml.GeometryModel): # the section IDs are not checked since they not count # as real sources continue for sg in sm.src_groups: srcids[sm.fname].extend(src.source_id for src in sg) fnames.append(sm.fname) check_unique(srcids, 'in branch %s' % branch, strict=strict)
[docs]def save_read_times(dstore, source_models): """ Store how many seconds it took to read each source model file in a table (fname, rtime) """ dt = [('fname', hdf5.vstr), ('rtime', float)] arr = numpy.array([(sm.fname, sm.rtime) for sm in source_models], dt) dstore.create_dset('source_model_read_times', arr)
[docs]def get_csm(oq, full_lt, dstore=None): """ Build a CompositeSourceModel without applying the uncertainties, that are applied in the workers, see modified_groups. """ converter = sourceconverter.SourceConverter( oq.investigation_time, oq.rupture_mesh_spacing, oq.complex_fault_mesh_spacing, oq.width_of_mfd_bin, oq.area_source_discretization, oq.minimum_magnitude, oq.source_id, discard_trts=[s.strip() for s in oq.discard_trts.split(',')], floating_x_step=oq.floating_x_step, floating_y_step=oq.floating_y_step, source_nodes=oq.source_nodes, infer_occur_rates=oq.infer_occur_rates, filter_sourcecodes=oq.filter_sourcecodes) full_lt.ses_seed = oq.ses_seed logging.info('Reading the source model(s) in parallel') # NB: the source models file must be in the shared directory # NB: dstore is None in logictree_test.py allargs = [] sdata = full_lt.source_model_lt.source_data allpaths = set(full_lt.source_model_lt.info.smpaths) dic = general.group_array(sdata, 'fname') smpaths = [] ss = os.environ.get('OQ_SAMPLE_SOURCES') applied = set() for srcs in full_lt.source_model_lt.info.applytosources.values(): applied.update(srcs) for fname, rows in dic.items(): path = os.path.abspath( os.path.join(full_lt.source_model_lt.basepath, fname)) smpaths.append(path) allargs.append((path, rows[0]['branch'], converter, applied, ss)) for path in allpaths - set(smpaths): # geometry models allargs.append((path, '', converter, applied, ss)) smdict = parallel.Starmap(read_source_model, allargs, h5=dstore if dstore else None).reduce() parallel.Starmap.shutdown() # save memory smdict = {k: smdict[k] for k in sorted(smdict)} if dstore: save_read_times(dstore, smdict.values()) check_duplicates(smdict, strict=oq.disagg_by_src) found = add_bangs(smdict) if found: logging.info('Found different sources with same ID %s', general.shortlist(found)) check_same_sources(smdict, oq.inputs.get( 'source_model_logic_tree', oq.inputs['job_ini'])) # checking ps_grid_spacing pointlike_sources = 0 for sm in smdict.values(): for sg in sm.src_groups: for src in sg: if src.code in b'PAM': pointlike_sources += 1 break if (oq.strict and oq.mosaic_model and pointlike_sources and 'classical' in oq.calculation_mode and oq.ps_grid_spacing == 0 and not oq.sites and not oq.disagg_by_src): raise InvalidFile(f'{oq.inputs["job_ini"]}: ' 'missing ps_grid_spacing') return build_csm(oq, full_lt, smdict, dstore)
[docs]def get_bset_values(full_lt, sources): """ :param full_lt: a FullLogicTree instance :param sources: a SourceGroup or a list of sources of the same group :returns: the dictionary of uncertainties to apply expected by modified_groups, i.e. one entry for each set of realizations with the same uncertainties; the uncertainties of a set are the ones of its first realization NB: only the uncertainties relevant for the given sources are returned, since the logic tree can be huge and the dictionary is sent to the workers """ ordinals = {trt_smrs[0] % TWO24 for src in sources for trt_smrs in get_ts_sets(src)} return {ordinal: full_lt.get_bset_values(ordinal) for ordinal in sorted(ordinals)}
[docs]def modified_groups(sources, bset_values): """ Apply the uncertainties to a group of sources built *without* them, as needed by the workers computing the rates or the ruptures: this is done one set of realizations at a time, i.e. one set with the same uncertainties at a time (see build_groups). :param sources: a SourceGroup or a list of sources of the same group :param bset_values: the uncertainties to apply, as returned by get_bset_values; it can be empty, if there are no uncertainties at all :returns: a generator of (trt_smrs, group) pairs, one for each set of realizations with the same uncertainties, with the uncertainties applied and the ts_samples restricted to the set of realizations """ for trt_smrs, srcs in _subsets_by_unc(sources).items(): grp = _restricted_group(sources, srcs, trt_smrs) # NB: the trt_smrs are trti * TWO24 + ordinal, see gen_groups bvals = bset_values[trt_smrs[0] % TWO24] if bset_values else [] # NB: check=False since the group is a fragment of the original # one (split by weight in the preclassical), so the correlated # branchsets were already checked at build time, see build_groups grp = apply_uncertainties(bvals, grp, check=False) yield trt_smrs, grp
def _subsets_by_unc(sources): """ :returns: a dictionary trt_smrs -> sources, i.e. the sources grouped by set of realizations with the same uncertainties """ if getattr(sources, 'atomic', False): # the sources of an atomic group are mutually exclusive (or belong # to a cluster), so they must be kept together, see sample_cluster # and cmakers_groups_gkeys; the sets of realizations are the same for # all of them, since they belong to the same source model return {trt_smrs: list(sources) for trt_smrs in get_ts_sets(sources[0])} subsets = {} for src in sources: for trt_smrs in get_ts_sets(src): subsets.setdefault(trt_smrs, []).append(src) return subsets def _restricted_group(sources, srcs, trt_smrs): """ :returns: a group with the given sources, i.e. copies of the sources of `sources` with the ts_samples restricted to trt_smrs """ restricted = [restrict_ts_samples(src, trt_smrs) for src in srcs] if hasattr(sources, 'sources'): # keep the attributes of the group grp = copy.copy(sources) grp.sources = restricted else: # a plain list of sources, e.g. a block of sources grp = SourceGroup(sources[0].tectonic_region_type, restricted) return grp
[docs]def unc_signature(bset_values, src): """ :returns: a tuple identifying the uncertainties applied to src """ sig = [] for bset, value in bset_values: ok, val = get_bset_value(bset, value, src) if ok: sig.append((bset.id, str(val))) return tuple(sig)
[docs]def collect_sources(full_lt, rlz_groups): """ :param full_lt: a FullLogicTree instance :param rlz_groups: an iterator of (sm_rlz, source group) pairs :returns: a dictionary id(grp) -> [grp, {id(src): (src, pairs)}], where pairs is a list of (trt_smr, samples, signature) tuples, one per realization of the source """ dic = {} for rlz, grp in rlz_groups: trti = full_lt.trti.get(grp.trt, 0) trt_smr = trti * TWO24 + rlz.ordinal bset_values = full_lt.get_bset_values(rlz.ordinal) # NB: the uncertainties are applied later, in the workers, on # groups split by weight, so the correlated branchsets are checked # here, where the groups are still whole check_correlated(bset_values, grp) # NB: the sources are collected by id(grp), since the group objects # are shared by all the realizations selecting the same source model # file (see gen_groups); the groups built from them are then merged # by trt_smrs below. The dicts preserve the order of first # appearance, making the groups and the sources inside them # reproducible. srcs = dic.setdefault(id(grp), [grp, {}])[1] for src in grp: sig = unc_signature(bset_values, src) pairs = srcs.setdefault(id(src), (src, []))[1] pairs.append((trt_smr, rlz.samples, sig)) return dic
[docs]def source_with_ts_sets(src, pairs, sigrows): """ :param src: a source :param pairs: a list of (trt_smr, samples, signature) tuples, one per realization of the source :param sigrows: a list of rows describing the uncertainty signatures, extended with the signatures of the source :returns: a copy of the source with the attributes ts_samples and bysrc_unc set, i.e. the sets of realizations with the same uncertainties are stored in the field ts """ sigdict = {} for trt_smr, samples, sig in pairs: sigdict.setdefault(sig, []).append(trt_smr) # NB: the field ts is the smallest trt_smr of the set of realizations # with the same uncertainties; it is 0 for all the realizations if # the uncertainties are the same everywhere (or there are none), so # the sources without uncertainties keep their ts_samples as it is tsdict = {sig: min(t) for sig, t in sigdict.items()} multi = len(sigdict) > 1 arrays = [(trt_smr, samples, tsdict[sig] if multi else 0) for trt_smr, samples, sig in pairs] new_src = copy.copy(src) new_src.ts_samples = numpy.array( sorted(arrays), ts_samples_dt) # sorted by trt_smr # flag the sources which will be modified in the workers: they must # not be split in the preclassical, since the splitting destroys the # geometry (and the MFD of the fault sources) new_src.bysrc_unc = any(sigdict) # NB: () means no uncertainty # the uncertainty signatures of the source, i.e. the sets of # realizations with the same uncertainties, which are the indices of # rate attribution for its rates (see get_ts_sets) for sig, trt_smrs in sigdict.items(): sigrows.append(dict(source_id=src.source_id, realizations=len(arrays), signature=dict(sig), count=len(trt_smrs))) return new_src
[docs]def build_groups(full_lt, rlz_groups, oq, dstore=None): """ Build the source groups without applying the uncertainties, as needed by the workers. The uncertainties to be applied in each realization are not known until the workers, so the realizations with different uncertainties are stored in the field ts of the ts_samples of each source (see get_ts_sets). """ # NB: the uncertainty signatures are stored in the datastore, so that # `oq check_input` can print them, see store_unc_signatures # NB: the same source_id can be used by different sources, i.e. in # different source models, so the sources are keyed by id(src) and not # by source_id; the ids are disambiguated at the end, by adding a # semicolon, see add_semicolons sigrows = [] # dicts with keys source_id, realizations, signature, count dic = collect_sources(full_lt, rlz_groups) out, atomic, acc = [], [], general.AccumDict(accum=[]) for grp, srcs in dic.values(): new_srcs = [source_with_ts_sets(src, pairs, sigrows) for src, pairs in srcs.values()] if grp.atomic: # the atomic groups are never merged with the other groups, # since their sources must be computed together new = copy.copy(grp) new.sources = new_srcs atomic.append(new) else: acc[grp.trt].extend(new_srcs) if atomic: logging.info('Found %d atomic groups', len(atomic)) # NB: the sources are grouped by trt_smrs and TOM, so that there is # one cmaker for each set of realizations, see get_cmakers red_sources = 0 for trt, sources in acc.items(): grps, red = _group_sources(trt, sources) out.extend(grps) red_sources += red if red_sources: logging.info('reduce_sources was called %d times', red_sources) out.extend(atomic) for grp in out: splitMF(grp.sources, oq.disagg_by_src) add_semicolons(out) # else sources with the same id are lost store_unc_signatures(out, sigrows, full_lt, oq, dstore) return out
[docs]def store_unc_signatures(groups, sigrows, full_lt, oq, dstore=None): """ Store the uncertainty signatures of the sources in the datastore, so that they can be printed by `oq check_input` (see the check_input command) and read back with `oq show unc_signatures`. NB: the signatures are stored as JSON objects, so that the unc_signatures view can display a column for each branchset. :param groups: a list of SourceGroups built without the uncertainties :param sigrows: a list of dicts with keys source_id, realizations, signature (a dictionary branchset_id -> value) and count :param full_lt: a FullLogicTree instance :param oq: an OqParam instance :param dstore: a DataStore instance or None """ if dstore is not None and any(row['signature'] for row in sigrows): # NB: the signatures are stored only if there are uncertainties, # i.e. only if the rates must be split in indices of rate # attribution dt = [('source_id', hdf5.vstr), ('realizations', int), ('signature', hdf5.vstr), ('count', int)] data = [(row['source_id'], row['realizations'], json.dumps( row['signature']), row['count']) for row in sigrows] dstore.create_df('unc_signatures', numpy.array(data, dt)) if oq.calculation_mode not in CLASSICAL_MODES: # the event based calculators attribute the rates to the # realizations of the group, not to the subsets return core_trt_smrs = get_core_trt_smrs(groups) if dstore is not None: dstore.hdf5.save_vlen('core_trt_smrs', core_trt_smrs) Gt = get_core_size(core_trt_smrs, full_lt) if Gt >= TWO32: # NB: the gids are stored as uint32 in the _rates dataset, so # there cannot be more than 2**32 columns in the RateMap raise ValueError( 'The core size Gt=%d is too large (the maximum is %d), ' 'you must reduce the logic tree' % (Gt, TWO32)) logging.warning('Core size Gt=%d out of R=%d realizations', Gt, full_lt.get_num_paths()) if dstore is not None and 'sitecol' in dstore: # NB: the sites are read after the sources, so the size in bytes # is logged only if they are already in the datastore N = len(dstore['sitecol']) imtls = oq.imtls L = imtls.size if hasattr(imtls, 'size') else len(imtls) logging.warning('Global RateMap of %s for %d sites and %d levels', general.humansize(4 * N * L * Gt), N, L)
[docs]def get_core_trt_smrs(groups): """ :param groups: a list of SourceGroups built without applying the uncertainties (i.e. the src_groups of a CompositeSourceModel) :returns: a sorted list of trt_smrs (to be stored as hdf5.vuint32) The uncertainties are applied in the workers, so the realizations with different uncertainties are not separated at build time (see build_groups). The rates are nevertheless computed separately for each set of uncertainties and must be attributed to the right realizations, hence this extra list; a rate is attributed to the unit containing its trt_smrs. """ all_trt_smrs = [trt_smrs for sg in groups for src in sg for trt_smrs in get_ts_sets(src)] unique, _inverse = get_unique_inverse(all_trt_smrs) return [numpy.array(trt_smrs, numpy.uint32) for trt_smrs in unique]
[docs]def get_core_size(core_trt_smrs, full_lt): """ :param core_trt_smrs: the sets of realizations with the same uncertainties, as returned by get_core_trt_smrs :param full_lt: a FullLogicTree instance :returns: the core size Gt = Σ_i G(trt_i) of the logic tree, i.e. the number of distinct rate components, i.e. of gids (see FullLogicTree.get_gids), which is >= len(core_trt_smrs) since each unit contributes G(trt_i) >= 1 gids """ return sum(len(full_lt.gsim_lt.values[full_lt.trts[t[0] // TWO24]]) for t in core_trt_smrs)
[docs]def build_csm(oq, full_lt, smdict, dstore): """ :param oq: OqParam instance :param full_lt: FullLogicTree instance :param smdict: dictionary source_model_path -> SourceModel instance :param dstore: DataStore instance :returns: a CompositeSourceModel instance """ with performance.Monitor('_build_groups', measuremem=True) as mon: rlz_groups = [] for rlz in full_lt.sm_rlzs: rlz_groups.extend((rlz, grp) for grp in gen_groups(full_lt, smdict, rlz)) logging.info(mon) logging.info('Building CompositeSourceModel') groups = build_groups(full_lt, rlz_groups, oq, dstore) csm = CompositeSourceModel(oq, full_lt, groups) store_data(oq, smdict, csm, dstore) return csm
[docs]def store_data(oq, smdict, csm, dstore): """ Create src_mutex, grp_probability in calc_XXX.hdf5 and sources and mf_sections in calc_XXX_tmp.hdf5 """ out = [] probs = [] for sg in csm.src_groups: if sg.src_interdep == 'mutex' and 'src_mutex' not in dstore: segments = [] for src in sg: segments.append(src.source_id.split(':')[1]) t = (src.source_id, src.grp_id, src.num_ruptures, src.mutex_weight, sg.rup_interdep == 'mutex') out.append(t) probs.append((src.grp_id, sg.grp_probability)) assert len(segments) == len(set(segments)), segments if out: dtlist = [('src_id', hdf5.vstr), ('grp_id', int), ('num_ruptures', int), ('mutex_weight', float), ('rup_mutex', bool)] dstore.create_dset('src_mutex', numpy.array(out, dtlist)) lst = [('grp_id', int), ('probability', float)] dstore.create_dset('grp_probability', numpy.array(probs, lst)) # add rupids_by_tag to multifault sources if there is a single site try: sitecol = dstore['sitecol'] except (KeyError, TypeError): # 'NoneType' object is not subscriptable sitecol = None else: # NB: in AELO we can have multiple vs30 on the same location lonlats = set(zip(sitecol.lons, sitecol.lats)) if len(lonlats) > 1: sitecol = None # must be called *after* add_semicolons t0 = time.time() secparams = fix_geometry_sections( smdict, csm.src_groups, dstore.tempname if dstore else '', sitecol if oq.disagg_by_src and oq.use_rates else None, split=not oq.calculation_mode.startswith('event_based')) if secparams is not None and len(secparams): logging.info('Spent %.1f seconds in fix_geometry_sections', time.time()-t0)
# attributes which are not part of the content of a source NOT_CONTENT = {'source_id', 'name', 'ts_samples', 'branch', 'width_of_mfd_bin', 'checksum'}
[docs]def content_hash(src, exclude=NOT_CONTENT): """ :param src: a source object :param exclude: the attributes to ignore :returns: the md5 hex digest of the other attributes of the source NB: by default the ID and the name are ignored, i.e. two copies of the same source with different IDs or names have the same hash """ dic = {k: v for k, v in vars(src).items() if k not in exclude} return hashlib.md5(pickle.dumps(dic, protocol=4)).hexdigest()
# called by reduce_sources
[docs]def add_checksums(srcs): """ Build and attach a checksum to each source """ # width_of_mfd_bin is set by modify_set_recurrow; the name is not part # of the checksum, i.e. sources with the same content are reduced even # if they have different names for src in srcs: src.checksum = content_hash(src)
# called before add_semicolons
[docs]def add_bangs(smdict): """ Discriminate different sources with same ID (false duplicates) and put an exclamation mark in their source ID """ acc = general.AccumDict(accum=[]) atomic = set() for smodel in smdict.values(): for sgroup in smodel.src_groups: for src in sgroup: src.branch = smodel.branch srcid = (src.source_id if sgroup.atomic else basename(src)) acc[srcid].append(src) if sgroup.atomic: atomic.add(src.source_id) found = [] for srcid, srcs in acc.items(): if len(srcs) > 1: # duplicated ID if any(src.source_id in atomic for src in srcs): raise RuntimeError('Sources in atomic groups cannot be ' 'duplicated: %s', srcid) if any(getattr(src, 'mutex_weight', 0) for src in srcs): raise RuntimeError('Mutually exclusive sources cannot be ' 'duplicated: %s', srcid) add_checksums(srcs) gb = general.AccumDict(accum=[]) for src in srcs: gb[checksum(src)].append(src) if len(gb) > 1: for same_checksum in gb.values(): for src in same_checksum: check_branchID(src.branch) src.source_id += '!%s' % src.branch found.append(srcid) return found
[docs]def check_same_sources(smdict, fname): """ Log a warning if the same source (i.e. a source with the same content) appears in different source models with different IDs: then the engine cannot recognize the copies as the same source, i.e. it cannot reduce them (see reduce_sources), and the source is computed once per source model. :param smdict: a dictionary source model file -> SourceModel :param fname: the source model logic tree file, used in the warning """ # NB: the atomic groups are skipped, since their sources cannot be # duplicated anyway (see add_bangs); the sources are hashed only if # their code and TRT appear in more than one source model file, to # save time bykey = general.AccumDict(accum=[]) # (code, trt) -> [(smfile, src)] for smfile, smodel in smdict.items(): for sgroup in smodel.src_groups: if sgroup.atomic: continue for src in sgroup: bykey[src.code, src.tectonic_region_type].append( (smfile, src)) errors = [] for pairs in bykey.values(): if len({smfile for smfile, _ in pairs}) < 2: continue byhash = general.AccumDict(accum=[]) for smfile, src in pairs: byhash[content_hash(src)].append((smfile, src)) for same in byhash.values(): smfiles = {smfile for smfile, _ in same} # NB: the original IDs, without the bangs added by add_bangs # for sources with the same ID and a different content ids = {basename(src).split('!')[0] for _, src in same} if len(smfiles) > 1 and len(ids) > 1: errors.append(sorted({'%s[%s]' % ( basename(src).split('!')[0], os.path.basename(smfile)) for smfile, src in same})) if errors: lines = [' ' + general.shortlist(err) for err in errors] msg = ('%s: %d sources appear in different source models with ' 'different IDs, use the same ID for each of them:\n%s' % ( fname, len(errors), '\n'.join(lines))) # NB: in oq check_input the models must be fixed, so it is an error if os.environ.get('OQ_CHECK_INPUT'): raise InvalidFile(msg) logging.warning(msg)
[docs]def fix_geometry_sections(smdict, src_groups, hdf5path='', site1=None, split=True): """ If there are MultiFaultSources, fix the sections according to the GeometryModels (if any). """ gmodels = [] gfiles = [] for fname, mod in smdict.items(): if isinstance(mod, nrml.GeometryModel): gmodels.append(mod) gfiles.append(fname) # merge and reorder the sections sec_ids = [] sections = {} for gmod in gmodels: sec_ids.extend(gmod.sections) sections.update(gmod.sections) check_unique(sec_ids, 'section ID in files ' + ' '.join(gfiles)) if sections: # save in the temporary file sources and sections assert hdf5path, ('You forgot to pass the dstore to ' 'get_composite_source_model') mfsources = [] for sg in src_groups: for src in sg: if src.code == b'F': mfsources.append(src) if mfsources: split_dic, secparams = save_and_split( mfsources, sections, hdf5path, site1, split=split) for sg in src_groups: new = [] for src in sg.sources: tag = src.source_id if tag in split_dic: new.extend(split_dic[tag]) else: new.append(src) sg.sources[:] = new return secparams return None
def _groups_ids(smlt_dir, smdict, fnames): # extract the source groups and ids from a sequence of source files groups = [] for fname in fnames: fullname = os.path.abspath(os.path.join(smlt_dir, fname)) groups.extend(smdict[fullname].src_groups) return groups, set(src.source_id for grp in groups for src in grp) def _add_ts_samples(src, rlz, trti): # associate the source to the ts_samples parameters of the realization; # the same source can appear in multiple realizations, hence the list. # NB: & and it enters # the classical calculations too, via SourceGroup.fix_src_offset and # the source_info rows, so the ts_samples must be always set sampl = ts_samples(rlz.samples, trti * TWO24 + rlz.ordinal) if src.ts_samples is None: # the first time src.ts_samples = [sampl] else: # if the same source belongs to multiple realizations src.ts_samples.append(sampl)
[docs]def gen_groups(full_lt, smdict, rlz): # yield all the possible source groups from the given rlz smlt_file = full_lt.source_model_lt.filename smlt_dir = os.path.dirname(smlt_file) src_groups, source_ids = _groups_ids( smlt_dir, smdict, rlz.value[0].split()) bset_values = full_lt.source_model_lt.bset_values(rlz.lt_path) if rlz.ordinal % 100 == 0: logging.info('Building source groups for rlz' f'#{rlz.ordinal}: {"_".join(rlz.lt_path)}') while (bset_values and bset_values[0][0].uncertainty_type == 'extendModel'): (_bset, value), *bset_values = bset_values extra, extra_ids = _groups_ids(smlt_dir, smdict, value.split()) common = source_ids & extra_ids if common: raise InvalidFile( '%s contains source(s) %s already present in %s' % (value, common, rlz.value)) src_groups.extend(extra) # NB: the uncertainties are not applied here, but in the workers, # one set of realizations at a time, see modified_groups; the # ts_samples info is set anyway, since it determines the multiplicity for src_group in src_groups: trti = full_lt.trti.get(src_group.trt, 0) for src in src_group: # tested in case_83_eb _add_ts_samples(src, rlz, trti) yield src_group # check applyToSources sm_branch = rlz.lt_path[0] src_id = full_lt.source_model_lt.info.applytosources[sm_branch] for srcid in src_id: if srcid not in source_ids: if full_lt.source_model_lt.branchID: continue raise ValueError( "The source %s is not in the source model," " please fix applyToSources in %s or the " "source model(s) %s" % (srcid, smlt_file, rlz.value[0].split()))
[docs]def reduce_sources(sources_with_same_id): """ :param sources_with_same_id: a list of sources with the same source_id :returns: a list of truly unique sources """ # first reduce identical sources having the same id(src) # tested in LogictreeTestCase.test_case_08, where <PoinstSource 2> # appears 3 times unique = get_unique(sources_with_same_id) out = [] add_checksums(unique) # in LogicTreeCase2ClassicalPSHA there 81 unique sources # grouped in 9 groups of 9 sources each with the same checksum for srcs in general.groupby(unique, checksum).values(): # NB: the simplest test featuring the same source in two # different source models is logictree/case_01 src = srcs[0] if len(srcs) > 1: # NB: each copy can have many realizations, since they are # collected before the reduction (see collect_sources), so # the realizations of all the copies must be kept src.ts_samples = numpy.sort(numpy.concatenate( [s.ts_samples for s in srcs]), order='trt_smr') out.append(src) return out
[docs]def split_by_tom(sources): """ Groups together sources with the same TOM and collect multifault sources """ def key(src): tom = getattr(src, 'temporal_occurrence_model', None) return (tom.__class__.__name__, src.code == b'F') return general.groupby(sources, key).values()
def _group_sources(trt, sources): """ Reduce identical sources, regroup by trt_smrs and TOM, then return (source_groups, reduction_count). """ key = operator.attrgetter('source_id', 'code') lst = [] red = 0 for srcs in general.groupby(sources, key).values(): if len(srcs) > 1: srcs = reduce_sources(srcs) red += 1 lst.extend(srcs) src_groups = [] for sources in general.groupby(lst, trt_smrs).values(): for grp in split_by_tom(sources): src_groups.append(sourceconverter.SourceGroup(trt, grp)) return src_groups, red