Пример #1
0
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}))
Пример #2
0
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=',')