def sample_ruptures(sources, cmaker, sitecol=None, monitor=Monitor()): """ :param sources: a sequence of sources of the same group :param cmaker: a ContextMaker instance with ses_per_logic_tree_path, ses_seed :param sitecol: SiteCollection instance used for filtering (None for no filtering) :param monitor: monitor instance :yields: dictionaries with keys rup_array, calc_times """ srcfilter = SourceFilter(sitecol, cmaker.maximum_distance) # AccumDict of arrays with 3 elements num_ruptures, num_sites, calc_time calc_times = AccumDict(accum=numpy.zeros(3, numpy.float32)) # Compute and save stochastic event sets num_ses = cmaker.ses_per_logic_tree_path grp_id = sources[0].grp_id # Compute the number of occurrences of the source group. This is used # for cluster groups or groups with mutually exclusive sources. if (getattr(sources, 'atomic', False) and getattr(sources, 'cluster', False)): eb_ruptures, calc_times = sample_cluster( sources, srcfilter, num_ses, vars(cmaker)) # Yield ruptures er = sum(src.num_ruptures for src, _ in srcfilter.filter(sources)) yield AccumDict(dict(rup_array=get_rup_array(eb_ruptures, srcfilter), calc_times=calc_times, eff_ruptures={grp_id: er})) else: eb_ruptures = [] eff_ruptures = 0 # AccumDict of arrays with 2 elements weight, calc_time calc_times = AccumDict(accum=numpy.zeros(3, numpy.float32)) for src, _ in srcfilter.filter(sources): nr = src.num_ruptures eff_ruptures += nr t0 = time.time() if len(eb_ruptures) > MAX_RUPTURES: # yield partial result to avoid running out of memory yield AccumDict(dict(rup_array=get_rup_array(eb_ruptures, srcfilter), calc_times={}, eff_ruptures={})) eb_ruptures.clear() samples = getattr(src, 'samples', 1) for rup, trt_smr, n_occ in src.sample_ruptures( samples * num_ses, cmaker.ses_seed): ebr = EBRupture(rup, src.source_id, trt_smr, n_occ) eb_ruptures.append(ebr) dt = time.time() - t0 calc_times[src.id] += numpy.array([nr, src.nsites, dt]) rup_array = get_rup_array(eb_ruptures, srcfilter) yield AccumDict(dict(rup_array=rup_array, calc_times=calc_times, eff_ruptures={grp_id: eff_ruptures}))
class VectorValuedCalculator(): def __init__(self, oqparam, sites_col, correlation_model): self.oqparam = oqparam self.ssm_lt = get_source_model_lt(oqparam) # Read the SSC logic tree self.hc = mdhc.MultiDimensionalHazardCurve(oqparam.imtls, sites_col, correlation_model, oqparam.maximum_distance) self.ndims = len(oqparam.imtls.keys()) self.periods = get_imts(oqparam) self.sites = sites_col self.cm = correlation_model self.srcfilter = SourceFilter(sites_col, oqparam.maximum_distance) self.integration_prms = {'truncation_level': oqparam.truncation_level, 'abseps': 0.0001, # Documentation: Optimal value is 1E-6 'maxpts': self.ndims*10 # Documentation: Optimal value is len(lnSA)*1000 } self.integration_prms.update({'trunc_norm': self._truncation_normalization_factor()}) def _truncation_normalization_factor(self): """ Returns the N-D normalization factor for the normal law integration over the [ -n*std, +n*std ] domain """ mu = np.zeros((self.ndims,)) lower_trunc = -self.integration_prms['truncation_level']*np.ones_like(mu) upper_trunc = self.integration_prms['truncation_level']*np.ones_like(mu) trunc_norm, _ = mvn.mvnun(lower_trunc, upper_trunc, mu, self.hc.corr, abseps=self.integration_prms['abseps'], maxpts=self.integration_prms['maxpts']) return trunc_norm def count_pointsources(self): """ Returns the total number of point-sources involved in the current calculation """ n = 0 for rlz in self.ssm_lt: srcs = parser.get_sources_from_rlz(rlz, self.oqparam, self.ssm_lt, sourcefilter=self.srcfilter) for src in srcs: for _ in self.srcfilter.filter(src): n += 1 return n def gm_poe(self, gsim, dist_ctx, rup_ctx, site_ctx, lnSA): """ Returns the multivariate probability to exceed a specified set of ground motion acceleration levels """ nsites = len(site_ctx) lnAVG = np.zeros((nsites,self.ndims)) lnSTD = np.zeros(lnAVG.shape) for i in range(self.ndims): # gsim().get_mean_and_stddevs returns ground-motion as ln(PSA) in units of g: means, stddevs = gsim.get_mean_and_stddevs(site_ctx, rup_ctx, dist_ctx, self.periods[i], [const.StdDev.TOTAL]) lnAVG[:,i] = np.squeeze(means) lnSTD[:,i] = np.squeeze(stddevs[0]) mu = np.zeros((self.ndims,)) prob = np.zeros((nsites,)) for j in range(nsites): # Build covariance matrix: #D = np.diag(lnSTD[j,:]) #cov = D @ self.hc.corr @ D #lower_trunc = lnAVG[j,:]-self.integration_prms['truncation_level']*np.sqrt(np.diag(cov)) #upper_trunc = lnAVG[j,:]+self.integration_prms['truncation_level']*np.sqrt(np.diag(cov)) lower_trunc = -self.integration_prms['truncation_level']*np.ones_like(mu) upper_trunc = self.integration_prms['truncation_level']*np.ones_like(mu) lower = (np.array(lnSA) - lnAVG[j,:])/lnSTD[j,:] # Convert accel to epsilon if np.any(lower >= upper_trunc): # Requested value is above the 3-sigma truncature for at least one spectral period: prob[j] = 0 continue if np.any(lower <= lower_trunc): indices = (lower <= lower_trunc).nonzero() lower[indices] = -self.integration_prms['truncation_level'] prob[j], error = mvn.mvnun(lower, upper_trunc, mu, self.hc.corr, abseps=self.integration_prms['abseps'], maxpts=self.integration_prms['maxpts']) # Normalize poe over the truncation interval [-n*sigma, n*sigma] prob[j] /= self.integration_prms['trunc_norm'] return prob def pt_src_are(self, pt_src, gsim, weight, lnSA, monitor): """ Returns the vector-valued Annual Rate of Exceedance for one single point-source :param pt_src: single instance of class "openquake.hazardlib.source.area.PointSource" :param gsim: tuple, containing (only one?) instance of Openquake GSIM class :param: weight, weight to be multiplied to ARE estimate :param lnSA: list, natural logarithm of acceleration values for each spectral period. Note : Values should be ordered in the same order than self.periods """ annual_rate = 0 # Loop over ruptures: # i.e. one rupture for each combination of (mag, nodal plane, hypocentral depth): for r in pt_src.iter_ruptures(): # NOTE: IF ACCOUNTING FOR "pointsource_distance" IN THE INI FILE, ONE SHOULD USE THE # "point_ruptures()" METHOD BELOW: # Loop over ruptures, one rupture for each magnitude ( neglect floating and combination on # nodal plane and hypocentral depth): ## for r in pt_src.point_ruptures(): # Note: Seismicity rate evenly distributed over all point sources # Seismicity rate also accounts for FMD (i.e. decreasing for # increasing magnitude value) # Filter the site collection with respect to the rupture and prepare context objects: context_maker = ContextMaker(r.tectonic_region_type, gsim) site_ctx, dist_ctx = context_maker.make_contexts(self.sites, r) rup_ctx = RuptureContext() rup_ctx.mag = r.mag rup_ctx.rake = r.rake assert len(gsim)==1 annual_rate += r.occurrence_rate * weight * self.gm_poe(gsim[0], dist_ctx, rup_ctx, site_ctx, lnSA) return annual_rate def are(self, lnSA): """ Returns the vector-valued annual rate of exceedance param *lnSA: tuple, natural logarithm of acceleration values, in unit of g. """ are = 0 for rlz in self.ssm_lt: # Loop over realizations _, weight = parser.get_value_and_weight_from_rlz(rlz) srcs = parser.get_sources_from_rlz(rlz, self.oqparam, self.ssm_lt, sourcefilter=self.srcfilter) for src in srcs: # Loop over (filtered) seismic sources (area, fault, etc...) for pt in self.srcfilter.filter(src): # Loop over point-sources gsim_lt = get_gsim_lt(self.oqparam, trts=[src.tectonic_region_type]) for gsim_rlz in gsim_lt: # Loop over GSIM Logic_tree gsim_model, gsim_weight = parser.get_value_and_weight_from_gsim_rlz( gsim_rlz) pt_weight = weight * gsim_weight are += self.pt_src_are(pt, gsim_model, pt_weight, lnSA, None) return are def are_parallel(self, lnSA): """ Returns the vector-valued annual rate of exceedance param *lnSA: tuple, natural logarithm of acceleration values, in unit of g. """ args_list = list() for rlz in self.ssm_lt: # Loop over realizations _, weight = parser.get_value_and_weight_from_rlz(rlz) srcs = parser.get_sources_from_rlz(rlz, self.oqparam, self.ssm_lt, sourcefilter=self.srcfilter) for src in srcs: # Loop over (filtered) seismic sources (area, fault, etc...) for pt in self.srcfilter.filter(src): # Loop over point-sources gsim_lt = get_gsim_lt(self.oqparam, trts=[src.tectonic_region_type]) for gsim_rlz in gsim_lt: # Loop over GSIM Logic_tree gsim_model, gsim_weight = parser.get_value_and_weight_from_gsim_rlz(gsim_rlz) # Distribute ARE: pt_weight = weight*gsim_weight args = (self, pt, gsim_model, pt_weight, lnSA) args_list.append(args) are = 0 for value in Starmap(self.pt_src_are.__func__, args_list): are += value return are def poe(self, lnSA): """ Returns the vector-valued probability of exceedance param *lnSA: tuple, natural logarithm of acceleration values, in unit of g. """ are = self.are(lnSA) return 1-np.exp(-are*self.oqparam.investigation_time) def poe_parallel(self, lnSA): """ Returns the vector-valued probability of exceedance param *lnSA: tuple, natural logarithm of acceleration values, in unit of g. """ are = self.are_parallel(lnSA) return 1-np.exp(-are*self.oqparam.investigation_time) def hazard_matrix_calculation(self, quantity='poe'): """ Compute exhaustively the full VPSHA hazard matrix of ARE/POE over the N-dimensional space of spectral periods or parameters. NOTE: Parallelization occurs on the loop over seismic point sources WARNING !! This computation can be extremely expensive for high-dimensional problems ! """ # Initialization step: #hc_calc_method = getattr(self, quantity) # self.poe or self.are method hc_calc_method = getattr(self, quantity+'_parallel') # self.poe or self.are method shape = (len(self.sites),) + tuple(len(self.oqparam.imtls[str(p)]) for p in self.periods) max_nb = np.prod(shape) logging.warning('hazard matrix shape: [N_sites x N_IMT_1 x ... x N_IMT_k]: {}'.format(shape)) logging.warning('hazard matrix has {} elements'.format(max_nb)) output = np.empty(shape) acc_discretization = [np.log(self.oqparam.imtls[str(p)]) for p in self.periods] # create a N-dimensional mesh of spectral acceleration values: acc_meshes = np.meshgrid(*acc_discretization, indexing='ij', copy=False) nelts = int(np.prod(shape[1:])) # Number of N-D pseudo spectral values for i in range(nelts): indices = np.unravel_index(i, shape[1:]) # Flat to multi-dimensional index accels = [ acc_meshes[j][indices] for j in range(self.ndims)] # Call hazard curve computation method: hazard_output = hc_calc_method(accels) # Sort results for each site: for k in range(len(hazard_output)): # Loop on sites, i.e. 1st dimension of "hazard_output": output[(k,) + indices] = hazard_output[k] self.hc.hazard_matrix = output return self.hc def hazard_matrix_calculation_parallel(self, quantity='poe'): """ Compute exhaustively the full VPSHA hazard matrix of ARE/POE over the N-dimensional space of spectral periods or parameters. NOTE: Parallelization occurs on the loop over N-D hazard matrix cells WARNING !! This computation can be extremely expensive for high-dimensional problems ! """ # Initialization step: hc_calc_method = getattr(self, quantity) # self.poe or self.are method args = list() shape = (len(self.sites),) + tuple(len(self.oqparam.imtls[str(p)]) for p in self.periods) max_nb = np.prod(shape) logging.warning('hazard matrix shape: [N_sites x N_IMT_1 x ... x N_IMT_k]: {}'.format(shape)) logging.warning('hazard matrix has {} elements'.format(max_nb)) acc_discretization = [np.log(self.oqparam.imtls[str(p)]) for p in self.periods] # create a N-dimensional mesh of spectral acceleration values: acc_meshes = np.meshgrid(*acc_discretization, indexing='ij', copy=False) nelts = int(np.prod(shape[1:])) # Number of N-D pseudo spectral values for i in range(nelts): indices = np.unravel_index(i, shape[1:]) # Flat to multi-dimensional index accels = [ acc_meshes[j][indices] for j in range(self.ndims)] logging.debug(f" # Current acceleration vector: {tuple(str(p) for p in self.periods)} = {accels}\n") # Call hazard curve computation method: #hazard_output = hc_calc_method(accels) args.append((indices, hc_calc_method, accels)) """ # Sort results for each site: for k in range(len(hazard_output)): # Loop on sites, i.e. 1st dimension of "hazard_output" # indx = np.ravel_multi_index((k,)+indices, shape) # indices = np.unravel_index(indx, shape) output[(k,) + indices] = hazard_output[k] """ output = np.empty(shape) for result in Starmap(_matrix_cell_worker, args): # Sort results for each site: for k in range(len(result['output'])): # Loop on sites, i.e. 1st dimension of "hazard_output" # indx = np.ravel_multi_index((k,)+indices, shape) # indices = np.unravel_index(indx, shape) output[(k,) + result['indices']] = result['output'][k] self.hc.hazard_matrix = output return self.hc def find_matching_poe_parallel_runs(self, target, quantity='poe', tol=None, nsol=1, outputfile=None): """ Returns a list of vector-valued coordinates corresponding to the Multi-Dimensional Hazard Curve ARE/POE value TARGET (within tolerance interval +/- TOL). This list of coordinates is obtained using an optimization algorithm. Parallelization is realized by sending one individual optimization run on each worker. :return: Coordinate of vector-sample with matching QUANTITY=TARGET """ # TOL: Tolerance on cost-function evaluation w/r to TARGET: if tol is None: tol = target/1E3 lower_bound = [np.log(min(self.oqparam.imtls[str(p)])) for p in self.periods] upper_bound = [np.log(max(self.oqparam.imtls[str(p)])) for p in self.periods] coord = np.empty( (nsol, 3+len(self.periods)) ) # coord[i,:] = [ ARE_OR_POE, N_ITER, N_FEV, SA_1, ..., SA_N] worker_args = list() for i in range(nsol): rs = np.random.RandomState(seed=np.random.random_integers(0,1E9)) worker_args.append((getattr(self, quantity), target, lower_bound, upper_bound, tol, rs)) i = 0 for res in Starmap(_root_finder_worker, worker_args): logging.info('Starting point: {}'.format(res.x0)) logging.info('{}/{}: Convergence met for sample {} ({}={})'.format( i+1,nsol,np.exp(res.x),quantity,res.fun+target)) coord[i, 0] = res.fun+target # Evaluate ARE/POE at solution coord[i, 1] = res.nit coord[i, 2] = res.nfev coord[i, 3:] = np.exp(res.x) # Convert lnSA to SA in units of g i = i + 1 with open(outputfile, 'ab') as f: np.savetxt(f, coord, fmt='%.6e', delimiter=',') def find_matching_poe(self, target, quantity='poe', tol=None, nsol=1, outputfile=None): """ Returns a list of vector-valued coordinates corresponding to the Multi-Dimensional Hazard Curve ARE/POE value TARGET (within tolerance interval +/- TOL). This list of coordinates is obtained using an optimization algorithm. Parallelization is realized by distributing individual AREs over each point source. :return: Coordinate of vector-sample with matching QUANTITY=TARGET """ # TOL: Tolerance on cost-function evaluation w/r to TARGET: if tol is None: tol = target/1E3 lower_bound = [np.log(min(self.oqparam.imtls[str(p)])) for p in self.periods] upper_bound = [np.log(max(self.oqparam.imtls[str(p)])) for p in self.periods] coord = np.empty( (nsol, 3+len(self.periods)) ) # NB: coord[i,:] = [ ARE_OR_POE, N_ITER, N_FEV, SA_1, ..., SA_N] hc_calc_method = getattr(self, quantity+'_parallel') for i in range(nsol): rs = np.random.RandomState(seed=np.random.random_integers(0,1E9)) res = _root_finder_worker(hc_calc_method, target, lower_bound, upper_bound, tol, rs, None) logging.info('Starting point: {}'.format(res.x0)) logging.info('{}/{}: Convergence met for sample {} ({}={})'.format( i + 1, nsol, np.exp(res.x), quantity, res.fun + target)) coord[i, 0] = res.fun + target # Evaluate ARE/POE at solution coord[i, 1] = res.nit coord[i, 2] = res.nfev coord[i, 3:] = np.exp(res.x) # Convert lnSA to SA in units of g with open(outputfile, 'ab') as f: np.savetxt(f, coord[i,:][np.newaxis,:], fmt='%.6e', delimiter=',')