diff --git a/doc/api-reference/openquake.hazardlib.rst b/doc/api-reference/openquake.hazardlib.rst index afbf3e6c0942..ddc975ace042 100644 --- a/doc/api-reference/openquake.hazardlib.rst +++ b/doc/api-reference/openquake.hazardlib.rst @@ -111,6 +111,14 @@ site :undoc-members: :show-inheritance: +site_lt +------------------------------- + +.. automodule:: openquake.hazardlib.site_lt + :members: + :undoc-members: + :show-inheritance: + sourceconverter ------------------------------------------ diff --git a/doc/user-guide/inputs/site-model-inputs.rst b/doc/user-guide/inputs/site-model-inputs.rst index 5e8b51be2b69..49146299e3f9 100644 --- a/doc/user-guide/inputs/site-model-inputs.rst +++ b/doc/user-guide/inputs/site-model-inputs.rst @@ -127,5 +127,39 @@ We used this feature to split the ESHM20 model in two parts (Northern Europe and full hazard map was as trivial as joining the generated CSV files. Without the ``custom_site_id`` the site IDs would overlap, thus making impossible to join the outputs. -A geohash string (see https://en.wikipedia.org/wiki/Geohash) makes a good ``custom_site_id`` since it can enable the +A geohash string (see https://en.wikipedia.org/wiki/Geohash) makes a good ``custom_site_id`` since it can enable the unique identification of all potential sites across the globe. + +Site model logic tree +--------------------- + +The ``site_model_file`` can point to a NRML logic tree XML declaring alternative site models with weights, instead of +a single site model file. This adds a third leg to the SSC × GMM logic tree; under full enumeration realizations become +``R_SSC × R_GMM × R_SITE`` while under sampling all three legs are Monte-Carlo sampled ``num_samples`` times. Each +realization path gains a third ``~``-separated branch ID (e.g. ``A~A~B``). All branches must reference the same sites +(identical ``lon``/``lat`` and ``depth`` if present, in the same order, and identical field sets); only per-site parameter +values (``vs30``, ``z1pt0``, ``z2pt5``, ...) may differ. Branch files may be CSV or NRML ```` XML. *Site model +logic trees are currently only supported in classical and disaggregation.* + +Example logic tree XML:: + + + + + + + site_model_rock.csv + 0.6 + + + site_model_soil.csv + 0.4 + + + + + +Referenced from ``job.ini`` as:: + + [geometry] + site_model_file = site_model_lt.xml diff --git a/openquake/calculators/base.py b/openquake/calculators/base.py index 0c5b8ccbc596..24cb345dcb15 100644 --- a/openquake/calculators/base.py +++ b/openquake/calculators/base.py @@ -157,8 +157,10 @@ def get_weights(oq, dstore): :returns: float32 array of realization weights """ samples = oq.number_of_logic_tree_samples - if samples: - weights = numpy.ones(samples, dtype=F32)/samples + # Under a site-model LT weights may be non-uniform (late_weights), + # so always read them from the datastore in that case + if samples and 'full_lt/site_model_lt' not in dstore: + weights = numpy.ones(samples, dtype=F32) / samples else: weights = dstore['weights'][:] return weights @@ -844,6 +846,24 @@ def R(self): return 1 return len(get_weights(self.oqparam, self.datastore)) + def _overlay_sitecol(self, arr): + """ + Overlay ``arr``'s per-site params on ``self.sitecol`` and + ``dstore['sitecol/*']``; ``arr`` is a structured array with the + same rows as the sitecol (either a per-branch site model or a + copy used to restore it) + """ + # Geometry fields are shared across branches - never overlay + skip = {'lon', 'lat', 'depth', 'sids'} + h5 = self.datastore.hdf5 + for name in arr.dtype.names: + if name in skip: + continue + self.sitecol.array[name] = arr[name] + key = 'sitecol/' + name + if key in h5: + h5[key][:] = arr[name] + def read_exposure(self, haz_sitecol): # after load_crmodel """ Read the exposure, the risk models and update the attributes diff --git a/openquake/calculators/classical.py b/openquake/calculators/classical.py index ee6825a39232..e8f0f0fbf11e 100644 --- a/openquake/calculators/classical.py +++ b/openquake/calculators/classical.py @@ -36,7 +36,7 @@ from openquake.hazardlib.calc import disagg from openquake.hazardlib.map_array import ( RateMap, MapArray, rates_dt, check_hmaps, gen_chunks) -from openquake.commonlib import calc +from openquake.commonlib import calc, readinput from openquake.calculators import base, getters, preclassical, views get_weight = operator.attrgetter('weight') @@ -303,6 +303,10 @@ def postclassical(pgetter, hstats, individual_rlzs, amplifier, monitor): continue if R == 1 or individual_rlzs: for r in range(R): + # Under site LT each pgetter owns only its rlz_mask rlzs + if (pgetter.rlz_mask is not None + and not pgetter.rlz_mask[r]): + continue pmap_by_kind['hcurves-rlzs'][r].array[idx] = ( pc[:, r].reshape(M, L1)) if hstats: @@ -406,8 +410,10 @@ def agg_dicts(self, acc, dic): self.dparam_mb = max(dic.pop('dparam_mb'), self.dparam_mb) self.source_mb = max(dic.pop('source_mb'), self.source_mb) - # store rup_data if there are few sites - if self.few_sites and len(dic['rup_data']): + # Store rup_data if there are few sites; skip on site-LT re-runs + # since rupture data is source-only and would duplicate in rup/* + if (self.few_sites and len(dic['rup_data']) + and not getattr(self, '_skip_store_ctxs', False)): with self.monitor('saving rup_data'): store_ctxs(self.datastore, dic['rup_data'], grp_id) @@ -547,7 +553,10 @@ def execute(self): oq.inputs) self.source_data = AccumDict(accum=[]) sgs, ds = self._pre_execute() - self._execute(sgs, ds) + if getattr(self.full_lt, 'site_model_lt', None) is not None: + self._execute_epistemic_site(sgs, ds) + else: + self._execute(sgs, ds) if self.cfactor[0] == 0: if self.N == 1: logging.error('The site is far from all seismic sources' @@ -692,6 +701,48 @@ def check_mean_rates(self, mean_rates_by_src): ok = got[m] < 2. numpy.testing.assert_allclose(got[m, ok], exp[m, ok], atol=1E-5) + def _execute_epistemic_site(self, sgs, ds): + """ + Run :meth:`_execute` once per site-model realization; each branch's + rates are stored in a separate ``_rates_site_i`` group + """ + oq = self.oqparam + smep = readinput.get_site_models_epistemic(oq) + # Baseline site params from the canonical (first) branch + used = {r.site_rlz.ordinal + for r in self.full_lt.get_realizations() + if r.site_rlz is not None} + baseline = self.sitecol.array.copy() + for i, arr in enumerate(smep.arrays): + if i not in used: + continue + self._overlay_sitecol(arr) + self.rmap = {} + self.cfactor = numpy.zeros(2) + self.rel_ruptures = AccumDict(accum=0) + # Purge _rates so _execute starts clean; rup is source-only + # and is written once (see _skip_store_ctxs below) + h5 = self.datastore.hdf5 + for key in ('_rates/slice_by_idx', '_rates/sid', '_rates/lid', + '_rates/gid', '_rates/rate', '_rates', 'grp_keys'): + if key in h5: + del h5[key] + self.datastore.create_df( + '_rates', [(n, rates_dt[n]) for n in rates_dt.names], GZIP) + self.datastore.create_dset( + '_rates/slice_by_idx', getters.slice_dt) + self._skip_store_ctxs = i > 0 + self._execute(sgs, ds) + # Drop SWMR before renaming _rates + self.datastore.close() + self.datastore.open('a') + h5 = self.datastore.hdf5 + target = '_rates_site_%d' % i + if target in h5: + del h5[target] + h5.move('_rates', target) + self._overlay_sitecol(baseline) + def store_info(self): """ Store full_lt, source_info and source_data @@ -733,16 +784,22 @@ def collect_hazard(self, acc, pmap_by_kind): # this is practically instantaneous if pmap_by_kind is None: # instead of a dict raise MemoryError('You ran out of memory!') + # Under site-model LT, sum across getters (each covers disjoint rlzs) + site_lt = getattr(self.full_lt, 'site_model_lt', None) is not None for kind in pmap_by_kind: # hmaps-XXX, hcurves-XXX pmaps = pmap_by_kind[kind] - if kind in self.hazard: - array = self.hazard[kind] - else: + accum = site_lt and kind in self.hazard + if kind not in self.hazard: dset = self.datastore.getitem(kind) - array = self.hazard[kind] = numpy.zeros(dset.shape, dset.dtype) + self.hazard[kind] = numpy.zeros(dset.shape, dset.dtype) + array = self.hazard[kind] for r, pmap in enumerate(pmaps): for idx, sid in enumerate(pmap.sids): - array[sid, r] = pmap.array[idx] # shape (M, P) + val = pmap.array[idx] # shape (M, P) or (M, L1) + if accum: + array[sid, r] += val + else: + array[sid, r] = val def post_execute(self, dummy): """ @@ -806,7 +863,9 @@ def build_curves_maps(self): oq = self.oqparam hstats = oq.hazard_stats() N, S, M, P, L1 = self._create_hcurves_maps() - if '_rates' in set(self.datastore) or not self.datastore.parent: + has_rates = any(k == '_rates' or k.startswith('_rates_site_') + for k in self.datastore) + if has_rates or not self.datastore.parent: dstore = self.datastore else: dstore = self.datastore.parent diff --git a/openquake/calculators/disaggregation.py b/openquake/calculators/disaggregation.py index 59d64dce1bf0..d977e9e2edca 100644 --- a/openquake/calculators/disaggregation.py +++ b/openquake/calculators/disaggregation.py @@ -31,7 +31,7 @@ from openquake.hazardlib import stats, map_array, valid from openquake.hazardlib.calc import disagg, mean_rates from openquake.hazardlib.contexts import read_cmakers, read_ctx_by_grp -from openquake.commonlib import util +from openquake.commonlib import util, readinput from openquake.calculators import base, getters POE_TOO_BIG = '''\ @@ -47,7 +47,7 @@ def compute_disagg(dstore, ctxt, sitecol, cmaker, bin_edges, src_mutex, rwdic, - monitor): + rlz_filter, monitor): """ :param dstore: a DataStore instance @@ -63,6 +63,9 @@ def compute_disagg(dstore, ctxt, sitecol, cmaker, bin_edges, src_mutex, rwdic, a dictionary src_id -> weight, usually empty :param rwdic: dictionary rlz -> weight, empty for individual realizations + :param rlz_filter: + set of rlz ordinals to keep, or ``None`` to disable filtering + (used by the site-model LT outer loop in :meth:`compute`) :param monitor: monitor of the currently running job :returns: @@ -85,6 +88,10 @@ def compute_disagg(dstore, ctxt, sitecol, cmaker, bin_edges, src_mutex, rwdic, imtls = {imt: iml2[m] for m, imt in enumerate(cmaker.imts)} rlzs = dstore['best_rlzs'][dis.sid] + if rlz_filter is not None: + rlzs = numpy.array([r for r in rlzs if r in rlz_filter]) + if len(rlzs) == 0: + continue res = dis.disagg_by_magi(imtls, rlzs, rwdic, src_mutex, mon0, mon1, mon2, mon3) out.extend(res) @@ -112,12 +119,14 @@ def output_dict(shapedic, disagg_outputs, Z): return dic -def submit(smap, dstore, ctxt, sitecol, cmaker, bin_edges, src_mutex, rwdic): +def submit(smap, dstore, ctxt, sitecol, cmaker, bin_edges, src_mutex, rwdic, + rlz_filter=None): mags = list(numpy.unique(ctxt.mag)) logging.debug('Sending %d/%d sites for grp_id=%d, mags=%s', len(sitecol), len(sitecol.complete), ctxt.grp_id[0], shortlist(mags)) - smap.submit((dstore, ctxt, sitecol, cmaker, bin_edges, src_mutex, rwdic)) + smap.submit((dstore, ctxt, sitecol, cmaker, bin_edges, src_mutex, rwdic, + rlz_filter)) def check_memory(N, Z, shape8D): @@ -180,7 +189,8 @@ def full_disaggregation(self): try: full_lt = self.full_lt except AttributeError: - full_lt = self.datastore['full_lt'].init() + # Skip .init() here: map_getters will call it below + full_lt = self.datastore['full_lt'] if oq.rlz_index is None and oq.num_rlzs_disagg == 0: oq.num_rlzs_disagg = self.R # 0 means all rlzs self.oqparam.mags_by_trt = self.datastore['source_mags'] @@ -244,7 +254,38 @@ def full_disaggregation(self): for z, r in enumerate(rlzs[s])} return self.compute() - def _submit_all(self, smap, cmakers, ctx_by_grp, src_mutex_by_grp): + def compute(self): + """ + Submit disaggregation tasks and return the results + """ + oq = self.oqparam + full_lt = getattr(self, 'full_lt', None) or self.datastore['full_lt'] + site_lt = getattr(full_lt, 'site_model_lt', None) + if site_lt is None: + return self._compute_pass(rlz_filter=None) + # Site-model epistemic uncertainty: one pass per site branch + self.full_lt = full_lt + smep = readinput.get_site_models_epistemic(oq) + baseline = self.sitecol.array.copy() + all_rlzs = self.full_lt.get_realizations() + combined = None + for i, arr in enumerate(smep.arrays): + rlz_filter = {r.ordinal for r in all_rlzs + if r.site_rlz.ordinal == i} + if not rlz_filter: + continue + self._overlay_sitecol(arr) + part = self._compute_pass(rlz_filter=rlz_filter) + if combined is None: + combined = part + else: + for k, v in part.items(): + combined[k] = combined.get(k, 0) + v + self._overlay_sitecol(baseline) + return combined + + def _submit_all(self, smap, cmakers, ctx_by_grp, src_mutex_by_grp, + rlz_filter): # compute the total weight of the contexts and the maxsize ct = self.oqparam.concurrent_tasks or 1 totweight = sum(cmakers[grp_id].Z * len(ctx) @@ -278,7 +319,8 @@ def _submit_all(self, smap, cmakers, ctx_by_grp, src_mutex_by_grp): if ntasks < 1 or len(src_mutex) or rup_mutex: # do not split (test case_11) submit(smap, self.datastore, ctxt, self.sitecol, cmaker, - self.bin_edges, src_mutex, rwdic) + self.bin_edges, src_mutex, rwdic, + rlz_filter=rlz_filter) continue # split by tiles @@ -290,15 +332,19 @@ def _submit_all(self, smap, cmakers, ctx_by_grp, src_mutex_by_grp): for c in disagg.split_by_magbin( ctx, self.bin_edges[0]).values(): submit(smap, self.datastore, c, tile, cmaker, - self.bin_edges, src_mutex, rwdic) + self.bin_edges, src_mutex, rwdic, + rlz_filter=rlz_filter) elif len(ctx): # see case_multi in the oq-risk-tests submit(smap, self.datastore, ctx, tile, cmaker, - self.bin_edges, src_mutex, rwdic) + self.bin_edges, src_mutex, rwdic, + rlz_filter=rlz_filter) - def compute(self): + def _compute_pass(self, rlz_filter): """ - Submit disaggregation tasks and return the results + Single pass of the disaggregation compute loop; used both by + the regular path (``rlz_filter=None``) and by the site-model + epistemic outer loop (one pass per site rlz) """ dstore = (self.datastore.parent if self.datastore.parent else self.datastore) @@ -326,7 +372,8 @@ def compute(self): # that would break the ordering of the indices causing an incredibly # worse performance, but visible only in extra-large calculations! - self._submit_all(smap, cmakers, ctx_by_grp, src_mutex_by_grp) + self._submit_all(smap, cmakers, ctx_by_grp, src_mutex_by_grp, + rlz_filter) s = self.shapedic shape8D = (s['trt'], s['mag'], s['dist'], s['lon'], s['lat'], s['eps'], s['M'], s['P']) diff --git a/openquake/calculators/export/hazard.py b/openquake/calculators/export/hazard.py index c715266d33ef..d1b13abec0d7 100644 --- a/openquake/calculators/export/hazard.py +++ b/openquake/calculators/export/hazard.py @@ -309,9 +309,12 @@ def get_metadata(rlzs, kind): """ metadata = {} if kind.startswith('rlz-'): - smlt_path, gslt_path = rlzs[int(kind[4:])]['branch_path'].split('~') - metadata['smlt_path'] = smlt_path - metadata['gsimlt_path'] = gslt_path + # SSC~GMM or SSC~GMM~SITE when a site-model LT is active + parts = rlzs[int(kind[4:])]['branch_path'].split('~') + metadata['smlt_path'] = parts[0] + metadata['gsimlt_path'] = parts[1] + if len(parts) > 2: + metadata['site_lt_path'] = parts[2] elif kind.startswith('quantile-'): metadata['statistics'] = 'quantile' metadata['quantile_value'] = float(kind[9:]) diff --git a/openquake/calculators/getters.py b/openquake/calculators/getters.py index 4f911e6f8761..73a78fe42b59 100644 --- a/openquake/calculators/getters.py +++ b/openquake/calculators/getters.py @@ -105,6 +105,12 @@ def get_hcurve(self, src_id, imt=None, site_id=0, gsim_idx=None): assert ';' in src_id, src_id # must be a realization specific src_id imt_slc = self.imtls(imt) if imt else slice(None) start, gsims, weights = self.bysrc[src_id] + # Source-specific LT + site LT is unsupported: _rates is split + # across _rates_site_i groups so there is no unambiguous curve + if '_rates' not in self.dstore: + raise NotImplementedError( + 'HcurvesGetter is not supported when a site-model logic ' + 'tree is present') dset = self.dstore['_rates'] if gsim_idx is None: curves = dset[start:start + len(gsims), site_id, imt_slc] @@ -193,6 +199,7 @@ def map_getters(dstore, full_lt=None, oq=None, disagg=False): _req_gb, trt_rlzs, trt_smrs = get_rmap_gb(dstore, full_lt) attrs = vars(full_lt) full_lt.init() + gweights, wgets = None, None if oq.fastmean: gweights = [full_lt.g_weights(trt_smrs)] else: @@ -212,21 +219,69 @@ def map_getters(dstore, full_lt=None, oq=None, disagg=False): for f in os.listdir(calc_dir): if f.endswith('.hdf5'): fnames.append(os.path.join(calc_dir, f)) - out = [] + sids = dstore['sitecol/sids'][:] + + # Under a site-model LT there are Rsite variants + # (rates_dset, rlz_mask); otherwise a single ('_rates', None) variant + variants, gws_by_site = _site_lt_variants(full_lt, trt_smrs, oq.fastmean) + site_lt_on = getattr(full_lt, 'site_model_lt', None) is not None + out = [] for chunk in range(n): tile = sids[sids % n == chunk] - getter = MapGetter(fnames, chunk, trt_rlzs, tile, R, oq) - if oq.fastmean: - getter.gweights = gweights + sub_getters = [] + for j, (rates_dset, rlz_mask) in enumerate(variants): + getter = MapGetter(fnames, chunk, trt_rlzs, tile, R, oq, + rates_dset=rates_dset, rlz_mask=rlz_mask) + if oq.fastmean: + getter.gweights = [gws_by_site[j]] if site_lt_on else gweights + else: + getter.wgets = wgets + if oq.site_labels: + getter.ilabels = dstore['sitecol'].ilabel + sub_getters.append(getter) + if site_lt_on and (disagg or oq.fastmean): + out.append(MergedMapGetter(sub_getters)) else: - getter.wgets = wgets - if oq.site_labels: - getter.ilabels = dstore['sitecol'].ilabel - out.append(getter) + out.extend(sub_getters) return out +def _site_lt_variants(full_lt, trt_smrs, fastmean): + """ + :returns: ``(variants, gweights_by_site)`` + ``variants`` is a list of ``(rates_dset, rlz_mask)``; for the + no-site-LT case that's a single ``('_rates', None)`` entry. + ``gweights_by_site`` is ``None`` unless ``fastmean`` is on + together with a site LT + """ + site_lt = getattr(full_lt, 'site_model_lt', None) + if site_lt is None: + return [('_rates', None)], None + site_ords = numpy.array([r.site_rlz.ordinal + for r in full_lt.get_realizations()]) + used_i = [i for i in range(site_lt.Rsite) if (site_ords == i).any()] + variants = [('_rates_site_%d' % i, site_ords == i) for i in used_i] + if not fastmean: + return variants, None + # Per-site-rlz gweights: for each gid, sum weights of rlzs owned by + # this branch only, so fastmean's sum over sub-getters reproduces + # sum_r w_r * rate_r + gws_by_site = [] + for i in used_i: + mask = site_ords == i + gwi = [] + for _trt_smrs in trt_smrs: + for grlzs in full_lt.get_rlzs_by_gsim(_trt_smrs).values(): + mrlzs = numpy.array([r for r in grlzs if mask[r]], dtype=int) + if len(mrlzs): + gwi.append(full_lt.weights[mrlzs].sum(axis=0)) + else: + gwi.append(numpy.zeros_like(full_lt.weights[0])) + gws_by_site.append(numpy.array(gwi)) + return variants, gws_by_site + + class ZeroGetter(object): """ Return an array of zeros of shape (L, R) @@ -294,7 +349,8 @@ class MapGetter(object): Read hazard curves from the datastore for all realizations or for a specific realization. """ - def __init__(self, filenames, chunk, trt_rlzs, sids, R, oq): + def __init__(self, filenames, chunk, trt_rlzs, sids, R, oq, + rates_dset='_rates', rlz_mask=None): self.filenames = filenames self.chunk = chunk self.trt_rlzs = trt_rlzs @@ -306,6 +362,10 @@ def __init__(self, filenames, chunk, trt_rlzs, sids, R, oq): self.eids = None self.ilabels = () # overridden in case of ilabels self.array = None + # Under site-model LT, rates are split into _rates_site_i groups + # and rlz_mask picks the rlzs owned by that site branch + self.rates_dset = rates_dset + self.rlz_mask = rlz_mask @property def imts(self): @@ -335,12 +395,16 @@ def init(self): return self.array sid2idx = {sid: idx for idx, sid in enumerate(self.sids)} self.array = numpy.zeros((self.N, self.L, self.Gt)) # move to 32 bit + slice_key = '%s/slice_by_idx' % self.rates_dset for fname in self.filenames: with hdf5.File(fname) as dstore: - slices = dstore['_rates/slice_by_idx'][:] + if self.rates_dset not in dstore: + continue + slices = dstore[slice_key][:] slices = slices[slices['idx'] == self.chunk] for start, stop in zip(slices['start'], slices['stop']): - df = dstore.read_df('_rates', slc=slice(start, stop)) + df = dstore.read_df( + self.rates_dset, slc=slice(start, stop)) idxs = U32([sid2idx[sid] for sid in df.sid]) lid = df.lid.to_numpy() gid = df.gid.to_numpy() @@ -360,6 +424,8 @@ def get_hcurve(self, sid): # used in classical rlzs = t_rlzs % TWO24 rates = array[idx, :, g] for rlz in rlzs: + if self.rlz_mask is not None and not self.rlz_mask[rlz]: + continue r0[:, rlz] += rates return to_probs(r0) @@ -384,6 +450,80 @@ def get_fast_mean(self): return means +class MergedMapGetter(object): + """ + Wraps a list of per-site-rlz :class:`MapGetter` instances (all sharing + ``chunk``, ``sids``, ``trt_rlzs``, ``R``) into one getter with a single + hcurve per ``sid``; used by disagg and fastmean under a site-model LT + """ + def __init__(self, sub_getters): + first = sub_getters[0] + self.sub_getters = sub_getters + self.filenames = first.filenames + self.chunk = first.chunk + self.trt_rlzs = first.trt_rlzs + self.sids = first.sids + self.R = first.R + self.imtls = first.imtls + self.poes = first.poes + self.use_rates = first.use_rates + self.eids = None + self.ilabels = first.ilabels + self.wgets = getattr(first, 'wgets', None) + self.gweights = getattr(first, 'gweights', None) + self.array = None + + @property + def imts(self): + return list(self.imtls) + + @property + def L(self): + return self.imtls.size + + @property + def M(self): + return len(self.imtls) + + def init(self): + for sub in self.sub_getters: + sub.init() + self.sid2idx = self.sub_getters[0].sid2idx + + def get_hcurve(self, sid): + """ + :returns: an array of shape ``(L, R)``; each column ``r`` is taken + from the sub-getter whose ``rlz_mask`` includes ``r`` + """ + r0 = numpy.zeros((self.L, self.R)) + for sub in self.sub_getters: + r0[:, sub.rlz_mask] = sub.get_hcurve(sid)[:, sub.rlz_mask] + return r0 + + def get_fast_mean(self): + """ + :returns: a :class:`MapArray` of shape ``(N, M, L1)`` with the mean + hcurves, aggregated across sub-getter rates before ``to_probs`` + """ + M = self.M + L1 = self.L // M + means = MapArray(U32(self.sids), M, L1).fill(0) + for sub in self.sub_getters: + sub.init() + gweights = sub.gweights[0] + imt_dep = gweights.shape[1] > 1 + for sid in self.sids: + gw = (sub.gweights[sub.ilabels[sid]] + if len(sub.ilabels) else gweights) + rates = sub.array[sub.sid2idx[sid]] # shape (L, G) + sidx = means.sidx[sid] + for m in range(M): + means.array[sidx, m] += rates[m*L1:m*L1+L1] @ gw[ + :, m if imt_dep else -1] + means.array[:] = to_probs(means.array) + return means + + def get_ebruptures(dstore): """ Extract EBRuptures from the datastore diff --git a/openquake/calculators/tests/logictree_test.py b/openquake/calculators/tests/logictree_test.py index e15cdd096336..fcf9d16dbcb0 100644 --- a/openquake/calculators/tests/logictree_test.py +++ b/openquake/calculators/tests/logictree_test.py @@ -19,6 +19,7 @@ import sys import unittest import numpy +import copy from openquake.baselib import general, config from openquake.baselib.general import decode from openquake.hazardlib import contexts, source_group, InvalidFile @@ -28,14 +29,15 @@ from openquake.calculators.views import view, text_table from openquake.calculators.export import export from openquake.calculators.extract import extract +from openquake.calculators.getters import _site_lt_variants from openquake.calculators.tests import CalculatorTestCase, strip_calc_id from openquake.qa_tests_data.logictree import ( case_01, case_02, case_03, case_04, case_05, case_06, case_07, case_08, case_09, case_10, case_11, case_12, case_13, case_14, case_15, case_16, - case_17, case_18, case_19, case_20, case_21, case_22, case_23, case_25, - case_28, case_29, case_30, case_31, case_32, case_33, case_36, case_39, - case_45, case_46, case_52, case_56, case_58, case_59, case_67, case_68, - case_71, case_73, case_79, case_80, case_83, case_84) + case_17, case_18, case_19, case_20, case_21, case_22, case_23, case_24, + case_25, case_28, case_29, case_30, case_31, case_32, case_33, case_36, + case_39, case_45, case_46, case_52, case_56, case_58, case_59, case_67, + case_68, case_71, case_73, case_79, case_80, case_83, case_84) ae = numpy.testing.assert_equal aac = numpy.testing.assert_allclose @@ -52,15 +54,47 @@ def assert_curves_ok(self, expected, test_dir, delta=None, **kw): export(('uhs/' + kind, 'csv'), ds)) self.assertEqual(len(expected), len(got), str(got)) for fname, actual in zip(expected, got): - self.assertEqualFiles('expected/%s' % fname, actual, - delta=delta) + self.assertEqualFiles('expected/%s' % fname, actual, delta=delta) return got + def _reconstruct_fastmean_poes(self, dstore): + # Mirrors MergedMapGetter.get_fast_mean for the site-LT tests + N = dstore['sitecol/sids'].size + L = self.calc.oqparam.imtls.size + full_lt = dstore['full_lt'].init() + trt_smrs, _ = contexts.get_unique_inverse(dstore['trt_smrs']) + variants, gweights_by_site = _site_lt_variants( + full_lt, trt_smrs, fastmean=True) + mean_rate = numpy.zeros((N, L)) + for (dset, _), gweights in zip(variants, gweights_by_site): + df = dstore.read_df(dset) + for sid, lid, gid, rate in zip(df.sid, df.lid, df.gid, df.rate): + mean_rate[sid, lid] += rate * gweights[gid, -1] + return 1 - numpy.exp(-mean_rate) # Rates -> poes + + def _assert_site_lt_disagg_keys(self, dstore): + # At least one _rates_site_i must exist (sampling may drop some) + keys = list(dstore.parent) if dstore.parent != () else list(dstore) + assert any(k.startswith('_rates_site_') for k in keys), keys + assert 'disagg-rlzs' in dstore, list(dstore) + + def _assert_sampling_weights(self, dstore, n, uniform): + # Sampled rlz weights: n items summing to 1, uniform if early_weights + ws = dstore['weights'][:] + ae(len(ws), n) + aac(ws.sum(), 1.0, atol=1e-6) + if uniform: + aac(ws, numpy.full(n, 1./n), atol=1e-6) + else: + assert len(numpy.unique(numpy.round(ws, 6))) > 1, ws + def tearDown(self): if not hasattr(self, 'calc'): # some test broke return if 'hcurves-stats' not in self.calc.datastore: # not produced return + if not hasattr(self.calc, 'csm'): # child calcs have no csm + return oq = self.calc.oqparam csm = self.calc.csm csm_read = source_group.read_csm(self.calc.datastore) @@ -79,10 +113,31 @@ def tearDown(self): sitecol = self.calc.datastore['sitecol'] trt_smrs, _ = contexts.get_unique_inverse( self.calc.datastore['trt_smrs']) - rmap = get_rmap(csm_read.src_groups, full_lt, sitecol, oq)[0] wget = full_lt.gsim_lt.wget - mean_rates = calc_mean_rates( - rmap, full_lt.g_weights(trt_smrs), wget, oq.imtls) + + def rates(sc, gws): + rmap = get_rmap(csm_read.src_groups, full_lt, sc, oq)[0] + return calc_mean_rates(rmap, gws, wget, oq.imtls) + + # Under a site-model LT sum per-branch rates with per-branch + # gweights (rlzs bound to that branch only) + if getattr(full_lt, 'site_model_lt', None) is not None: + smep = readinput.get_site_models_epistemic(oq) + skip = {'lon', 'lat', 'depth', 'sids'} + variants, gws_by_site = _site_lt_variants( + full_lt, trt_smrs, fastmean=True) + used_i = [int(name.rsplit('_', 1)[1]) for name, _ in variants] + partials = [] + for i, gws_i in zip(used_i, gws_by_site): + arr = smep.arrays[i] + sc = copy.deepcopy(sitecol) + for name in set(arr.dtype.names) - skip: + if name in sc.array.dtype.names: + sc.array[name] = arr[name] + partials.append(rates(sc, gws_i)) + mean_rates = sum(partials) + else: + mean_rates = rates(sitecol, full_lt.g_weights(trt_smrs)) er = exp_rates[exp_rates < 1] mr = mean_rates[mean_rates < 1] aac(mr, er, atol=2e-5) @@ -501,6 +556,177 @@ def test_case_23_bis(self): ns = len(self.calc.datastore['source_info']) assert ns == 26 + def test_case_24(self): + # Site LT: 2 SSC x 2 GMM x 2 SITE = 8 rlzs with 3-part paths + # (SSC~GMM~SITE) + self.run_calc(case_24.__file__, 'job.ini') + dstore = self.calc.datastore + rlzs = dstore['full_lt'].rlzs + ae(sorted(r['branch_path'] for r in rlzs), + ['A~A~A', 'A~A~B', 'A~B~A', 'A~B~B', + 'B~A~A', 'B~A~B', 'B~B~A', 'B~B~B']) + # SSC=[0.7,0.3], GMM=[0.6,0.4], SITE=[0.6,0.4] + expected_ws = sorted( + [0.7 * gw * sw for gw in (0.6, 0.4) for sw in (0.6, 0.4)] + + [0.3 * gw * sw for gw in (0.6, 0.4) for sw in (0.6, 0.4)]) + aac(sorted(r['weight'] for r in rlzs), expected_ws, atol=1e-6) + + hc = dstore['hcurves-rlzs'][:] + weights = numpy.array([r['weight'] for r in rlzs]) + aac(dstore['hcurves-stats'][:, 0], + numpy.average(hc, axis=1, weights=weights), atol=1e-5) + + # Compare mean + first / last rlz for both hcurves and hmaps + keep = {'hazard_curve-mean-PGA.csv', + 'hazard_curve-rlz-000-PGA.csv', + 'hazard_curve-rlz-007-PGA.csv', + 'hazard_map-mean.csv', + 'hazard_map-rlz-000.csv', + 'hazard_map-rlz-007.csv'} + for fname in (export(('hcurves', 'csv'), dstore) + + export(('hmaps', 'csv'), dstore)): + basename = strip_calc_id(fname) + if basename in keep: + self.assertEqualFiles('expected/' + basename, fname) + + def test_case_24_smpl(self): + # Classical + site LT, sampling all three legs num_samples=4 times + keep = {'hazard_curve-mean-PGA.csv', + 'hazard_curve-rlz-000-PGA.csv', + 'hazard_curve-rlz-001-PGA.csv'} + + # early_weights: rlz weights uniformised to 1/N + self.run_calc(case_24.__file__, 'job_sampling.ini') + dstore = self.calc.datastore + ae(dstore['hcurves-rlzs'].shape[1], 4) + self._assert_sampling_weights(dstore, n=4, uniform=True) + for fname in export(('hcurves', 'csv'), dstore): + base = strip_calc_id(fname) + if base in keep: + self.assertEqualFiles('expected/sampling_' + base, fname) + + # late_weights: preserve normalised w_ssc * w_gmm * w_site + self.run_calc(case_24.__file__, 'job_sampling.ini', + sampling_method='late_weights') + dstore = self.calc.datastore + ae(dstore['hcurves-rlzs'].shape[1], 4) + self._assert_sampling_weights(dstore, n=4, uniform=False) + for fname in export(('hcurves', 'csv'), dstore): + base = strip_calc_id(fname) + if base in keep: + self.assertEqualFiles('expected/sampling_late_' + base, fname) + + def test_case_24_fm(self): + # Classical fastmean + site LT, full enumeration + self.run_calc(case_24.__file__, 'job_fastmean.ini') + dstore = self.calc.datastore + fm_mean = dstore['hcurves-stats'][:, 0] + aac(fm_mean, self._reconstruct_fastmean_poes(dstore).reshape( + fm_mean.shape), atol=1e-6) + [got] = export(('hcurves', 'csv'), dstore) + self.assertEqualFiles('expected/fastmean_' + strip_calc_id(got), got) + + def test_case_24_fm_smpl(self): + # Classical fastmean + site LT with sampling + + # early_weights + self.run_calc(case_24.__file__, 'job_fastmean_sampling.ini') + dstore = self.calc.datastore + self._assert_sampling_weights(dstore, n=4, uniform=True) + fm_mean = dstore['hcurves-stats'][:, 0] + aac(fm_mean, self._reconstruct_fastmean_poes(dstore).reshape( + fm_mean.shape), atol=1e-6) + [got] = export(('hcurves', 'csv'), dstore) + self.assertEqualFiles( + 'expected/fastmean_sampling_' + strip_calc_id(got), got) + + # late_weights + self.run_calc(case_24.__file__, 'job_fastmean_sampling.ini', + sampling_method='late_weights') + dstore = self.calc.datastore + self._assert_sampling_weights(dstore, n=4, uniform=False) + fm_mean = dstore['hcurves-stats'][:, 0] + aac(fm_mean, self._reconstruct_fastmean_poes(dstore).reshape( + fm_mean.shape), atol=1e-6) + [got] = export(('hcurves', 'csv'), dstore) + self.assertEqualFiles( + 'expected/fastmean_sampling_late_' + strip_calc_id(got), got) + + def test_case_24_dsg(self): + # Disagg + site LT with full enumeration (8 rlzs) + self.run_calc(case_24.__file__, 'job_disagg.ini') + dstore = self.calc.datastore + self._assert_site_lt_disagg_keys(dstore) + + for fname in export(('disagg-stats', 'csv'), dstore): + basename = strip_calc_id(fname) + if basename.startswith('Mag-mean-'): + self.assertEqualFiles('expected/disagg_' + basename, fname) + + def test_case_24_dsg_smpl(self): + # Disagg + site LT with sampling + prefix = 'Mag-mean-' + + # early_weights + self.run_calc(case_24.__file__, 'job_disagg_sampling.ini') + dstore = self.calc.datastore + self._assert_site_lt_disagg_keys(dstore) + self._assert_sampling_weights(dstore, n=4, uniform=True) + for fname in export(('disagg-stats', 'csv'), dstore): + base = strip_calc_id(fname) + if base.startswith(prefix): + self.assertEqualFiles( + 'expected/disagg_sampling_' + base, fname) + + # late_weights + self.run_calc(case_24.__file__, 'job_disagg_sampling.ini', + sampling_method='late_weights') + dstore = self.calc.datastore + self._assert_site_lt_disagg_keys(dstore) + self._assert_sampling_weights(dstore, n=4, uniform=False) + for fname in export(('disagg-stats', 'csv'), dstore): + base = strip_calc_id(fname) + if base.startswith(prefix): + self.assertEqualFiles( + 'expected/disagg_sampling_late_' + base, fname) + + def test_case_24_xml(self): + # Site model LT branches point toward XML site models instead + self.run_calc(case_24.__file__, 'job_xml_branches.ini') + xml_dstore = self.calc.datastore + xml_hc = xml_dstore['hcurves-rlzs'][:] + ae(xml_hc.shape[1], 8) + + keep = {'xml_hazard_curve-mean-PGA.csv', + 'xml_hazard_curve-rlz-000-PGA.csv', + 'xml_hazard_curve-rlz-007-PGA.csv'} + for fname in export(('hcurves', 'csv'), xml_dstore): + expname = 'xml_' + strip_calc_id(fname) + if expname in keep: + self.assertEqualFiles('expected/' + expname, fname) + + # Cross-check against the CSV variant + self.run_calc(case_24.__file__, 'job.ini') + csv_hc = self.calc.datastore['hcurves-rlzs'][:] + aac(xml_hc, csv_hc, atol=1e-6) + + def test_case_24_hdf5(self): + # Classical + sampling first, then disagg reading the parent dstore + self.run_calc(case_24.__file__, + 'job_sampling.ini,job_disagg_from_parent.ini') + dstore = self.calc.datastore + # Site-model LT metadata must survive the __toh5__/__fromh5__ round-trip + assert dstore['full_lt'].init().site_model_lt is not None + self._assert_site_lt_disagg_keys(dstore) + self._assert_sampling_weights(dstore, n=4, uniform=True) + # Values differ from standalone disagg since hcurves come from the + # parent dstore instead of being recomputed - dedicated expected files + for fname in export(('disagg-stats', 'csv'), dstore): + base = strip_calc_id(fname) + if base.startswith('Mag-mean-'): + self.assertEqualFiles( + 'expected/disagg_hdf5_' + base, fname) + def test_case_25(self): # BCHydro-style correlated uncertainties self.run_calc(case_25.__file__, 'job_alt1.ini', exports='csv') diff --git a/openquake/commonlib/readinput.py b/openquake/commonlib/readinput.py index 4a287970cbd6..db38bd829cb1 100644 --- a/openquake/commonlib/readinput.py +++ b/openquake/commonlib/readinput.py @@ -62,7 +62,7 @@ from openquake.hazardlib.calc.gmf import CorrelationButNoInterIntraStdDevs from openquake.hazardlib import ( source, geo, site, imt, valid, sourceconverter, source_reader, nrml, - pmf, logictree, gsim_lt, get_smlt) + pmf, logictree, gsim_lt, get_smlt, site_lt) from openquake.hazardlib.source.rupture import ( build_planar_rupture_from_dict, get_ruptures, get_ebrupture) from openquake.hazardlib.map_array import MapArray @@ -654,6 +654,89 @@ def check_site_param(oqparam, name): return value +def _expand_site_model_lt(oqparam): + # If inputs['site_model'] is a site-LT XML, rewrite it to the list + # of per-branch site-model files and cache the tree on oqparam + if getattr(oqparam, '_site_model_lt', None) is not None: + return oqparam._site_model_lt + oqparam._site_model_lt = None + files = oqparam.inputs.get('site_model') or [] + if len(files) != 1: + return None + fname = files[0] + if not site_lt.SiteModelLogicTree.is_site_model_lt(fname): + return None + if oqparam.calculation_mode not in ('classical', 'disaggregation'): + raise InvalidFile( + '%s: site-model logic trees are only supported by the ' + 'classical and disaggregation calculators (got %r)' + % (fname, oqparam.calculation_mode)) + tree = site_lt.SiteModelLogicTree(fname) + oqparam._site_model_lt = tree + oqparam.inputs['site_model'] = list(tree.filenames) + return tree + + +def _parse_site_model_file(fname, oqparam, arrays, sm_fieldsets): + # Parse one CSV or NRML XML site model, append to arrays and record + # its field set; extracted so the site-LT loader shares this path + if isinstance(fname, str) and not fname.endswith('.xml'): + _smparse(fname, oqparam, arrays, sm_fieldsets) + return + nodes = nrml.read(fname).siteModel + params = [valid.site_param(node.attrib) for node in nodes] + missing = set(oqparam.req_site_params) - set(params[0]) + if 'vs30measured' in missing: # use a default of False + missing -= {'vs30measured'} + for param in params: + param['vs30measured'] = False + if 'backarc' in missing: # use a default of False + missing -= {'backarc'} + for param in params: + param['backarc'] = False + if 'ampcode' in missing: # use a default of b'' + missing -= {'ampcode'} + for param in params: + param['ampcode'] = b'' + if missing: + raise InvalidFile('%s: missing parameter %s' % + (fname, ', '.join(missing))) + # NB: the sorted in sorted(params[0]) is essential, otherwise there is + # an heisenbug in scenario/test_case_4 + site_model_dt = numpy.dtype([(p, site.site_param_dt[p]) + for p in sorted(params[0])]) + sm = numpy.array([tuple(param[name] for name in site_model_dt.names) + for param in params], site_model_dt) + dupl = "\n".join( + '%s %s' % loc for loc, n in countby(sm, 'lon', 'lat').items() + if n > 1) + if dupl: + raise InvalidFile('There are duplicated sites in %s:\n%s' % + (fname, dupl)) + sm_fieldsets[fname] = set(sm.dtype.names) + arrays.append(sm) + + +def get_site_models_epistemic(oqparam): + """ + :returns: a :class:`SiteModelsEpistemic` if the ``site_model`` input + is a logic-tree XML, else ``None`` + """ + tree = _expand_site_model_lt(oqparam) + if tree is None: + return None + per_branch_arrays = [] + for path in tree.filenames: + arrays = [] + _parse_site_model_file(path, oqparam, arrays, {}) + [arr] = arrays + per_branch_arrays.append(arr) + return site_lt.SiteModelsEpistemic( + tree.branch_ids, tree.weights, per_branch_arrays, + filenames=tree.filenames, tree_filename=tree.filename, + branchset_id=tree.branchset_id) + + def get_site_model(oqparam, h5=None): """ :param oqparam: @@ -673,47 +756,16 @@ def get_site_model(oqparam, h5=None): h5['site_model'] = sm return sm + # Under a site-model LT all branches share lon/lat/depth so only + # the first (canonical) branch is loaded here; per-branch overlays + # are applied later by the classical/disagg outer loops + tree = _expand_site_model_lt(oqparam) + site_files = (oqparam.inputs['site_model'][:1] if tree + else oqparam.inputs['site_model']) arrays = [] sm_fieldsets = {} - for fname in oqparam.inputs['site_model']: - if isinstance(fname, str) and not fname.endswith('.xml'): - # parsing site_model.csv and populating arrays - _smparse(fname, oqparam, arrays, sm_fieldsets) - continue - - # parsing site_model.xml - nodes = nrml.read(fname).siteModel - params = [valid.site_param(node.attrib) for node in nodes] - missing = set(oqparam.req_site_params) - set(params[0]) - if 'vs30measured' in missing: # use a default of False - missing -= {'vs30measured'} - for param in params: - param['vs30measured'] = False - if 'backarc' in missing: # use a default of False - missing -= {'backarc'} - for param in params: - param['backarc'] = False - if 'ampcode' in missing: # use a default of b'' - missing -= {'ampcode'} - for param in params: - param['ampcode'] = b'' - if missing: - raise InvalidFile('%s: missing parameter %s' % - (oqparam.inputs['site_model'], - ', '.join(missing))) - # NB: the sorted in sorted(params[0]) is essential, otherwise there is - # an heisenbug in scenario/test_case_4 - site_model_dt = numpy.dtype([(p, site.site_param_dt[p]) - for p in sorted(params[0])]) - sm = numpy.array([tuple(param[name] for name in site_model_dt.names) - for param in params], site_model_dt) - dupl = "\n".join( - '%s %s' % loc for loc, n in countby(sm, 'lon', 'lat').items() - if n > 1) - if dupl: - raise InvalidFile('There are duplicated sites in %s:\n%s' % - (fname, dupl)) - arrays.append(sm) + for fname in site_files: + _parse_site_model_file(fname, oqparam, arrays, sm_fieldsets) # all source model input files must have the same fields for this_sm_fname in sm_fieldsets: @@ -995,7 +1047,9 @@ def get_full_lt(oqparam): logging.warning('Unknown TRT=%s in [reqv] section' % trt) gsim_lt = get_gsim_lt(oqparam, trts or ['*']) oversampling = oqparam.oversampling - full_lt = logictree.FullLogicTree(source_model_lt, gsim_lt, oversampling) + site_model_lt = get_site_models_epistemic(oqparam) + full_lt = logictree.FullLogicTree( + source_model_lt, gsim_lt, oversampling, site_model_lt=site_model_lt) p = full_lt.source_model_lt.num_paths * gsim_lt.get_num_paths() if oqparam.number_of_logic_tree_samples: diff --git a/openquake/hazardlib/logictree.py b/openquake/hazardlib/logictree.py index 0882ee5c0c36..743288bb6631 100644 --- a/openquake/hazardlib/logictree.py +++ b/openquake/hazardlib/logictree.py @@ -48,6 +48,8 @@ from openquake.hazardlib.lt import ( Branch, BranchSet, count_paths, Realization, CompositeLogicTree, LogicTreeError, parse_uncertainty, attach_branches) +from openquake.hazardlib.site_lt import ( + SiteModelsEpistemic, site_model_lt_dt) U16 = numpy.uint16 U32 = numpy.uint32 @@ -350,18 +352,33 @@ def reduce_full(full_lt, rlz_clusters): :param rlz_clusters: list of paths for a realization cluster :returns: a dictionary with what can be reduced """ + site_lt = getattr(full_lt, 'site_model_lt', None) smrlz_clusters = [] gsrlz_clusters = [] + site_shorts = set() for path in rlz_clusters: - smr, gsr = decode(path).split('~') - smrlz_clusters.append(smr) - gsrlz_clusters.append(gsr) + parts = decode(path).split('~') + smrlz_clusters.append(parts[0]) + gsrlz_clusters.append(parts[1]) + if site_lt is not None: + site_shorts.add(parts[2]) f1, *p1 = reducible(full_lt.source_model_lt, smrlz_clusters) f2, *p2 = reducible(full_lt.gsim_lt, gsrlz_clusters) before = (full_lt.source_model_lt.get_num_paths() * full_lt.gsim_lt.get_num_paths()) - after = before / prod(len(p[1]) for p in p1 + p2) - return {f1: dict(p1), f2: dict(p2), 'size_before_after': (before, after)} + result = {f1: dict(p1), f2: dict(p2)} + # Site LT reduces when every rlz in the cluster uses the same branch + p3 = [] + if site_lt is not None: + before *= site_lt.Rsite + if len(site_shorts) == 1: + short_to_name = {v: k for k, v in site_lt.shortener.items()} + surviving = short_to_name[site_shorts.pop()] + p3.append((site_lt.branchset_id, [surviving])) + result[site_lt.filename] = dict(p3) + after = before / prod(len(p[1]) for p in p1 + p2 + p3) + result['size_before_after'] = (before, after) + return result class SourceModelLogicTree(object): @@ -962,17 +979,18 @@ def get_field(data, field, default): class LtRealization(object): """ Composite realization build on top of a source model realization and - a GSIM realization. + a GSIM realization; optionally carries a site-model realization """ # NB: for EUR, with 302_990_625 realizations, the usage of __slots__ # saves little memory, from 95.3 GB down to 81.0 GB - __slots__ = ['ordinal', 'sm_lt_path', 'gsim_rlz', 'weight'] + __slots__ = ['ordinal', 'sm_lt_path', 'gsim_rlz', 'weight', 'site_rlz'] - def __init__(self, ordinal, sm_lt_path, gsim_rlz, weight): + def __init__(self, ordinal, sm_lt_path, gsim_rlz, weight, site_rlz=None): self.ordinal = ordinal self.sm_lt_path = sm_lt_path self.gsim_rlz = gsim_rlz self.weight = weight + self.site_rlz = site_rlz # Realization of the site LT, or None def __repr__(self): return '<%d,w=%s>' % (self.ordinal, self.weight) @@ -1039,19 +1057,23 @@ def fake(cls, gsimlt=None): self.source_model_lt = SourceModelLogicTree.fake() self.gsim_lt = gsim_lt self.sm_rlzs = [fakeSM] + self.site_model_lt = None return self - def __init__(self, source_model_lt, gsim_lt, oversampling='tolerate'): + def __init__(self, source_model_lt, gsim_lt, oversampling='tolerate', + site_model_lt=None): self.source_model_lt = source_model_lt self.gsim_lt = gsim_lt self.oversampling = oversampling + self.site_model_lt = site_model_lt self.init() # set .sm_rlzs and .trts def __getstate__(self): # .sd will not be available in the workers return {'source_model_lt': self.source_model_lt, 'gsim_lt': self.gsim_lt, - 'oversampling': self.oversampling} + 'oversampling': self.oversampling, + 'site_model_lt': self.site_model_lt} def init(self): if self.source_model_lt.num_samples: @@ -1059,7 +1081,9 @@ def init(self): # of realizations in case of sampling self.sm_rlzs = get_effective_rlzs(self.source_model_lt) else: # full enumeration - samples = self.gsim_lt.get_num_paths() + Rsite = (self.site_model_lt.Rsite + if self.site_model_lt is not None else 1) + samples = self.gsim_lt.get_num_paths() * Rsite self.sm_rlzs = [] for sm_rlz in self.source_model_lt: sm_rlz.samples = samples @@ -1234,14 +1258,21 @@ def get_num_paths(self): """ if self.num_samples: return self.num_samples - return len(self.sm_rlzs) * self.gsim_lt.get_num_paths() + Rsite = (self.site_model_lt.Rsite + if self.site_model_lt is not None else 1) + return len(self.sm_rlzs) * self.gsim_lt.get_num_paths() * Rsite def get_realizations(self): """ :returns: the complete list of LtRealizations """ num_samples = self.source_model_lt.num_samples - self.gsim_lt.wget = IMTWeigher(self.gsim_lt, num_samples) + # Preserve any weights already on wget so repeat calls don't wipe them + prev = getattr(self.gsim_lt, 'wget', None) + self.gsim_lt.wget = IMTWeigher( + self.gsim_lt, num_samples, + prev.weights if prev is not None else None) + site_lt = self.site_model_lt if num_samples: # sampling rlzs = numpy.empty(num_samples, object) sm_rlzs = [] @@ -1249,22 +1280,36 @@ def get_realizations(self): sm_rlzs.extend([sm_rlz] * sm_rlz.samples) gsim_rlzs = self.gsim_lt.sample( num_samples, self.seed + 1, self.sampling_method) - for i, gsim_rlz in enumerate(gsim_rlzs): - rlzs[i] = LtRealization(i, sm_rlzs[i].lt_path, gsim_rlz, - sm_rlzs[i].weight * gsim_rlz.weight) + site_rlzs = (site_lt.sample(num_samples, self.seed + 2, + self.sampling_method) + if site_lt is not None else [None] * num_samples) + for k, (gsim_rlz, site_rlz) in enumerate( + zip(gsim_rlzs, site_rlzs)): + w = sm_rlzs[k].weight * gsim_rlz.weight + if site_rlz is not None: + w = w * site_rlz.weight + rlzs[k] = LtRealization( + k, sm_rlzs[k].lt_path, gsim_rlz, w, site_rlz) if self.sampling_method.startswith('early_'): for rlz in rlzs: rlz.weight[:] = 1. / num_samples else: # full enumeration + site_rlzs = (site_lt.get_realizations() + if site_lt is not None else [None]) gsim_rlzs = list(self.gsim_lt) ws = numpy.array([gsim_rlz.weight for gsim_rlz in gsim_rlzs]) - rlzs = numpy.empty(len(ws) * len(self.sm_rlzs), object) - i = 0 + rlzs = numpy.empty( + len(ws) * len(self.sm_rlzs) * len(site_rlzs), object) + k = 0 for sm_rlz in self.sm_rlzs: smpath = sm_rlz.lt_path for gsim_rlz, weight in zip(gsim_rlzs, sm_rlz.weight * ws): - rlzs[i] = LtRealization(i, smpath, gsim_rlz, weight) - i += 1 + for site_rlz in site_rlzs: + w = (weight if site_rlz is None + else weight * site_rlz.weight) + rlzs[k] = LtRealization( + k, smpath, gsim_rlz, w, site_rlz) + k += 1 # rescale the weights if not one, see case_52 # and logictree/case_30 for IMT-dependent weights tot_weight = sum(rlz.weight for rlz in rlzs) @@ -1323,20 +1368,29 @@ def __toh5__(self): for sm in self.sm_rlzs: sm_data.append((str(sm.value), sm.weight, '~'.join(sm.lt_path), sm.samples)) - return (dict( + dic = dict( source_model_lt=self.source_model_lt, gsim_lt=self.gsim_lt, source_data=self.source_model_lt.source_data, - sm_data=numpy.array(sm_data, source_model_dt)), - dict(seed=self.seed, num_samples=self.num_samples, + sm_data=numpy.array(sm_data, source_model_dt)) + attrs = dict(seed=self.seed, num_samples=self.num_samples, trts=hdf5.array_of_vstr(self.gsim_lt.values), - oversampling=self.oversampling)) + oversampling=self.oversampling) + smlt = self.site_model_lt + if smlt is not None: + dic['site_model_lt'] = numpy.array( + list(zip(smlt.names, smlt.weights, smlt.filenames)), + site_model_lt_dt) + attrs['site_model_tree_filename'] = smlt.filename + attrs['site_model_branchset_id'] = smlt.branchset_id + return dic, attrs # FullLogicTree def __fromh5__(self, dic, attrs): # TODO: this is called more times than needed, maybe we should cache it sm_data = dic['sm_data'] sd = dic.pop('source_data', numpy.zeros(0)) # empty for engine <= 3.16 + site_lt_arr = dic.pop('site_model_lt', None) vars(self).update(attrs) self.source_model_lt = dic['source_model_lt'] self.source_model_lt.source_data = sd[:] @@ -1347,6 +1401,18 @@ def __fromh5__(self, dic, attrs): sm = Realization( rec['name'], rec['weight'], sm_id, path, rec['samples']) self.sm_rlzs.append(sm) + self.site_model_lt = None + if site_lt_arr is not None and len(site_lt_arr): + names = [decode(r['name']) for r in site_lt_arr] + filenames = [decode(r['filename']) for r in site_lt_arr] + # Zero-length placeholder arrays; the real arrays are reloaded + # on demand via readinput.get_site_models_epistemic + dummy = numpy.zeros(0, [('lon', float), ('lat', float)]) + self.site_model_lt = SiteModelsEpistemic( + names, [r['weight'] for r in site_lt_arr], + [dummy] * len(names), filenames, + tree_filename=attrs.get('site_model_tree_filename', ''), + branchset_id=attrs.get('site_model_branchset_id', 'bs_site')) def get_num_potential_paths(self): """ @@ -1361,10 +1427,17 @@ def rlzs(self): """ sh1 = self.source_model_lt.shortener sh2 = self.gsim_lt.shortener + sh3 = (self.site_model_lt.shortener + if self.site_model_lt is not None else None) tups = [] for r in self.get_realizations(): - path = '%s~%s' % (shorten(r.sm_lt_path, sh1, 'smlt'), - shorten(r.gsim_rlz.lt_path, sh2, 'gslt')) + sm_p = shorten(r.sm_lt_path, sh1, 'smlt') + gs_p = shorten(r.gsim_rlz.lt_path, sh2, 'gslt') + if sh3 is not None and r.site_rlz is not None: + si_p = sh3[r.site_rlz.value] + path = '%s~%s~%s' % (sm_p, gs_p, si_p) + else: + path = '%s~%s' % (sm_p, gs_p) tups.append((r.ordinal, path, r.weight[-1])) return numpy.array(tups, rlz_dt) diff --git a/openquake/hazardlib/site_lt.py b/openquake/hazardlib/site_lt.py new file mode 100644 index 000000000000..40e97b0cb9dd --- /dev/null +++ b/openquake/hazardlib/site_lt.py @@ -0,0 +1,225 @@ +# -*- coding: utf-8 -*- +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright (C) 2010-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 . +""" +Epistemic uncertainty on the site model: a NRML logic tree whose single +``uncertaintyType="siteModel"`` branchset lists per-branch site-model +files with weights summing to 1 +""" +import os +import numpy + +from openquake.baselib import hdf5 +from openquake.baselib.general import BASE183 +from openquake.hazardlib import InvalidFile, nrml +from openquake.hazardlib import lt +from openquake.hazardlib.lt import Realization + + +F32 = numpy.float32 + + +def _iter_branchsets(lt_node): + # Tolerate both flat and -wrapped layouts + for child in lt_node: + if child.tag.endswith('logicTreeBranchSet'): + yield child + elif child.tag.endswith('logicTreeBranchingLevel'): + for bset in child: + if bset.tag.endswith('logicTreeBranchSet'): + yield bset + + +class SiteModelLogicTree(object): + """ + Parser for a site-model logic tree NRML XML + """ + filename = '' + + @classmethod + def is_site_model_lt(cls, filename): + """ + :returns: ``True`` if the given XML file is a site-model logic tree + """ + try: + root = nrml.read(filename) + for bset in _iter_branchsets(root.logicTree): + if bset.attrib.get('uncertaintyType') == 'siteModel': + return True + except Exception: + pass + return False + + def __init__(self, filename, base_path=None): + self.filename = filename + self.base_path = base_path or os.path.dirname(filename) + self.branches = [] # list of (branchID, filename, weight) + self.branchset_id = '' + self._parse() + + def _parse(self): + root = nrml.read(self.filename) + try: + ltree = root.logicTree + except AttributeError: + raise InvalidFile( + '%s: missing element' % self.filename) + bsets = list(_iter_branchsets(ltree)) + if not bsets: + raise InvalidFile( + '%s: no siteModel branchset found' % self.filename) + if len(bsets) > 1: + raise InvalidFile( + '%s: only one is supported' + % self.filename) + [bset] = bsets + utype = bset.attrib.get('uncertaintyType') + if utype != 'siteModel': + raise InvalidFile( + '%s: only uncertaintyType="siteModel" is supported ' + 'in a site-model logic tree, got %r' + % (self.filename, utype)) + self.branchset_id = bset.attrib.get('branchSetID', 'bs_site') + for br in bset: + brid = br.attrib.get('branchID', '') + rel = br.uncertaintyModel.text.strip() + weight = float(br.uncertaintyWeight.text) + fname = os.path.normpath(os.path.join(self.base_path, rel)) + self.branches.append((brid, fname, weight)) + brids = [b for b, _, _ in self.branches] + if len(set(brids)) != len(brids): + dups = sorted({b for b in brids if brids.count(b) > 1}) + raise InvalidFile( + '%s: duplicate branchID(s) in site-model logic tree: %s' + % (self.filename, dups)) + # Keeps the site leg of the composite path a single BASE183 char + if len(self.branches) > len(BASE183): + raise InvalidFile( + '%s: too many branches (%d > %d)' + % (self.filename, len(self.branches), len(BASE183))) + wsum = sum(w for _, _, w in self.branches) + if abs(wsum - 1.) > 1e-5: + raise InvalidFile( + '%s: siteModel branch weights sum to %s, expected 1.0' + % (self.filename, wsum)) + + @property + def filenames(self): + return [f for _, f, _ in self.branches] + + @property + def weights(self): + return numpy.array([w for _, _, w in self.branches]) + + @property + def branch_ids(self): + return [b for b, _, _ in self.branches] + + def get_num_paths(self): + return len(self.branches) + + def __repr__(self): + return '' % ( + os.path.basename(self.filename), len(self.branches)) + + +class SiteModelsEpistemic(object): + """ + Container holding one structured site-model array per branch; branches + share identical geometry (``lon``, ``lat``, and ``depth`` if present) + and field sets + """ + def __init__(self, names, weights, arrays, filenames=None, + tree_filename='', branchset_id='bs_site'): + self.names = list(names) + self.weights = numpy.asarray(weights, F32) + self.arrays = list(arrays) + self.filenames = list(filenames) if filenames else list(names) + self.filename = tree_filename + self.branchset_id = branchset_id + self._validate() + + def _validate(self): + # NOTE: custom_site_id is intentionally not checked - users might + # use it to denote per-branch differences at the same location + ref = self.arrays[0] + ref_name = self.filenames[0] + n = len(ref) + for other, oname in zip(self.arrays[1:], self.filenames[1:]): + if len(other) != n: + raise InvalidFile( + 'Site models %s (%d sites) and %s (%d sites) have ' + 'different numbers of sites' + % (ref_name, n, oname, len(other))) + if set(other.dtype.names) != set(ref.dtype.names): + diff = set(ref.dtype.names) ^ set(other.dtype.names) + raise InvalidFile( + 'Site models %s and %s have different field sets ' + '(differing fields: %s)' + % (ref_name, oname, sorted(diff))) + for name in ('lon', 'lat', 'depth'): + if name in ref.dtype.names and not numpy.allclose( + ref[name], other[name]): + raise InvalidFile( + 'Site models %s and %s must have identical %s ' + 'values in the same order' + % (ref_name, oname, name)) + + def __len__(self): + return len(self.arrays) + + @property + def Rsite(self): + """ + :returns: number of site-model realizations + """ + return len(self.arrays) + + def get_realizations(self): + """ + :returns: a list of :class:`Realization` objects, one per branch + """ + return [Realization(value=name, weight=float(w), ordinal=i, + lt_path=(name,), samples=1) + for i, (name, w) in enumerate(zip(self.names, self.weights))] + + def sample(self, n, seed, sampling_method='early_weights'): + """ + Monte-Carlo sample ``n`` site branches with prob = branch weight; + returns :class:`Realization` objects (branches may repeat or be absent) + """ + probs = lt.random(n, seed, sampling_method) + return lt.sample(self.get_realizations(), probs, sampling_method) + + @property + def shortener(self): + """ + :returns: dict ``branchID -> single BASE183 character`` (consistent + with the SSC and GSIM shorteners) + """ + return {name: BASE183[i] for i, name in enumerate(self.names)} + + def __repr__(self): + return '' % ( + self.Rsite, self.weights.tolist()) + + +site_model_lt_dt = numpy.dtype([ + ('name', hdf5.vstr), + ('weight', F32), + ('filename', hdf5.vstr), +]) diff --git a/openquake/hazardlib/tests/site_lt_test.py b/openquake/hazardlib/tests/site_lt_test.py new file mode 100644 index 000000000000..99f0fa7d09ad --- /dev/null +++ b/openquake/hazardlib/tests/site_lt_test.py @@ -0,0 +1,334 @@ +# -*- coding: utf-8 -*- +# vim: tabstop=4 shiftwidth=4 softtabstop=4 +# +# Copyright (C) 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 . +import os +import tempfile +import unittest +import numpy +from openquake.baselib import InvalidFile, hdf5 +from openquake.hazardlib import logictree +from openquake.hazardlib.gsim_lt import GsimLogicTree +from openquake.hazardlib.logictree import ( + SourceModelLogicTree, FullLogicTree) +from openquake.hazardlib.site_lt import ( + SiteModelLogicTree, SiteModelsEpistemic) + + +# Valid 2-branch site LT: branchsets directly under +FLAT_XML = ''' + + + + + rock.csv + 0.6 + + + soil.csv + 0.4 + + + +''' + +# Same tree wrapped in (older NRML nesting) +WRAPPED_XML = ''' + + + + + + rock.csv + 0.6 + + + soil.csv + 0.4 + + + + +''' + +# Branch weights sum to 1.1 instead of 1.0, should be rejected by the parser +BAD_WEIGHT_XML = ''' + + + + + rock.csv + 0.7 + + + soil.csv + 0.4 + + + +''' + +# uncertaintyType is gmpeModel not siteModel, should be rejected +WRONG_UTYPE_XML = ''' + + + + + Foo + 1.0 + + + +''' + +# Two branches sharing the same branchID, should be rejected +DUP_BRID_XML = ''' + + + + + rock.csv + 0.6 + + + soil.csv + 0.4 + + + +''' + + +# with no at all +NO_BSET_XML = ''' + + + +''' + + +def _write(text): + fd, path = tempfile.mkstemp(suffix='.xml', text=True) + os.close(fd) + with open(path, 'w', encoding='utf-8') as f: + f.write(text) + return path + + +class SiteModelLogicTreeTest(unittest.TestCase): + + def test_parses_both_flat_and_branchinglevel_layouts(self): + # Two accepted NRML nestings should yield identical branches + flat = SiteModelLogicTree(_write(FLAT_XML)) + wrap = SiteModelLogicTree(_write(WRAPPED_XML)) + self.assertEqual(flat.branch_ids, wrap.branch_ids) + numpy.testing.assert_allclose(flat.weights, wrap.weights) + + def test_rejects_wrong_uncertainty_type(self): + with self.assertRaises(InvalidFile) as ctx: + SiteModelLogicTree(_write(WRONG_UTYPE_XML)) + self.assertIn('siteModel', str(ctx.exception)) + + def test_rejects_weights_not_summing_to_one(self): + with self.assertRaises(InvalidFile) as ctx: + SiteModelLogicTree(_write(BAD_WEIGHT_XML)) + self.assertIn('sum to', str(ctx.exception)) + + def test_rejects_duplicate_branch_ids(self): + with self.assertRaises(InvalidFile) as ctx: + SiteModelLogicTree(_write(DUP_BRID_XML)) + self.assertIn('duplicate', str(ctx.exception).lower()) + + def test_rejects_missing_branchset(self): + with self.assertRaises(InvalidFile) as ctx: + SiteModelLogicTree(_write(NO_BSET_XML)) + self.assertIn('no siteModel branchset', str(ctx.exception)) + + +class SiteModelsEpistemicTest(unittest.TestCase): + + dt = numpy.dtype([('lon', float), ('lat', float), ('vs30', float)]) + + def _arr(self, lon, lat, vs30): + return numpy.array( + list(zip(lon, lat, vs30)), self.dt) + + def test_coords_mismatch_is_rejected(self): + # Make sure non-identical coords raise error + a = self._arr([-65., -64.], [0., 0.], [760., 760.]) + b = self._arr([-65., -63.], [0., 0.], [400., 400.]) + with self.assertRaises(InvalidFile) as ctx: + SiteModelsEpistemic(['A', 'B'], [0.6, 0.4], [a, b]) + self.assertIn('identical lon values', str(ctx.exception)) + + def test_depth_mismatch_is_rejected(self): + # When depth is present, mismatched depths across branches must + # also be rejected (lon/lat identical here to isolate depth) + dt = numpy.dtype([('lon', float), ('lat', float), + ('depth', float), ('vs30', float)]) + a = numpy.array([(-65., 0., 0., 760.), + (-64., 0., 0., 760.)], dt) + b = numpy.array([(-65., 0., 0., 400.), + (-64., 0., 10., 400.)], dt) + with self.assertRaises(InvalidFile) as ctx: + SiteModelsEpistemic(['A', 'B'], [0.6, 0.4], [a, b]) + self.assertIn('identical depth values', str(ctx.exception)) + + def test_field_set_mismatch_is_rejected(self): + # Branches must expose the same site parameters; a differing + # field set (e.g. one branch has z1pt0, the other does not) + # must be rejected before the per-field geometry loop runs + dt_a = numpy.dtype([('lon', float), ('lat', float), + ('vs30', float)]) + dt_b = numpy.dtype([('lon', float), ('lat', float), + ('vs30', float), ('z1pt0', float)]) + a = numpy.array([(-65., 0., 760.)], dt_a) + b = numpy.array([(-65., 0., 400., 50.)], dt_b) + with self.assertRaises(InvalidFile) as ctx: + SiteModelsEpistemic(['A', 'B'], [0.6, 0.4], [a, b]) + self.assertIn('different field sets', str(ctx.exception)) + + def test_shortener_uses_base183_and_is_unique(self): + # The site leg of the composite path uses BASE183, consistent + # with the SSC and GSIM shorteners; short chars must be unique + site = self._arr([-65.], [0.], [760.]) + names = ['b%d' % i for i in range(30)] # Make 30 branch names + weights = numpy.full(30, 1.0 / 30) # Assign weight to each + smep = SiteModelsEpistemic(names, weights, [site] * 30) + chars = list(smep.shortener.values()) + self.assertEqual(len(chars), len(set(chars))) # Unique per branch + + +class ReduceFullWithSiteLTTest(unittest.TestCase): + + def _full_lt_with_site_lt(self): + dt = numpy.dtype([('lon', float), ('lat', float), ('vs30', float)]) + a = numpy.array([(-65., 0., 760.)], dt) + b = numpy.array([(-65., 0., 360.)], dt) + smep = SiteModelsEpistemic( + ['rock', 'soil'], [0.6, 0.4], [a, b], + tree_filename='site_lt.xml', branchset_id='bs_site') + full_lt = FullLogicTree.fake(GsimLogicTree.from_('[FromFile]')) + full_lt.source_model_lt = SourceModelLogicTree.fake() + full_lt.site_model_lt = smep + return full_lt, smep + + def test_site_branch_shared_across_cluster_is_marked_reducible(self): + # Two rlzs both pinned to rock: the soil branch is unused + full_lt, smep = self._full_lt_with_site_lt() + rock_short = smep.shortener['rock'] + paths = ['A~A~%s' % rock_short, 'A~A~%s' % rock_short] + res = logictree.reduce_full(full_lt, paths) + self.assertIn('site_lt.xml', res) + self.assertEqual(res['site_lt.xml'], {'bs_site': ['rock']}) + # before = 1 (ssc) * 1 (gsim) * 2 (site) + before, _after = res['size_before_after'] + self.assertEqual(before, 2) + + def test_multiple_site_branches_in_cluster_not_reducible(self): + # Two rlzs on different site branches: nothing to reduce + full_lt, smep = self._full_lt_with_site_lt() + paths = ['A~A~%s' % smep.shortener['rock'], + 'A~A~%s' % smep.shortener['soil']] + res = logictree.reduce_full(full_lt, paths) + self.assertNotIn('site_lt.xml', res) + + +class FullLogicTreeRoundtripTest(unittest.TestCase): + + def _build(self): + dt = numpy.dtype([('lon', float), ('lat', float), ('vs30', float)]) + a = numpy.array([(-65., 0., 760.)], dt) + b = numpy.array([(-65., 0., 360.)], dt) + full_lt = FullLogicTree.fake(GsimLogicTree.from_('[FromFile]')) + full_lt.source_model_lt = SourceModelLogicTree.fake() + full_lt.site_model_lt = SiteModelsEpistemic( + ['rock', 'soil'], [0.6, 0.4], [a, b], + filenames=['rock.csv', 'soil.csv'], + tree_filename='site_lt.xml', branchset_id='bs_site') + return full_lt + + def _roundtrip(self, full_lt): + fd, path = tempfile.mkstemp(suffix='.hdf5') + os.close(fd) + with hdf5.File(path, 'w') as f: + f['flt'] = full_lt + with hdf5.File(path, 'r') as f: + return f['flt'] + + def test_roundtrip_with_site_lt_preserves_metadata(self): + # names, weights, filenames, tree_filename, branchset_id + # must survive __toh5__/__fromh5__ + full_lt = self._build() + reloaded = self._roundtrip(full_lt) + smep = reloaded.site_model_lt + self.assertIsNotNone(smep) + self.assertEqual(smep.names, ['rock', 'soil']) + numpy.testing.assert_allclose(smep.weights, [0.6, 0.4]) + self.assertEqual(smep.filenames, ['rock.csv', 'soil.csv']) + self.assertEqual(smep.filename, 'site_lt.xml') + self.assertEqual(smep.branchset_id, 'bs_site') + + +class GetRealizationsWithSiteLTTest(unittest.TestCase): + + def _build(self, num_samples=0, sampling_method='early_weights'): + dt = numpy.dtype([('lon', float), ('lat', float), ('vs30', float)]) + a = numpy.array([(-65., 0., 760.)], dt) + b = numpy.array([(-65., 0., 360.)], dt) + full_lt = FullLogicTree.fake(GsimLogicTree.from_('[FromFile]')) + full_lt.source_model_lt = SourceModelLogicTree.fake() + full_lt.source_model_lt.num_samples = num_samples + full_lt.source_model_lt.sampling_method = sampling_method + full_lt.site_model_lt = SiteModelsEpistemic( + ['rock', 'soil'], [0.6, 0.4], [a, b]) + full_lt.init() # Pick up the site LT and num_samples + return full_lt + + def test_full_enumeration_produces_outer_product(self): + # 1 SSC * 1 GSIM * 2 SITE = 2 rlzs, weight = w_ssc * w_gmm * w_site + full_lt = self._build(num_samples=0) + rlzs = full_lt.get_realizations() + self.assertEqual(len(rlzs), 2) + self.assertTrue(all(r.site_rlz is not None for r in rlzs)) + weights = sorted(r.weight[0] for r in rlzs) + numpy.testing.assert_allclose(weights, [0.4, 0.6], atol=1e-6) + numpy.testing.assert_allclose( + sum(r.weight[0] for r in rlzs), 1.0, atol=1e-6) + + def test_early_weights_sampling_produces_uniform_weights(self): + # early_weights forces uniform 1/num_samples weight per rlz + full_lt = self._build(num_samples=4) + rlzs = full_lt.get_realizations() + self.assertEqual(len(rlzs), 4) + self.assertTrue(all(r.site_rlz is not None for r in rlzs)) + for r in rlzs: + numpy.testing.assert_allclose(r.weight[0], 1.0 / 4) + + def test_late_weights_sampling_preserves_tree_weights(self): + # late_weights preserves per-rlz tree-weight products (rescaled) + full_lt = self._build(num_samples=100, + sampling_method='late_weights') + rlzs = full_lt.get_realizations() + self.assertEqual(len(rlzs), 100) + self.assertTrue(all(r.site_rlz is not None for r in rlzs)) + numpy.testing.assert_allclose( + sum(r.weight[0] for r in rlzs), 1.0, atol=1e-6) + # Exactly two distinct weights (rock/soil) with ratio 0.6 / 0.4 + distinct = sorted(set(round(float(r.weight[0]), 9) for r in rlzs)) + self.assertEqual(len(distinct), 2) + numpy.testing.assert_allclose( + distinct[1] / distinct[0], 0.6 / 0.4, atol=1e-6) diff --git a/openquake/qa_tests_data/logictree/README.md b/openquake/qa_tests_data/logictree/README.md index 7846f8500076..0d187be0202b 100644 --- a/openquake/qa_tests_data/logictree/README.md +++ b/openquake/qa_tests_data/logictree/README.md @@ -27,6 +27,14 @@ | case\_22 | Test sigma_model_alatik2015 | | case\_23 | Arctic region and IDL (no bounding box) | | case\_23\_bis | Correlated uncertainties | +| case\_24 | Site model logic tree + classical, full enumeration | +| case\_24\_smpl | Site model logic tree + classical, sampling | +| case\_24\_fm | Site model logic tree + classical fastmean, full enumeration | +| case\_24\_fm\_smpl | Site model logic tree + classical fastmean, sampling | +| case\_24\_dsg | Site model logic tree + disaggregation, full enumeration | +| case\_24\_dsg_smpl | Site model logic tree + disaggregation, sampling | +| case\_24\_xml | Site model logic tree with NRML XML branch files (not CSV) | +| case\_24\_hdf5 | Site model logic tree + disagg reading a classical with sampling dstore | | case\_28 | Test collapse\_gsim\_logic\_tree | | case\_28\_bis | Test missing z1pt0 | | case\_25 | BC Hydro NVA SSC LT source model LT | diff --git a/openquake/qa_tests_data/logictree/case_24/__init__.py b/openquake/qa_tests_data/logictree/case_24/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/openquake/qa_tests_data/logictree/case_24/expected/disagg_Mag-mean-0.csv b/openquake/qa_tests_data/logictree/case_24/expected/disagg_Mag-mean-0.csv new file mode 100644 index 000000000000..fe2fa4a14efe --- /dev/null +++ b/openquake/qa_tests_data/logictree/case_24/expected/disagg_Mag-mean-0.csv @@ -0,0 +1,6 @@ +#,,,,"generated_by='OpenQuake engine 3.27.0-gitdc8d6a57e0', start_date='2026-07-26T09:12:12', checksum=3232325529, investigation_time=50.0, mag_bin_edges=[5.0, 5.5, 6.0, 6.5, 7.0], dist_bin_edges=[0.0, 25.0, 50.0, 75.0, 100.0, 125.0, 150.0, 175.0, 200.0], lon_bin_edges=[-66.79864, -65.0, -63.20136], lat_bin_edges=[-1.79864, 0.0, 1.79864], eps_bin_edges=[-3.0, -1.0, 1.0, 3.0], tectonic_region_types=['Active Shallow Crust'], lon=-65.0, lat=0.0" +imt,iml,poe,mag,mean +PGA,1.30133E-01,1.00000E-01,5.25000E+00,5.73591E-02 +PGA,1.30133E-01,1.00000E-01,5.75000E+00,2.76096E-02 +PGA,1.30133E-01,1.00000E-01,6.25000E+00,2.20332E-02 +PGA,1.30133E-01,1.00000E-01,6.75000E+00,1.47474E-03 diff --git a/openquake/qa_tests_data/logictree/case_24/expected/disagg_Mag-mean-1.csv b/openquake/qa_tests_data/logictree/case_24/expected/disagg_Mag-mean-1.csv new file mode 100644 index 000000000000..93991d9e0dd8 --- /dev/null +++ b/openquake/qa_tests_data/logictree/case_24/expected/disagg_Mag-mean-1.csv @@ -0,0 +1,6 @@ +#,,,,"generated_by='OpenQuake engine 3.27.0-gitdc8d6a57e0', start_date='2026-07-26T09:12:12', checksum=3232325529, investigation_time=50.0, mag_bin_edges=[5.0, 5.5, 6.0, 6.5, 7.0], dist_bin_edges=[0.0, 25.0, 50.0, 75.0, 100.0, 125.0, 150.0, 175.0, 200.0], lon_bin_edges=[-66.84864068487155, -65.05, -63.25135931512843], lat_bin_edges=[-1.74864, 0.05, 1.84864], eps_bin_edges=[-3.0, -1.0, 1.0, 3.0], tectonic_region_types=['Active Shallow Crust'], lon=-65.05, lat=0.05" +imt,iml,poe,mag,mean +PGA,1.26271E-01,1.00000E-01,5.25000E+00,5.69932E-02 +PGA,1.26271E-01,1.00000E-01,5.75000E+00,2.91311E-02 +PGA,1.26271E-01,1.00000E-01,6.25000E+00,2.29844E-02 +PGA,1.26271E-01,1.00000E-01,6.75000E+00,1.48433E-03 diff --git a/openquake/qa_tests_data/logictree/case_24/expected/disagg_hdf5_Mag-mean-0.csv b/openquake/qa_tests_data/logictree/case_24/expected/disagg_hdf5_Mag-mean-0.csv new file mode 100644 index 000000000000..72ebdc347ca5 --- /dev/null +++ b/openquake/qa_tests_data/logictree/case_24/expected/disagg_hdf5_Mag-mean-0.csv @@ -0,0 +1,6 @@ +#,,,,"generated_by='OpenQuake engine 3.27.0-git86ab1bafc9', start_date='2026-07-26T23:31:41', checksum=1588112413, investigation_time=50.0, mag_bin_edges=[5.0, 5.5, 6.0, 6.5, 7.0], dist_bin_edges=[0.0, 25.0, 50.0, 75.0, 100.0, 125.0, 150.0, 175.0, 200.0], lon_bin_edges=[-66.79864, -65.0, -63.20136], lat_bin_edges=[-1.79864, 0.0, 1.79864], eps_bin_edges=[-3.0, -1.0, 1.0, 3.0], tectonic_region_types=['Active Shallow Crust'], lon=-65.0, lat=0.0" +imt,iml,poe,mag,mean +PGA,1.23587E-01,1.00000E-01,5.25000E+00,4.97055E-02 +PGA,1.23587E-01,1.00000E-01,5.75000E+00,2.25801E-02 +PGA,1.23587E-01,1.00000E-01,6.25000E+00,1.94902E-02 +PGA,1.23587E-01,1.00000E-01,6.75000E+00,0.00000E+00 diff --git a/openquake/qa_tests_data/logictree/case_24/expected/disagg_hdf5_Mag-mean-1.csv b/openquake/qa_tests_data/logictree/case_24/expected/disagg_hdf5_Mag-mean-1.csv new file mode 100644 index 000000000000..2bc86f71d293 --- /dev/null +++ b/openquake/qa_tests_data/logictree/case_24/expected/disagg_hdf5_Mag-mean-1.csv @@ -0,0 +1,6 @@ +#,,,,"generated_by='OpenQuake engine 3.27.0-git86ab1bafc9', start_date='2026-07-26T23:31:41', checksum=1588112413, investigation_time=50.0, mag_bin_edges=[5.0, 5.5, 6.0, 6.5, 7.0], dist_bin_edges=[0.0, 25.0, 50.0, 75.0, 100.0, 125.0, 150.0, 175.0, 200.0], lon_bin_edges=[-66.84864068487155, -65.05, -63.25135931512843], lat_bin_edges=[-1.74864, 0.05, 1.84864], eps_bin_edges=[-3.0, -1.0, 1.0, 3.0], tectonic_region_types=['Active Shallow Crust'], lon=-65.05, lat=0.05" +imt,iml,poe,mag,mean +PGA,1.20570E-01,1.00000E-01,5.25000E+00,4.82381E-02 +PGA,1.20570E-01,1.00000E-01,5.75000E+00,2.39251E-02 +PGA,1.20570E-01,1.00000E-01,6.25000E+00,2.05925E-02 +PGA,1.20570E-01,1.00000E-01,6.75000E+00,0.00000E+00 diff --git a/openquake/qa_tests_data/logictree/case_24/expected/disagg_sampling_Mag-mean-0.csv b/openquake/qa_tests_data/logictree/case_24/expected/disagg_sampling_Mag-mean-0.csv new file mode 100644 index 000000000000..1e713abadbc7 --- /dev/null +++ b/openquake/qa_tests_data/logictree/case_24/expected/disagg_sampling_Mag-mean-0.csv @@ -0,0 +1,6 @@ +#,,,,"generated_by='OpenQuake engine 3.27.0-git74b1f464a5', start_date='2026-07-26T12:13:40', checksum=1127840431, investigation_time=50.0, mag_bin_edges=[5.0, 5.5, 6.0, 6.5, 7.0], dist_bin_edges=[0.0, 25.0, 50.0, 75.0, 100.0, 125.0, 150.0, 175.0, 200.0], lon_bin_edges=[-66.79864, -65.0, -63.20136], lat_bin_edges=[-1.79864, 0.0, 1.79864], eps_bin_edges=[-3.0, -1.0, 1.0, 3.0], tectonic_region_types=['Active Shallow Crust'], lon=-65.0, lat=0.0" +imt,iml,poe,mag,mean +PGA,1.23587E-01,1.00000E-01,5.25000E+00,5.85165E-02 +PGA,1.23587E-01,1.00000E-01,5.75000E+00,2.61915E-02 +PGA,1.23587E-01,1.00000E-01,6.25000E+00,2.23483E-02 +PGA,1.23587E-01,1.00000E-01,6.75000E+00,0.00000E+00 diff --git a/openquake/qa_tests_data/logictree/case_24/expected/disagg_sampling_Mag-mean-1.csv b/openquake/qa_tests_data/logictree/case_24/expected/disagg_sampling_Mag-mean-1.csv new file mode 100644 index 000000000000..ac8ba9aa50c6 --- /dev/null +++ b/openquake/qa_tests_data/logictree/case_24/expected/disagg_sampling_Mag-mean-1.csv @@ -0,0 +1,6 @@ +#,,,,"generated_by='OpenQuake engine 3.27.0-git74b1f464a5', start_date='2026-07-26T12:13:40', checksum=1127840431, investigation_time=50.0, mag_bin_edges=[5.0, 5.5, 6.0, 6.5, 7.0], dist_bin_edges=[0.0, 25.0, 50.0, 75.0, 100.0, 125.0, 150.0, 175.0, 200.0], lon_bin_edges=[-66.84864068487155, -65.05, -63.25135931512843], lat_bin_edges=[-1.74864, 0.05, 1.84864], eps_bin_edges=[-3.0, -1.0, 1.0, 3.0], tectonic_region_types=['Active Shallow Crust'], lon=-65.05, lat=0.05" +imt,iml,poe,mag,mean +PGA,1.20570E-01,1.00000E-01,5.25000E+00,5.80196E-02 +PGA,1.20570E-01,1.00000E-01,5.75000E+00,2.76877E-02 +PGA,1.20570E-01,1.00000E-01,6.25000E+00,2.34087E-02 +PGA,1.20570E-01,1.00000E-01,6.75000E+00,0.00000E+00 diff --git a/openquake/qa_tests_data/logictree/case_24/expected/disagg_sampling_late_Mag-mean-0.csv b/openquake/qa_tests_data/logictree/case_24/expected/disagg_sampling_late_Mag-mean-0.csv new file mode 100644 index 000000000000..55487f373891 --- /dev/null +++ b/openquake/qa_tests_data/logictree/case_24/expected/disagg_sampling_late_Mag-mean-0.csv @@ -0,0 +1,6 @@ +#,,,,"generated_by='OpenQuake engine 3.27.0-git3a62871f55', start_date='2026-07-26T23:08:24', checksum=19771000, investigation_time=50.0, mag_bin_edges=[5.0, 5.5, 6.0, 6.5, 7.0], dist_bin_edges=[0.0, 25.0, 50.0, 75.0, 100.0, 125.0, 150.0, 175.0, 200.0], lon_bin_edges=[-66.79864, -65.0, -63.20136], lat_bin_edges=[-1.79864, 0.0, 1.79864], eps_bin_edges=[-3.0, -1.0, 1.0, 3.0], tectonic_region_types=['Active Shallow Crust'], lon=-65.0, lat=0.0" +imt,iml,poe,mag,mean +PGA,1.07694E-01,1.00000E-01,5.25000E+00,5.66913E-02 +PGA,1.07694E-01,1.00000E-01,5.75000E+00,2.52186E-02 +PGA,1.07694E-01,1.00000E-01,6.25000E+00,2.14547E-02 +PGA,1.07694E-01,1.00000E-01,6.75000E+00,1.50592E-03 diff --git a/openquake/qa_tests_data/logictree/case_24/expected/disagg_sampling_late_Mag-mean-1.csv b/openquake/qa_tests_data/logictree/case_24/expected/disagg_sampling_late_Mag-mean-1.csv new file mode 100644 index 000000000000..4344bfc8fbad --- /dev/null +++ b/openquake/qa_tests_data/logictree/case_24/expected/disagg_sampling_late_Mag-mean-1.csv @@ -0,0 +1,6 @@ +#,,,,"generated_by='OpenQuake engine 3.27.0-git3a62871f55', start_date='2026-07-26T23:08:24', checksum=19771000, investigation_time=50.0, mag_bin_edges=[5.0, 5.5, 6.0, 6.5, 7.0], dist_bin_edges=[0.0, 25.0, 50.0, 75.0, 100.0, 125.0, 150.0, 175.0, 200.0], lon_bin_edges=[-66.84864068487155, -65.05, -63.25135931512843], lat_bin_edges=[-1.74864, 0.05, 1.84864], eps_bin_edges=[-3.0, -1.0, 1.0, 3.0], tectonic_region_types=['Active Shallow Crust'], lon=-65.05, lat=0.05" +imt,iml,poe,mag,mean +PGA,1.06282E-01,1.00000E-01,5.25000E+00,5.58180E-02 +PGA,1.06282E-01,1.00000E-01,5.75000E+00,2.62635E-02 +PGA,1.06282E-01,1.00000E-01,6.25000E+00,2.20348E-02 +PGA,1.06282E-01,1.00000E-01,6.75000E+00,1.48391E-03 diff --git a/openquake/qa_tests_data/logictree/case_24/expected/fastmean_hazard_curve-mean-PGA.csv b/openquake/qa_tests_data/logictree/case_24/expected/fastmean_hazard_curve-mean-PGA.csv new file mode 100644 index 000000000000..5cfeca27f1cd --- /dev/null +++ b/openquake/qa_tests_data/logictree/case_24/expected/fastmean_hazard_curve-mean-PGA.csv @@ -0,0 +1,4 @@ +#,,,,,,,"generated_by='OpenQuake engine 3.27.0-gitdc8d6a57e0', start_date='2026-07-26T09:11:58', checksum=1712609555, kind='mean', investigation_time=50.0, imt='PGA'" +lon,lat,depth,poe-0.0500000,poe-0.1000000,poe-0.2000000,poe-0.4000000,poe-0.8000000 +-65.00000,0.00000,0.00000,3.338774E-01,1.536614E-01,5.096841E-02,1.163816E-02,1.397789E-03 +-65.05000,0.05000,0.00000,3.293542E-01,1.516250E-01,4.541731E-02,7.132113E-03,3.384352E-04 diff --git a/openquake/qa_tests_data/logictree/case_24/expected/fastmean_sampling_hazard_curve-mean-PGA.csv b/openquake/qa_tests_data/logictree/case_24/expected/fastmean_sampling_hazard_curve-mean-PGA.csv new file mode 100644 index 000000000000..17c6f66f71a6 --- /dev/null +++ b/openquake/qa_tests_data/logictree/case_24/expected/fastmean_sampling_hazard_curve-mean-PGA.csv @@ -0,0 +1,4 @@ +#,,,,,,,"generated_by='OpenQuake engine 3.27.0-git74b1f464a5', start_date='2026-07-26T12:13:30', checksum=954690005, kind='mean', investigation_time=50.0, imt='PGA'" +lon,lat,depth,poe-0.0500000,poe-0.1000000,poe-0.2000000,poe-0.4000000,poe-0.8000000 +-65.00000,0.00000,0.00000,3.214122E-01,1.415254E-01,4.591149E-02,1.001513E-02,1.072228E-03 +-65.05000,0.05000,0.00000,3.187771E-01,1.406486E-01,4.028273E-02,5.809307E-03,1.813173E-04 diff --git a/openquake/qa_tests_data/logictree/case_24/expected/fastmean_sampling_late_hazard_curve-mean-PGA.csv b/openquake/qa_tests_data/logictree/case_24/expected/fastmean_sampling_late_hazard_curve-mean-PGA.csv new file mode 100644 index 000000000000..872c588c1dbe --- /dev/null +++ b/openquake/qa_tests_data/logictree/case_24/expected/fastmean_sampling_late_hazard_curve-mean-PGA.csv @@ -0,0 +1,4 @@ +#,,,,,,,"generated_by='OpenQuake engine 3.27.0-git3a62871f55', start_date='2026-07-26T23:08:22', checksum=547069342, kind='mean', investigation_time=50.0, imt='PGA'" +lon,lat,depth,poe-0.0500000,poe-0.1000000,poe-0.2000000,poe-0.4000000,poe-0.8000000 +-65.00000,0.00000,0.00000,2.673423E-01,1.132336E-01,3.599566E-02,7.717609E-03,8.069277E-04 +-65.05000,0.05000,0.00000,2.656302E-01,1.122427E-01,3.077030E-02,4.192352E-03,1.103282E-04 diff --git a/openquake/qa_tests_data/logictree/case_24/expected/hazard_curve-mean-PGA.csv b/openquake/qa_tests_data/logictree/case_24/expected/hazard_curve-mean-PGA.csv new file mode 100644 index 000000000000..464732a20990 --- /dev/null +++ b/openquake/qa_tests_data/logictree/case_24/expected/hazard_curve-mean-PGA.csv @@ -0,0 +1,4 @@ +#,,,,,,,"generated_by='OpenQuake engine 3.27.0-gitdc8d6a57e0', start_date='2026-07-26T09:08:36', checksum=718876448, kind='mean', investigation_time=50.0, imt='PGA'" +lon,lat,depth,poe-0.0500000,poe-0.1000000,poe-0.2000000,poe-0.4000000,poe-0.8000000 +-65.00000,0.00000,0.00000,3.264544E-01,1.516123E-01,5.071152E-02,1.162187E-02,1.397461E-03 +-65.05000,0.05000,0.00000,3.222976E-01,1.496378E-01,4.517405E-02,7.122436E-03,3.383258E-04 diff --git a/openquake/qa_tests_data/logictree/case_24/expected/hazard_curve-rlz-000-PGA.csv b/openquake/qa_tests_data/logictree/case_24/expected/hazard_curve-rlz-000-PGA.csv new file mode 100644 index 000000000000..3356493162b5 --- /dev/null +++ b/openquake/qa_tests_data/logictree/case_24/expected/hazard_curve-rlz-000-PGA.csv @@ -0,0 +1,4 @@ +#,,,,,,,"generated_by='OpenQuake engine 3.27.0-gitdc8d6a57e0', start_date='2026-07-26T09:08:36', checksum=718876448, kind='rlz-000', investigation_time=50.0, imt='PGA'" +lon,lat,depth,poe-0.0500000,poe-0.1000000,poe-0.2000000,poe-0.4000000,poe-0.8000000 +-65.00000,0.00000,0.00000,2.914701E-01,1.230319E-01,3.884419E-02,8.208791E-03,8.367875E-04 +-65.05000,0.05000,0.00000,2.899046E-01,1.218920E-01,3.277298E-02,4.278049E-03,9.226844E-05 diff --git a/openquake/qa_tests_data/logictree/case_24/expected/hazard_curve-rlz-007-PGA.csv b/openquake/qa_tests_data/logictree/case_24/expected/hazard_curve-rlz-007-PGA.csv new file mode 100644 index 000000000000..e0720d483018 --- /dev/null +++ b/openquake/qa_tests_data/logictree/case_24/expected/hazard_curve-rlz-007-PGA.csv @@ -0,0 +1,4 @@ +#,,,,,,,"generated_by='OpenQuake engine 3.27.0-gitdc8d6a57e0', start_date='2026-07-26T09:08:36', checksum=718876448, kind='rlz-007', investigation_time=50.0, imt='PGA'" +lon,lat,depth,poe-0.0500000,poe-0.1000000,poe-0.2000000,poe-0.4000000,poe-0.8000000 +-65.00000,0.00000,0.00000,3.071955E-01,1.552624E-01,5.480368E-02,1.360644E-02,1.985971E-03 +-65.05000,0.05000,0.00000,3.010648E-01,1.524776E-01,5.117391E-02,9.699080E-03,7.330623E-04 diff --git a/openquake/qa_tests_data/logictree/case_24/expected/hazard_map-mean.csv b/openquake/qa_tests_data/logictree/case_24/expected/hazard_map-mean.csv new file mode 100644 index 000000000000..9f1445cdd773 --- /dev/null +++ b/openquake/qa_tests_data/logictree/case_24/expected/hazard_map-mean.csv @@ -0,0 +1,4 @@ +#,,"generated_by='OpenQuake engine 3.27.0-gitdc8d6a57e0', start_date='2026-07-26T09:08:36', checksum=718876448, kind='mean', investigation_time=50.0" +lon,lat,PGA-0.1 +-65.00000,0.00000,1.557507E-01 +-65.05000,0.05000,1.539323E-01 diff --git a/openquake/qa_tests_data/logictree/case_24/expected/hazard_map-rlz-000.csv b/openquake/qa_tests_data/logictree/case_24/expected/hazard_map-rlz-000.csv new file mode 100644 index 000000000000..9da3729e6610 --- /dev/null +++ b/openquake/qa_tests_data/logictree/case_24/expected/hazard_map-rlz-000.csv @@ -0,0 +1,4 @@ +#,,"generated_by='OpenQuake engine 3.27.0-gitdc8d6a57e0', start_date='2026-07-26T09:08:36', checksum=718876448, kind='rlz-000', investigation_time=50.0" +lon,lat,PGA-0.1 +-65.00000,0.00000,1.132716E-01 +-65.05000,0.05000,1.110117E-01 diff --git a/openquake/qa_tests_data/logictree/case_24/expected/hazard_map-rlz-007.csv b/openquake/qa_tests_data/logictree/case_24/expected/hazard_map-rlz-007.csv new file mode 100644 index 000000000000..065b935c2b79 --- /dev/null +++ b/openquake/qa_tests_data/logictree/case_24/expected/hazard_map-rlz-007.csv @@ -0,0 +1,4 @@ +#,,"generated_by='OpenQuake engine 3.27.0-gitdc8d6a57e0', start_date='2026-07-26T09:08:36', checksum=718876448, kind='rlz-007', investigation_time=50.0" +lon,lat,PGA-0.1 +-65.00000,0.00000,1.340223E-01 +-65.05000,0.05000,1.307112E-01 diff --git a/openquake/qa_tests_data/logictree/case_24/expected/sampling_hazard_curve-mean-PGA.csv b/openquake/qa_tests_data/logictree/case_24/expected/sampling_hazard_curve-mean-PGA.csv new file mode 100644 index 000000000000..3da0b9089acf --- /dev/null +++ b/openquake/qa_tests_data/logictree/case_24/expected/sampling_hazard_curve-mean-PGA.csv @@ -0,0 +1,4 @@ +#,,,,,,,"generated_by='OpenQuake engine 3.27.0-git74b1f464a5', start_date='2026-07-26T12:12:46', checksum=4012843174, kind='mean', investigation_time=50.0, imt='PGA'" +lon,lat,depth,poe-0.0500000,poe-0.1000000,poe-0.2000000,poe-0.4000000,poe-0.8000000 +-65.00000,0.00000,0.00000,3.195671E-01,1.409487E-01,4.583387E-02,1.001017E-02,1.072145E-03 +-65.05000,0.05000,0.00000,3.170634E-01,1.400561E-01,4.019572E-02,5.805832E-03,1.812945E-04 diff --git a/openquake/qa_tests_data/logictree/case_24/expected/sampling_hazard_curve-rlz-000-PGA.csv b/openquake/qa_tests_data/logictree/case_24/expected/sampling_hazard_curve-rlz-000-PGA.csv new file mode 100644 index 000000000000..64ee673609b9 --- /dev/null +++ b/openquake/qa_tests_data/logictree/case_24/expected/sampling_hazard_curve-rlz-000-PGA.csv @@ -0,0 +1,4 @@ +#,,,,,,,"generated_by='OpenQuake engine 3.27.0-git74b1f464a5', start_date='2026-07-26T12:12:46', checksum=4012843174, kind='rlz-000', investigation_time=50.0, imt='PGA'" +lon,lat,depth,poe-0.0500000,poe-0.1000000,poe-0.2000000,poe-0.4000000,poe-0.8000000 +-65.00000,0.00000,0.00000,2.914701E-01,1.230319E-01,3.884419E-02,8.208791E-03,8.367875E-04 +-65.05000,0.05000,0.00000,2.899046E-01,1.218920E-01,3.277298E-02,4.278049E-03,9.226844E-05 diff --git a/openquake/qa_tests_data/logictree/case_24/expected/sampling_hazard_curve-rlz-001-PGA.csv b/openquake/qa_tests_data/logictree/case_24/expected/sampling_hazard_curve-rlz-001-PGA.csv new file mode 100644 index 000000000000..85ab9328b125 --- /dev/null +++ b/openquake/qa_tests_data/logictree/case_24/expected/sampling_hazard_curve-rlz-001-PGA.csv @@ -0,0 +1,4 @@ +#,,,,,,,"generated_by='OpenQuake engine 3.27.0-git74b1f464a5', start_date='2026-07-26T12:12:46', checksum=4012843174, kind='rlz-001', investigation_time=50.0, imt='PGA'" +lon,lat,depth,poe-0.0500000,poe-0.1000000,poe-0.2000000,poe-0.4000000,poe-0.8000000 +-65.00000,0.00000,0.00000,2.914701E-01,1.230319E-01,3.884419E-02,8.208791E-03,8.367875E-04 +-65.05000,0.05000,0.00000,2.899046E-01,1.218920E-01,3.277298E-02,4.278049E-03,9.226844E-05 diff --git a/openquake/qa_tests_data/logictree/case_24/expected/sampling_late_hazard_curve-mean-PGA.csv b/openquake/qa_tests_data/logictree/case_24/expected/sampling_late_hazard_curve-mean-PGA.csv new file mode 100644 index 000000000000..0aa9c0539aa4 --- /dev/null +++ b/openquake/qa_tests_data/logictree/case_24/expected/sampling_late_hazard_curve-mean-PGA.csv @@ -0,0 +1,4 @@ +#,,,,,,,"generated_by='OpenQuake engine 3.27.0-git3a62871f55', start_date='2026-07-26T23:08:20', checksum=3939980386, kind='mean', investigation_time=50.0, imt='PGA'" +lon,lat,depth,poe-0.0500000,poe-0.1000000,poe-0.2000000,poe-0.4000000,poe-0.8000000 +-65.00000,0.00000,0.00000,2.659727E-01,1.130217E-01,3.597610E-02,7.716715E-03,8.069188E-04 +-65.05000,0.05000,0.00000,2.642627E-01,1.120344E-01,3.075582E-02,4.191944E-03,1.102580E-04 diff --git a/openquake/qa_tests_data/logictree/case_24/expected/sampling_late_hazard_curve-rlz-000-PGA.csv b/openquake/qa_tests_data/logictree/case_24/expected/sampling_late_hazard_curve-rlz-000-PGA.csv new file mode 100644 index 000000000000..2b68e6c894ae --- /dev/null +++ b/openquake/qa_tests_data/logictree/case_24/expected/sampling_late_hazard_curve-rlz-000-PGA.csv @@ -0,0 +1,4 @@ +#,,,,,,,"generated_by='OpenQuake engine 3.27.0-git3a62871f55', start_date='2026-07-26T23:08:20', checksum=3939980386, kind='rlz-000', investigation_time=50.0, imt='PGA'" +lon,lat,depth,poe-0.0500000,poe-0.1000000,poe-0.2000000,poe-0.4000000,poe-0.8000000 +-65.00000,0.00000,0.00000,2.914701E-01,1.230319E-01,3.884419E-02,8.208791E-03,8.367875E-04 +-65.05000,0.05000,0.00000,2.899046E-01,1.218920E-01,3.277298E-02,4.278049E-03,9.226844E-05 diff --git a/openquake/qa_tests_data/logictree/case_24/expected/sampling_late_hazard_curve-rlz-001-PGA.csv b/openquake/qa_tests_data/logictree/case_24/expected/sampling_late_hazard_curve-rlz-001-PGA.csv new file mode 100644 index 000000000000..1d862767fb57 --- /dev/null +++ b/openquake/qa_tests_data/logictree/case_24/expected/sampling_late_hazard_curve-rlz-001-PGA.csv @@ -0,0 +1,4 @@ +#,,,,,,,"generated_by='OpenQuake engine 3.27.0-git3a62871f55', start_date='2026-07-26T23:08:20', checksum=3939980386, kind='rlz-001', investigation_time=50.0, imt='PGA'" +lon,lat,depth,poe-0.0500000,poe-0.1000000,poe-0.2000000,poe-0.4000000,poe-0.8000000 +-65.00000,0.00000,0.00000,2.914701E-01,1.230319E-01,3.884419E-02,8.208791E-03,8.367875E-04 +-65.05000,0.05000,0.00000,2.899046E-01,1.218920E-01,3.277298E-02,4.278049E-03,9.226844E-05 diff --git a/openquake/qa_tests_data/logictree/case_24/expected/xml_hazard_curve-mean-PGA.csv b/openquake/qa_tests_data/logictree/case_24/expected/xml_hazard_curve-mean-PGA.csv new file mode 100644 index 000000000000..3dffd66e86d0 --- /dev/null +++ b/openquake/qa_tests_data/logictree/case_24/expected/xml_hazard_curve-mean-PGA.csv @@ -0,0 +1,4 @@ +#,,,,,,,"generated_by='OpenQuake engine 3.27.0-gitdc8d6a57e0', start_date='2026-07-26T09:12:22', checksum=3069385918, kind='mean', investigation_time=50.0, imt='PGA'" +lon,lat,depth,poe-0.0500000,poe-0.1000000,poe-0.2000000,poe-0.4000000,poe-0.8000000 +-65.00000,0.00000,0.00000,3.264544E-01,1.516123E-01,5.071152E-02,1.162187E-02,1.397461E-03 +-65.05000,0.05000,0.00000,3.222976E-01,1.496378E-01,4.517405E-02,7.122436E-03,3.383258E-04 diff --git a/openquake/qa_tests_data/logictree/case_24/expected/xml_hazard_curve-rlz-000-PGA.csv b/openquake/qa_tests_data/logictree/case_24/expected/xml_hazard_curve-rlz-000-PGA.csv new file mode 100644 index 000000000000..dab86c640d01 --- /dev/null +++ b/openquake/qa_tests_data/logictree/case_24/expected/xml_hazard_curve-rlz-000-PGA.csv @@ -0,0 +1,4 @@ +#,,,,,,,"generated_by='OpenQuake engine 3.27.0-gitdc8d6a57e0', start_date='2026-07-26T09:12:22', checksum=3069385918, kind='rlz-000', investigation_time=50.0, imt='PGA'" +lon,lat,depth,poe-0.0500000,poe-0.1000000,poe-0.2000000,poe-0.4000000,poe-0.8000000 +-65.00000,0.00000,0.00000,2.914701E-01,1.230319E-01,3.884419E-02,8.208791E-03,8.367875E-04 +-65.05000,0.05000,0.00000,2.899046E-01,1.218920E-01,3.277298E-02,4.278049E-03,9.226844E-05 diff --git a/openquake/qa_tests_data/logictree/case_24/expected/xml_hazard_curve-rlz-007-PGA.csv b/openquake/qa_tests_data/logictree/case_24/expected/xml_hazard_curve-rlz-007-PGA.csv new file mode 100644 index 000000000000..22a42dbff854 --- /dev/null +++ b/openquake/qa_tests_data/logictree/case_24/expected/xml_hazard_curve-rlz-007-PGA.csv @@ -0,0 +1,4 @@ +#,,,,,,,"generated_by='OpenQuake engine 3.27.0-gitdc8d6a57e0', start_date='2026-07-26T09:12:22', checksum=3069385918, kind='rlz-007', investigation_time=50.0, imt='PGA'" +lon,lat,depth,poe-0.0500000,poe-0.1000000,poe-0.2000000,poe-0.4000000,poe-0.8000000 +-65.00000,0.00000,0.00000,3.071955E-01,1.552624E-01,5.480368E-02,1.360644E-02,1.985971E-03 +-65.05000,0.05000,0.00000,3.010648E-01,1.524776E-01,5.117391E-02,9.699080E-03,7.330623E-04 diff --git a/openquake/qa_tests_data/logictree/case_24/gsim_logic_tree.xml b/openquake/qa_tests_data/logictree/case_24/gsim_logic_tree.xml new file mode 100644 index 000000000000..33d302d9e0d1 --- /dev/null +++ b/openquake/qa_tests_data/logictree/case_24/gsim_logic_tree.xml @@ -0,0 +1,18 @@ + + + + + + ChiouYoungs2014 + 0.6 + + + BooreEtAl2014 + 0.4 + + + + diff --git a/openquake/qa_tests_data/logictree/case_24/job.ini b/openquake/qa_tests_data/logictree/case_24/job.ini new file mode 100644 index 000000000000..839715fb88c2 --- /dev/null +++ b/openquake/qa_tests_data/logictree/case_24/job.ini @@ -0,0 +1,35 @@ +[general] +description = Site model logic tree - full enumeration +calculation_mode = classical +random_seed = 24 + +[geometry] +site_model_file = site_model_lt.xml + +[logic_tree] +number_of_logic_tree_samples = 0 + +[erf] +rupture_mesh_spacing = 2.0 +width_of_mfd_bin = 0.2 +area_source_discretization = 20.0 + +[site_params] +reference_vs30_type = measured +reference_vs30_value = 760.0 +reference_depth_to_2pt5km_per_sec = 5.0 +reference_depth_to_1pt0km_per_sec = 100.0 + +[calculation] +source_model_logic_tree_file = source_model_logic_tree.xml +gsim_logic_tree_file = gsim_logic_tree.xml +investigation_time = 50.0 +intensity_measure_types_and_levels = {"PGA": [0.05, 0.1, 0.2, 0.4, 0.8]} +truncation_level = 3 +maximum_distance = 200.0 + +[output] +mean_hazard_curves = true +hazard_maps = true +poes = 0.1 +individual_rlzs = true diff --git a/openquake/qa_tests_data/logictree/case_24/job_disagg.ini b/openquake/qa_tests_data/logictree/case_24/job_disagg.ini new file mode 100644 index 000000000000..9b5a4ad16383 --- /dev/null +++ b/openquake/qa_tests_data/logictree/case_24/job_disagg.ini @@ -0,0 +1,36 @@ +[general] +description = Site model logic tree - full enumeration - disaggregation +calculation_mode = disaggregation +random_seed = 24 + +[geometry] +site_model_file = site_model_lt.xml + +[logic_tree] +number_of_logic_tree_samples = 0 + +[erf] +rupture_mesh_spacing = 2.0 +width_of_mfd_bin = 0.2 +area_source_discretization = 20.0 + +[site_params] +reference_vs30_type = measured +reference_vs30_value = 760.0 +reference_depth_to_2pt5km_per_sec = 5.0 +reference_depth_to_1pt0km_per_sec = 100.0 + +[calculation] +source_model_logic_tree_file = source_model_logic_tree.xml +gsim_logic_tree_file = gsim_logic_tree.xml +investigation_time = 50.0 +intensity_measure_types_and_levels = {"PGA": [0.05, 0.1, 0.2, 0.4, 0.8]} +truncation_level = 3 +maximum_distance = 200.0 + +[disaggregation] +poes_disagg = 0.1 +mag_bin_width = 0.5 +distance_bin_width = 25.0 +coordinate_bin_width = 1.0 +num_epsilon_bins = 3 diff --git a/openquake/qa_tests_data/logictree/case_24/job_disagg_from_parent.ini b/openquake/qa_tests_data/logictree/case_24/job_disagg_from_parent.ini new file mode 100644 index 000000000000..a3d641359948 --- /dev/null +++ b/openquake/qa_tests_data/logictree/case_24/job_disagg_from_parent.ini @@ -0,0 +1,36 @@ +[general] +description = Site LT sampling - disagg reads dstore from classical hazard with sampling +calculation_mode = disaggregation +random_seed = 24 + +[geometry] +site_model_file = site_model_lt.xml + +[logic_tree] +number_of_logic_tree_samples = 4 + +[erf] +rupture_mesh_spacing = 2.0 +width_of_mfd_bin = 0.2 +area_source_discretization = 20.0 + +[site_params] +reference_vs30_type = measured +reference_vs30_value = 760.0 +reference_depth_to_2pt5km_per_sec = 5.0 +reference_depth_to_1pt0km_per_sec = 100.0 + +[calculation] +source_model_logic_tree_file = source_model_logic_tree.xml +gsim_logic_tree_file = gsim_logic_tree.xml +investigation_time = 50.0 +intensity_measure_types_and_levels = {"PGA": [0.05, 0.1, 0.2, 0.4, 0.8]} +truncation_level = 3 +maximum_distance = 200.0 + +[disaggregation] +poes_disagg = 0.1 +mag_bin_width = 0.5 +distance_bin_width = 25.0 +coordinate_bin_width = 1.0 +num_epsilon_bins = 3 diff --git a/openquake/qa_tests_data/logictree/case_24/job_disagg_sampling.ini b/openquake/qa_tests_data/logictree/case_24/job_disagg_sampling.ini new file mode 100644 index 000000000000..534d30b6ec12 --- /dev/null +++ b/openquake/qa_tests_data/logictree/case_24/job_disagg_sampling.ini @@ -0,0 +1,36 @@ +[general] +description = Site model logic tree - sampling - disaggregation with sampling +calculation_mode = disaggregation +random_seed = 24 + +[geometry] +site_model_file = site_model_lt.xml + +[logic_tree] +number_of_logic_tree_samples = 4 + +[erf] +rupture_mesh_spacing = 2.0 +width_of_mfd_bin = 0.2 +area_source_discretization = 20.0 + +[site_params] +reference_vs30_type = measured +reference_vs30_value = 760.0 +reference_depth_to_2pt5km_per_sec = 5.0 +reference_depth_to_1pt0km_per_sec = 100.0 + +[calculation] +source_model_logic_tree_file = source_model_logic_tree.xml +gsim_logic_tree_file = gsim_logic_tree.xml +investigation_time = 50.0 +intensity_measure_types_and_levels = {"PGA": [0.05, 0.1, 0.2, 0.4, 0.8]} +truncation_level = 3 +maximum_distance = 200.0 + +[disaggregation] +poes_disagg = 0.1 +mag_bin_width = 0.5 +distance_bin_width = 25.0 +coordinate_bin_width = 1.0 +num_epsilon_bins = 3 diff --git a/openquake/qa_tests_data/logictree/case_24/job_fastmean.ini b/openquake/qa_tests_data/logictree/case_24/job_fastmean.ini new file mode 100644 index 000000000000..9f9fc2d7c42d --- /dev/null +++ b/openquake/qa_tests_data/logictree/case_24/job_fastmean.ini @@ -0,0 +1,34 @@ +[general] +description = Site model logic tree - full enumeration - fastmean path (mean only,use_rates is True and no individual rlz) +calculation_mode = classical +random_seed = 24 + +[geometry] +site_model_file = site_model_lt.xml + +[logic_tree] +number_of_logic_tree_samples = 0 + +[erf] +rupture_mesh_spacing = 2.0 +width_of_mfd_bin = 0.2 +area_source_discretization = 20.0 + +[site_params] +reference_vs30_type = measured +reference_vs30_value = 760.0 +reference_depth_to_2pt5km_per_sec = 5.0 +reference_depth_to_1pt0km_per_sec = 100.0 + +[calculation] +source_model_logic_tree_file = source_model_logic_tree.xml +gsim_logic_tree_file = gsim_logic_tree.xml +investigation_time = 50.0 +intensity_measure_types_and_levels = {"PGA": [0.05, 0.1, 0.2, 0.4, 0.8]} +truncation_level = 3 +maximum_distance = 200.0 +use_rates = true + +[output] +mean_hazard_curves = true +individual_rlzs = false diff --git a/openquake/qa_tests_data/logictree/case_24/job_fastmean_sampling.ini b/openquake/qa_tests_data/logictree/case_24/job_fastmean_sampling.ini new file mode 100644 index 000000000000..0e86eae90faf --- /dev/null +++ b/openquake/qa_tests_data/logictree/case_24/job_fastmean_sampling.ini @@ -0,0 +1,34 @@ +[general] +description = Site model logic tree - sampling - fastmean path (mean only, use_rates is True and no individual rlz) +calculation_mode = classical +random_seed = 24 + +[geometry] +site_model_file = site_model_lt.xml + +[logic_tree] +number_of_logic_tree_samples = 4 + +[erf] +rupture_mesh_spacing = 2.0 +width_of_mfd_bin = 0.2 +area_source_discretization = 20.0 + +[site_params] +reference_vs30_type = measured +reference_vs30_value = 760.0 +reference_depth_to_2pt5km_per_sec = 5.0 +reference_depth_to_1pt0km_per_sec = 100.0 + +[calculation] +source_model_logic_tree_file = source_model_logic_tree.xml +gsim_logic_tree_file = gsim_logic_tree.xml +investigation_time = 50.0 +intensity_measure_types_and_levels = {"PGA": [0.05, 0.1, 0.2, 0.4, 0.8]} +truncation_level = 3 +maximum_distance = 200.0 +use_rates = true + +[output] +mean_hazard_curves = true +individual_rlzs = false diff --git a/openquake/qa_tests_data/logictree/case_24/job_sampling.ini b/openquake/qa_tests_data/logictree/case_24/job_sampling.ini new file mode 100644 index 000000000000..e899d0fbd29a --- /dev/null +++ b/openquake/qa_tests_data/logictree/case_24/job_sampling.ini @@ -0,0 +1,32 @@ +[general] +description = Site model logic tree - sampling +calculation_mode = classical +random_seed = 24 + +[geometry] +site_model_file = site_model_lt.xml + +[logic_tree] +number_of_logic_tree_samples = 4 + +[erf] +rupture_mesh_spacing = 2.0 +width_of_mfd_bin = 0.2 +area_source_discretization = 20.0 + +[site_params] +reference_vs30_type = measured +reference_vs30_value = 760.0 +reference_depth_to_2pt5km_per_sec = 5.0 +reference_depth_to_1pt0km_per_sec = 100.0 + +[calculation] +source_model_logic_tree_file = source_model_logic_tree.xml +gsim_logic_tree_file = gsim_logic_tree.xml +investigation_time = 50.0 +intensity_measure_types_and_levels = {"PGA": [0.05, 0.1, 0.2, 0.4, 0.8]} +truncation_level = 3 +maximum_distance = 200.0 + +[output] +individual_rlzs = true diff --git a/openquake/qa_tests_data/logictree/case_24/job_xml_branches.ini b/openquake/qa_tests_data/logictree/case_24/job_xml_branches.ini new file mode 100644 index 000000000000..c236aeea943e --- /dev/null +++ b/openquake/qa_tests_data/logictree/case_24/job_xml_branches.ini @@ -0,0 +1,33 @@ +[general] +description = Site model logic tree - XML branches +calculation_mode = classical +random_seed = 24 + +[geometry] +site_model_file = site_model_lt_xml.xml + +[logic_tree] +number_of_logic_tree_samples = 0 + +[erf] +rupture_mesh_spacing = 2.0 +width_of_mfd_bin = 0.2 +area_source_discretization = 20.0 + +[site_params] +reference_vs30_type = measured +reference_vs30_value = 760.0 +reference_depth_to_2pt5km_per_sec = 5.0 +reference_depth_to_1pt0km_per_sec = 100.0 + +[calculation] +source_model_logic_tree_file = source_model_logic_tree.xml +gsim_logic_tree_file = gsim_logic_tree.xml +investigation_time = 50.0 +intensity_measure_types_and_levels = {"PGA": [0.05, 0.1, 0.2, 0.4, 0.8]} +truncation_level = 3 +maximum_distance = 200.0 + +[output] +mean_hazard_curves = true +individual_rlzs = true diff --git a/openquake/qa_tests_data/logictree/case_24/site_model_lt.xml b/openquake/qa_tests_data/logictree/case_24/site_model_lt.xml new file mode 100644 index 000000000000..577a17164d36 --- /dev/null +++ b/openquake/qa_tests_data/logictree/case_24/site_model_lt.xml @@ -0,0 +1,17 @@ + + + + + + site_model_rock.csv + 0.6 + + + site_model_soil.csv + 0.4 + + + + diff --git a/openquake/qa_tests_data/logictree/case_24/site_model_lt_xml.xml b/openquake/qa_tests_data/logictree/case_24/site_model_lt_xml.xml new file mode 100644 index 000000000000..e98db33b260c --- /dev/null +++ b/openquake/qa_tests_data/logictree/case_24/site_model_lt_xml.xml @@ -0,0 +1,17 @@ + + + + + + site_model_rock.xml + 0.6 + + + site_model_soil.xml + 0.4 + + + + diff --git a/openquake/qa_tests_data/logictree/case_24/site_model_rock.csv b/openquake/qa_tests_data/logictree/case_24/site_model_rock.csv new file mode 100644 index 000000000000..8009813da949 --- /dev/null +++ b/openquake/qa_tests_data/logictree/case_24/site_model_rock.csv @@ -0,0 +1,3 @@ +lon,lat,vs30,z1pt0,z2pt5 +-65.0,0.0,760.0,20.0,1.0 +-65.05,0.05,760.0,20.0,1.0 diff --git a/openquake/qa_tests_data/logictree/case_24/site_model_rock.xml b/openquake/qa_tests_data/logictree/case_24/site_model_rock.xml new file mode 100644 index 000000000000..7671a9abe54c --- /dev/null +++ b/openquake/qa_tests_data/logictree/case_24/site_model_rock.xml @@ -0,0 +1,10 @@ + + + + + + + diff --git a/openquake/qa_tests_data/logictree/case_24/site_model_soil.csv b/openquake/qa_tests_data/logictree/case_24/site_model_soil.csv new file mode 100644 index 000000000000..a561be6f2a9c --- /dev/null +++ b/openquake/qa_tests_data/logictree/case_24/site_model_soil.csv @@ -0,0 +1,3 @@ +lon,lat,vs30,z1pt0,z2pt5 +-65.0,0.0,360.0,150.0,3.0 +-65.05,0.05,360.0,150.0,3.0 diff --git a/openquake/qa_tests_data/logictree/case_24/site_model_soil.xml b/openquake/qa_tests_data/logictree/case_24/site_model_soil.xml new file mode 100644 index 000000000000..02ef39efa40e --- /dev/null +++ b/openquake/qa_tests_data/logictree/case_24/site_model_soil.xml @@ -0,0 +1,10 @@ + + + + + + + diff --git a/openquake/qa_tests_data/logictree/case_24/source_model_a.xml b/openquake/qa_tests_data/logictree/case_24/source_model_a.xml new file mode 100644 index 000000000000..ada9e7994f63 --- /dev/null +++ b/openquake/qa_tests_data/logictree/case_24/source_model_a.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + -65.5 -0.5 -65.5 0.5 -64.5 0.5 -64.5 -0.5 + + + + + 0.0 + 15.0 + + WC1994 + 1.0 + + + + + + + + + + + diff --git a/openquake/qa_tests_data/logictree/case_24/source_model_b.xml b/openquake/qa_tests_data/logictree/case_24/source_model_b.xml new file mode 100644 index 000000000000..7b0537158c91 --- /dev/null +++ b/openquake/qa_tests_data/logictree/case_24/source_model_b.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + -65.5 -0.5 -65.5 0.5 -64.5 0.5 -64.5 -0.5 + + + + + 0.0 + 15.0 + + WC1994 + 1.0 + + + + + + + + + + + diff --git a/openquake/qa_tests_data/logictree/case_24/source_model_logic_tree.xml b/openquake/qa_tests_data/logictree/case_24/source_model_logic_tree.xml new file mode 100644 index 000000000000..c1772e32c3c9 --- /dev/null +++ b/openquake/qa_tests_data/logictree/case_24/source_model_logic_tree.xml @@ -0,0 +1,17 @@ + + + + + + source_model_a.xml + 0.7 + + + source_model_b.xml + 0.3 + + + +