# The Hazard Library# Copyright (C) 2012-2023 GEM Foundation## This program 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.## This program 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 this program. If not, see <http://www.gnu.org/licenses/>."""Module :mod:`openquake.hazardlib.source.point` defines :class:`PointSource`."""importmathimportcopyimportnumpyfromopenquake.baselib.generalimportAccumDict,groupby_gridfromopenquake.baselib.performanceimportMonitorfromopenquake.hazardlib.geoimportPoint,geodeticfromopenquake.hazardlib.geo.nodalplaneimportNodalPlanefromopenquake.hazardlib.geo.surface.planarimport(build_planar,PlanarSurface,planin_dt)fromopenquake.hazardlib.pmfimportPMFfromopenquake.hazardlib.scalerel.pointimportPointMSRfromopenquake.hazardlib.source.baseimportParametricSeismicSourcefromopenquake.hazardlib.source.ruptureimport(ParametricProbabilisticRupture,PointRupture)fromopenquake.hazardlib.geo.utilsimportget_bounding_box,angular_distance# this is fast
[docs]defget_rupdims(areas,dip,width,rar):""" Calculate and return the rupture length and width for given magnitude surface parameters. :returns: array of shape (M, 3) with rupture lengths, widths and heights The rupture area is calculated using method :meth:`~openquake.hazardlib.scalerel.base.BaseMSR.get_median_area` of source's magnitude-scaling relationship. In any case the returned dimensions multiplication is equal to that value. Than the area is decomposed to length and width with respect to source's rupture aspect ratio. If calculated rupture width being inclined by nodal plane's dip angle would not fit in between upper and lower seismogenic depth, the rupture width is shrunken to a maximum possible and rupture length is extended to preserve the same area. """out=numpy.zeros((len(areas),3))rup_length=numpy.sqrt(areas*rar)rup_width=areas/rup_lengthrdip=math.radians(dip)max_width=width/math.sin(rdip)big=rup_width>max_widthrup_width[big]=max_widthrup_length[big]=areas[big]/rup_width[big]out[:,0]=rup_lengthout[:,1]=rup_width*math.cos(rdip)out[:,2]=rup_width*math.sin(rdip)returnout
[docs]defmsr_name(src):""" :returns: string representation of the MSR or "Undefined" if not applicable """try:returnstr(src.magnitude_scaling_relationship)exceptAttributeError:# no MSR for nonparametric sourcesreturn'Undefined'
[docs]defcalc_average(pointsources):""" :returns: a dict with average strike, dip, rake, lon, lat, dep, upper_seismogenic_depth, lower_seismogenic_depth """acc=dict(lon=[],lat=[],dep=[],strike=[],dip=[],rake=[],upper_seismogenic_depth=[],lower_seismogenic_depth=[],rupture_aspect_ratio=[])rates=[]trt=pointsources[0].tectonic_region_typemsr=msr_name(pointsources[0])forsrcinpointsources:assertsrc.tectonic_region_type==trtassertmsr_name(src)==msrrates.append(sum(rform,rinsrc.get_annual_occurrence_rates()))ws,ds=zip(*src.nodal_plane_distribution.data)strike=numpy.average([np.strikefornpinds],weights=ws)dip=numpy.average([np.dipfornpinds],weights=ws)rake=numpy.average([np.rakefornpinds],weights=ws)ws,deps=zip(*src.hypocenter_distribution.data)dep=numpy.average(deps,weights=ws)acc['lon'].append(src.location.x)acc['lat'].append(src.location.y)acc['dep'].append(dep)acc['strike'].append(strike)acc['dip'].append(dip)acc['rake'].append(rake)acc['upper_seismogenic_depth'].append(src.upper_seismogenic_depth)acc['lower_seismogenic_depth'].append(src.lower_seismogenic_depth)acc['rupture_aspect_ratio'].append(src.rupture_aspect_ratio)dic={key:numpy.average(acc[key],weights=rates)forkeyinacc}dic['lon']=numpy.round(dic['lon'],6)dic['lat']=numpy.round(dic['lat'],6)returndic
[docs]classPointSource(ParametricSeismicSource):""" Point source typology represents seismicity on a single geographical location. :param upper_seismogenic_depth: Minimum depth an earthquake rupture can reach, in km. :param lower_seismogenic_depth: Maximum depth an earthquake rupture can reach, in km. :param location: :class:`~openquake.hazardlib.geo.point.Point` object representing the location of the seismic source. :param nodal_plane_distribution: :class:`~openquake.hazardlib.pmf.PMF` object with values that are instances of :class:`openquake.hazardlib.geo.nodalplane.NodalPlane`. Shows the distribution of probability for rupture to have the certain nodal plane. :param hypocenter_distribution: :class:`~openquake.hazardlib.pmf.PMF` with values being float numbers in km representing the depth of the hypocenter. Latitude and longitude of the hypocenter is always set to ones of ``location``. See also :class:`openquake.hazardlib.source.base.ParametricSeismicSource` for description of other parameters. :raises ValueError: If upper seismogenic depth is below lower seismogenic depth, if one or more of hypocenter depth values is shallower than upper seismogenic depth or deeper than lower seismogenic depth. """code=b'P'MODIFICATIONS=set()ps_grid_spacing=0# updated in CollapsedPointSourcedef__init__(self,source_id,name,tectonic_region_type,mfd,rupture_mesh_spacing,magnitude_scaling_relationship,rupture_aspect_ratio,temporal_occurrence_model,# point-specific parametersupper_seismogenic_depth,lower_seismogenic_depth,location,nodal_plane_distribution,hypocenter_distribution):super().__init__(source_id,name,tectonic_region_type,mfd,rupture_mesh_spacing,magnitude_scaling_relationship,rupture_aspect_ratio,temporal_occurrence_model)ifnotlower_seismogenic_depth>upper_seismogenic_depth:raiseValueError('lower seismogenic depth must be below ''upper seismogenic depth')ifnotall(upper_seismogenic_depth<=depth<=lower_seismogenic_depthfor(prob,depth)inhypocenter_distribution.data):raiseValueError('depths of all hypocenters must be in between ''lower and upper seismogenic depths')ifnotupper_seismogenic_depth>geodetic.EARTH_ELEVATION:raiseValueError("Upper seismogenic depth must be greater than the ""maximum elevation on Earth's surface (-8.848 km)")self.location=locationself.nodal_plane_distribution=nodal_plane_distributionself.hypocenter_distribution=hypocenter_distributionself.upper_seismogenic_depth=upper_seismogenic_depthself.lower_seismogenic_depth=lower_seismogenic_depth
[docs]defrestrict(self,nodalplane,depth):""" :returns: source restricted to a single nodal plane and depth """new=copy.copy(self)new.nodal_plane_distribution=PMF([(1.,nodalplane)])new.hypocenter_distribution=PMF([(1.,depth)])returnnew
[docs]defget_planin(self,magd,npd):""" :return: array of dtype planin_dt of shape (#mags, #planes, #depths) """msr=self.magnitude_scaling_relationshipwidth=self.lower_seismogenic_depth-self.upper_seismogenic_depthrar=self.rupture_aspect_ratioplanin=numpy.zeros((len(magd),len(npd)),planin_dt).view(numpy.recarray)mrate,mags=numpy.array(magd).T# shape (2, M)nrate=numpy.array([nratefornrate,npinnpd])planin['rate']=mrate[:,None]*nrateforn,(nrate,np)inenumerate(npd):arr=planin[:,n]areas=msr.get_median_area(mags,np.rake)arr['mag']=magsarr['strike']=np.strikearr['dip']=np.diparr['rake']=np.rakearr['dims']=get_rupdims(areas,np.dip,width,rar)returnplanin
[docs]defmax_radius(self,maxdist):""" :returns: max radius + ps_grid_spacing * sqrt(2)/2 """self._get_max_rupture_projection_radius()eff_radius=min(self.radius[-1],maxdist/2)returneff_radius+self.ps_grid_spacing*.707
[docs]defget_psdist(self,m,mag,psdist,magdist):""" :returns: the effective pointsource distance for the given magnitude """eff_radius=min(self.radius[m],magdist[mag]/2)returneff_radius+self.ps_grid_spacing*.707+psdist
def_get_max_rupture_projection_radius(self):""" Find a maximum radius of a circle on Earth surface enveloping a rupture produced by this source. :returns: Half of maximum rupture's diagonal surface projection. """ifhasattr(self,'radius'):returnself.radius[-1]# max radiusifisinstance(self.magnitude_scaling_relationship,PointMSR):M=len(self.get_annual_occurrence_rates())self.radius=numpy.zeros(M)returnself.radius[-1]magd=[(r,mag)formag,rinself.get_annual_occurrence_rates()]npd=self.nodal_plane_distribution.dataself.radius=numpy.zeros(len(magd))form,planininenumerate(self.get_planin(magd,npd)):rup_length,rup_width,_=planin.dims.max(axis=0)# (N, 3) => 3# the projection radius is half of the rupture diagonalself.radius[m]=math.sqrt(rup_length**2+rup_width**2)/2.0returnself.radius[-1]# max radius
[docs]defget_planar(self,shift_hypo=False,iruptures=False):""" :returns: a dictionary mag -> list of arrays of shape (U, 3) """magd=[(r,mag)formag,rinself.get_annual_occurrence_rates()]ifisinstance(self,CollapsedPointSource)andnotiruptures:out=AccumDict(accum=[])forsrcinself.pointsources:out+=src.get_planar(shift_hypo)returnoutnpd=self.nodal_plane_distribution.datahdd=numpy.array(self.hypocenter_distribution.data)clon,clat=self.location.x,self.location.yusd=self.upper_seismogenic_depthlsd=self.lower_seismogenic_depthplanin=self.get_planin(magd,npd)planar=build_planar(planin,hdd,clon,clat,usd,lsd)# MND3ifnotshift_hypo:# use the original hypocenterplanar.hypo[:,:,:,0]=clonplanar.hypo[:,:,:,1]=clatford,(drate,dep)inenumerate(hdd):planar.hypo[:,:,d,2]=depdic={mag:[pla.reshape(-1,3)]for(_rate,mag),plainzip(magd,planar)}returndic
def_gen_ruptures(self,shift_hypo=False,step=1,iruptures=False):magd=[(r,mag)formag,rinself.get_annual_occurrence_rates()]npd=self.nodal_plane_distribution.datahdd=self.hypocenter_distribution.dataclon,clat=self.location.x,self.location.yifstep==1:# return full ruptures (one per magnitude)planardict=self.get_planar(shift_hypo,iruptures)formag,[planar]inplanardict.items():forplainplanar.reshape(-1,3):surface=PlanarSurface.from_(pla)strike,dip,rake=pla.sdrrate=pla.wlr[2]yieldParametricProbabilisticRupture(mag,rake,self.tectonic_region_type,Point(*pla.hypo),surface,rate,self.temporal_occurrence_model)else:# return point ruptures (fast)magd_=list(enumerate(magd))npd_=list(enumerate(npd))hdd_=list(enumerate(hdd))form,(mrate,mag)inmagd_[::step]:forn,(nrate,np)innpd_[::step]:ford,(drate,cdep)inhdd_[::step]:rate=mrate*nrate*drateyieldPointRupture(mag,np.rake,self.tectonic_region_type,Point(clon,clat,cdep),np.strike,np.dip,rate,self.temporal_occurrence_model,self.lower_seismogenic_depth)
[docs]defiter_ruptures(self,**kwargs):""" Generate one rupture for each combination of magnitude, nodal plane and hypocenter depth. """returnself._gen_ruptures(shift_hypo=kwargs.get('shift_hypo'),step=kwargs.get('step',1))
# PointSource
[docs]defiruptures(self):""" Generate one rupture for each magnitude, called only if nphc > 1 """avg=calc_average([self])# over nodal planes and hypocentersnp=NodalPlane(avg['strike'],avg['dip'],avg['rake'])yield fromself.restrict(np,avg['dep'])._gen_ruptures(iruptures=True)
[docs]defcount_nphc(self):""" :returns: the number of nodal planes times the number of hypocenters """returnlen(self.nodal_plane_distribution.data)*len(self.hypocenter_distribution.data)
[docs]defcount_ruptures(self):""" See :meth: `openquake.hazardlib.source.base.BaseSeismicSource.count_ruptures`. """returnlen(self.get_annual_occurrence_rates())*self.count_nphc()
@propertydefpolygon(self):""" Polygon corresponding to the max_rupture_projection_radius """radius=self._get_max_rupture_projection_radius()returnself.location.to_polygon(radius)
[docs]defget_bounding_box(self,maxdist):""" Bounding box of the point, enlarged by the maximum distance """radius=self.max_radius(maxdist)returnget_bounding_box([self.location],maxdist+radius)
[docs]defwkt(self):""" :returns: the geometry as a WKT string """loc=self.locationreturn'POINT(%s%s)'%(loc.x,loc.y)
[docs]classCollapsedPointSource(PointSource):""" Source typology representing a cluster of point sources around a specific location. The underlying sources must all have the same tectonic region type, magnitude_scaling_relationship and temporal_occurrence_model. """code=b'p'MODIFICATIONS=set()def__init__(self,source_id,pointsources):self.source_id=source_idself.pointsources=pointsourcesself.tectonic_region_type=pointsources[0].tectonic_region_typeself.magnitude_scaling_relationship=(pointsources[0].magnitude_scaling_relationship)self.temporal_occurrence_model=(pointsources[0].temporal_occurrence_model)vars(self).update(calc_average(pointsources))self.location=Point(self.lon,self.lat,self.dep)self.nodal_plane_distribution=PMF([(1.,NodalPlane(self.strike,self.dip,self.rake))])self.hypocenter_distribution=PMF([(1.,self.dep)])
[docs]defget_annual_occurrence_rates(self):""" :returns: a list of pairs [(mag, mag_occur_rate), ...] """acc=AccumDict(accum=0)forpsourceinself.pointsources:acc+=dict(psource.get_annual_occurrence_rates())returnsorted(acc.items())
[docs]defcount_nphc(self):""" :returns: the total number of nodal planes and hypocenters """returnsum(src.count_nphc()forsrcinself.pointsources)
[docs]defiter_ruptures(self,**kwargs):""" :returns: an iterator over the underlying ruptures """step=kwargs.get('step',1)forsrcinself.pointsources[::step]:yield fromsrc.iter_ruptures(**kwargs)
# CollapsedPointSource
[docs]defiruptures(self):""" :yields: the underlying ruptures with mean nodal plane and hypocenter """np=NodalPlane(self.strike,self.dip,self.rake)yield fromself.restrict(np,self.location.z)._gen_ruptures(iruptures=True)
[docs]defcount_ruptures(self):""" :returns: the total number of underlying ruptures """returnsum(src.count_ruptures()forsrcinself.pointsources)
[docs]defgrid_point_sources(sources,ps_grid_spacing,msr,cnt=0,monitor=Monitor()):""" :param sources: a list of sources with the same grp_id (point sources and not) :param ps_grid_spacing: value of the point source grid spacing in km; if None, do nothing :param msr: magnitude scaling relationship as a string :param cnt: a counter starting from 0 used to produce distinct source IDs :returns: a dict grp_id -> list of non-point sources and collapsed point sources """grp_id=sources[0].grp_idforsrcinsources[1:]:assertsrc.grp_id==grp_id,(src.grp_id,grp_id)ifnotps_grid_spacing:return{grp_id:sources,'cnt':cnt}out=[srcforsrcinsourcesifnothasattr(src,'location')]ps=numpy.array([srcforsrcinsourcesifhasattr(src,'location')])iflen(ps)<2:# nothing to collapsereturn{grp_id:out+list(ps),'cnt':cnt}coords=numpy.zeros((len(ps),3))forp,psourceinenumerate(ps):coords[p,0]=psource.location.xcoords[p,1]=psource.location.ycoords[p,2]=psource.location.zif(len(numpy.unique(coords[:,0]))==1orlen(numpy.unique(coords[:,1]))==1):# degenerated rectangle, there is no grid, do not collapsereturn{grp_id:out+list(ps),'cnt':cnt}deltax=angular_distance(ps_grid_spacing,lat=coords[:,1].mean())deltay=angular_distance(ps_grid_spacing)grid=groupby_grid(coords[:,0],coords[:,1],deltax,deltay)task_no=getattr(monitor,'task_no',0)foridxsingrid.values():iflen(idxs)>1:cnt+=1name='cps-%03d-%04d'%(task_no,cnt)cps=CollapsedPointSource(name,ps[idxs])cps.grp_id=ps[0].grp_idcps.trt_smr=ps[0].trt_smrcps.ps_grid_spacing=ps_grid_spacingout.append(cps)else:# there is a single sourceout.append(ps[idxs[0]])return{grp_id:out,'cnt':cnt}