# Licensed under a 3-clause BSD style license - see LICENSE.rst"""Spatial models."""importloggingimportosimportnumpyasnpimportscipy.integrateimportscipy.specialimportastropy.unitsasufromastropy.coordinatesimportAngle,SkyCoordfromastropy.coordinates.angle_utilitiesimportangular_separation,position_anglefromastropy.utilsimportlazypropertyfromregionsimport(CircleAnnulusSkyRegion,CircleSkyRegion,EllipseSkyRegion,PointSkyRegion,RectangleSkyRegion,)importmatplotlib.pyplotaspltfromgammapy.mapsimportMap,WcsGeomfromgammapy.modelingimportParameterfromgammapy.modeling.covarianceimportcopy_covariancefromgammapy.utils.gaussimportGauss2DPDFfromgammapy.utils.scriptsimportmake_pathfrom.coreimportModelBase__all__=["ConstantFluxSpatialModel","ConstantSpatialModel","DiskSpatialModel","GaussianSpatialModel","GeneralizedGaussianSpatialModel","PointSpatialModel","Shell2SpatialModel","ShellSpatialModel","SpatialModel","TemplateSpatialModel",]log=logging.getLogger(__name__)MAX_OVERSAMPLING=200defcompute_sigma_eff(lon_0,lat_0,lon,lat,phi,major_axis,e):"""Effective radius, used for the evaluation of elongated models"""phi_0=position_angle(lon_0,lat_0,lon,lat)d_phi=phi-phi_0minor_axis=Angle(major_axis*np.sqrt(1-e**2))a2=(major_axis*np.sin(d_phi))**2b2=(minor_axis*np.cos(d_phi))**2denominator=np.sqrt(a2+b2)sigma_eff=major_axis*minor_axis/denominatorreturnminor_axis,sigma_eff
[docs]classSpatialModel(ModelBase):"""Spatial model base class."""_type="spatial"def__init__(self,**kwargs):frame=kwargs.pop("frame","icrs")super().__init__(**kwargs)ifnothasattr(self,"frame"):self.frame=frame
[docs]def__call__(self,lon,lat,energy=None):"""Call evaluate method"""kwargs={par.name:par.quantityforparinself.parameters}ifenergyisNoneandself.is_energy_dependent:raiseValueError("Missing energy value for evaluation")ifenergyisnotNone:kwargs["energy"]=energyreturnself.evaluate(lon,lat,**kwargs)
@propertydefevaluation_bin_size_min(self):returnNone# TODO: make this a hard-coded class attribute?@lazypropertydefis_energy_dependent(self):varnames=self.evaluate.__code__.co_varnamesreturn"energy"invarnames@propertydefposition(self):"""Spatial model center position (`~astropy.coordinates.SkyCoord`)"""lon=self.lon_0.quantitylat=self.lat_0.quantityreturnSkyCoord(lon,lat,frame=self.frame)@position.setterdefposition(self,skycoord):"""Spatial model center position"""coord=skycoord.transform_to(self.frame)self.lon_0.quantity=coord.data.lonself.lat_0.quantity=coord.data.lat@propertydefposition_lonlat(self):"""Spatial model center position `(lon, lat)` in rad and frame of the model"""lon=self.lon_0.quantity.to_value(u.rad)lat=self.lat_0.quantity.to_value(u.rad)returnlon,lat# TODO: get rid of this!_phi_0=0.0@propertydefphi_0(self):returnself._phi_0@phi_0.setterdefphi_0(self,phi_0=0.0):self._phi_0=phi_0@propertydefposition_error(self):"""Get 95% containment position error as (`~regions.EllipseSkyRegion`)"""ifself.covarianceisNone:raiseValueError("No position error information available.")pars=self.parameterssub_covar=self.covariance.get_subcovariance(["lon_0","lat_0"]).data.copy()cos_lat=np.cos(self.lat_0.quantity.to_value("rad"))sub_covar[0,0]*=cos_lat**2.0sub_covar[0,1]*=cos_latsub_covar[1,0]*=cos_lateig_vals,eig_vecs=np.linalg.eig(sub_covar)lon_err,lat_err=np.sqrt(eig_vals)y_vec=eig_vecs[:,0]phi=(np.arctan2(y_vec[1],y_vec[0])*u.rad).to("deg")+self.phi_0err=np.sort([lon_err,lat_err])scale_r95=Gauss2DPDF(sigma=1).containment_radius(0.95)err*=scale_r95iferr[1]==lon_err*scale_r95:phi+=90*u.degheight=2*err[1]*pars["lon_0"].unitwidth=2*err[0]*pars["lat_0"].unitelse:height=2*err[1]*pars["lat_0"].unitwidth=2*err[0]*pars["lon_0"].unitreturnEllipseSkyRegion(center=self.position,height=height,width=width,angle=phi)
[docs]defevaluate_geom(self,geom):"""Evaluate model on `~gammapy.maps.Geom` Parameters ---------- geom : `~gammapy.maps.WcsGeom` Returns ------- `~gammapy.maps.Map` """coords=geom.get_coord(frame=self.frame,sparse=True)ifself.is_energy_dependent:returnself(coords.lon,coords.lat,energy=coords["energy_true"])else:returnself(coords.lon,coords.lat)
[docs]defintegrate_geom(self,geom,oversampling_factor=None):"""Integrate model on `~gammapy.maps.Geom` or `~gammapy.maps.RegionGeom`. Integration is performed by simple rectangle approximation, the pixel center model value is multiplied by the pixel solid angle. An oversampling factor can be used for precision. By default, this parameter is set to None and an oversampling factor is automatically estimated based on the model estimation maximal bin width. For a RegionGeom, the model is integrated on a tangent WCS projection in the region. Parameters ---------- geom : `~gammapy.maps.WcsGeom` or `~gammapy.maps.RegionGeom` The geom on which the integration is performed oversampling_factor : int or None The oversampling factor to use for integration. Default is None: the factor is estimated from the model minimimal bin size Returns ------- `~gammapy.maps.Map` or `gammapy.maps.RegionNDMap`, containing the integral value in each spatial bin. """wcs_geom=geommask=Noneifgeom.is_region:wcs_geom=geom.to_wcs_geom().to_image()result=Map.from_geom(geom=wcs_geom)pix_scale=np.max(wcs_geom.pixel_scales.to_value("deg"))ifoversampling_factorisNone:ifself.evaluation_bin_size_minisnotNone:res_scale=self.evaluation_bin_size_min.to_value("deg")ifres_scale>0:oversampling_factor=np.minimum(int(np.ceil(pix_scale/res_scale)),MAX_OVERSAMPLING)else:oversampling_factor=MAX_OVERSAMPLINGelse:oversampling_factor=1ifoversampling_factor>1:ifself.evaluation_radiusisnotNone:# Is it still needed?width=2*np.maximum(self.evaluation_radius.to_value("deg"),pix_scale)wcs_geom=wcs_geom.cutout(self.position,width)upsampled_geom=wcs_geom.upsample(oversampling_factor,axis_name=None)# assume the upsampled solid angles are approximately factor**2 smallervalues=self.evaluate_geom(upsampled_geom)/oversampling_factor**2upsampled=Map.from_geom(upsampled_geom,unit=values.unit)upsampled+=valuesifgeom.is_region:mask=geom.contains(upsampled_geom.get_coord()).astype("int")integrated=upsampled.downsample(oversampling_factor,preserve_counts=True,weights=mask)# Finally stack resultresult._unit=integrated.unitresult.stack(integrated)else:values=self.evaluate_geom(wcs_geom)result._unit=values.unitresult+=valuesresult*=result.geom.solid_angle()ifgeom.is_region:mask=result.geom.region_mask([geom.region])result=Map.from_geom(geom,data=np.sum(result.data[mask]),unit=result.unit)returnresult
[docs]defto_dict(self,full_output=False):"""Create dict for YAML serilisation"""data=super().to_dict(full_output)data["spatial"]["frame"]=self.framedata["spatial"]["parameters"]=data["spatial"].pop("parameters")returndata
def_get_plot_map(self,geom):ifself.evaluation_radiusisNoneandgeomisNone:raiseValueError(f"{self.__class__.__name__} requires geom to be defined for plotting.")ifgeomisNone:width=2*max(self.evaluation_radius,0.1*u.deg)geom=WcsGeom.create(skydir=self.position,frame=self.frame,width=width,binsz=0.02)data=self.evaluate_geom(geom)returnMap.from_geom(geom,data=data.value,unit=data.unit)
[docs]defplot(self,ax=None,geom=None,**kwargs):"""Plot spatial model. Parameters ---------- ax : `~matplotlib.axes.Axes`, optional Axis geom : `~gammapy.maps.WcsGeom`, optional Geom to use for plotting. **kwargs : dict Keyword arguments passed to `~gammapy.maps.WcsMap.plot()` Returns ------- ax : `~matplotlib.axes.Axes`, optional Axis """m=self._get_plot_map(geom)ifnotm.geom.is_flat:raiseTypeError("Use .plot_interactive() or .plot_grid() for Map dimension > 2")returnm.plot(ax=ax,**kwargs)
[docs]defplot_interative(self,ax=None,geom=None,**kwargs):"""Plot spatial model. Parameters ---------- ax : `~matplotlib.axes.Axes`, optional Axis geom : `~gammapy.maps.WcsGeom`, optional Geom to use for plotting. **kwargs : dict Keyword arguments passed to `~gammapy.maps.WcsMap.plot()` Returns ------- ax : `~matplotlib.axes.Axes`, optional Axis """m=self._get_plot_map(geom)ifm.geom.is_image:raiseTypeError("Use .plot() for 2D Maps")m.plot_interactive(ax=ax,**kwargs)
[docs]defplot_error(self,ax=None,**kwargs):"""Plot position error Parameters ---------- ax : `~matplotlib.axes.Axes`, optional Axis **kwargs : dict Keyword arguments passed to `~gammapy.maps.WcsMap.plot()` Returns ------- ax : `~matplotlib.axes.Axes`, optional Axis """# plot center positionlon,lat=self.lon_0.value,self.lat_0.valueax=plt.gca()ifaxisNoneelseaxkwargs.setdefault("marker","x")kwargs.setdefault("color","red")kwargs.setdefault("label","position")ax.scatter(lon,lat,transform=ax.get_transform(self.frame),**kwargs)# plot position errorifnotnp.all(self.covariance.data==0):region=self.position_error.to_pixel(ax.wcs)artist=region.as_artist(facecolor="none",edgecolor=kwargs["color"])ax.add_artist(artist)returnax
[docs]defplot_grid(self,geom=None,**kwargs):"""Plot spatial model energy slices in a grid. Parameters ---------- geom : `~gammapy.maps.WcsGeom`, optional Geom to use for plotting. **kwargs : dict Keyword arguments passed to `~gammapy.maps.WcsMap.plot()` Returns ------- ax : `~matplotlib.axes.Axes`, optional Axis """if(geomisNone)orgeom.is_image:raiseTypeError("Use .plot() for 2D Maps")m=self._get_plot_map(geom)m.plot_grid(**kwargs)
[docs]@classmethoddeffrom_position(cls,position,**kwargs):"""Define the position of the model using a sky coord Parameters ---------- position : `~astropy.coordinates.SkyCoord` Position Returns ------- model : `SpatialModel` Spatial model """lon_0,lat_0=position.data.lon,position.data.latreturncls(lon_0=lon_0,lat_0=lat_0,frame=position.frame,**kwargs)
[docs]classPointSpatialModel(SpatialModel):r"""Point Source. For more information see :ref:`point-spatial-model`. Parameters ---------- lon_0, lat_0 : `~astropy.coordinates.Angle` Center position frame : {"icrs", "galactic"} Center position coordinate frame """tag=["PointSpatialModel","point"]lon_0=Parameter("lon_0","0 deg")lat_0=Parameter("lat_0","0 deg",min=-90,max=90)is_energy_dependent=False@propertydefevaluation_bin_size_min(self):"""Minimal evaluation bin size (`~astropy.coordinates.Angle`)."""return0*u.deg@propertydefevaluation_radius(self):"""Evaluation radius (`~astropy.coordinates.Angle`). Set as zero degrees. """return0*u.deg@staticmethoddef_grid_weights(x,y,x0,y0):"""Compute 4-pixel weights such that centroid is preserved."""dx=np.abs(x-x0)dx=np.where(dx<1,1-dx,0)dy=np.abs(y-y0)dy=np.where(dy<1,1-dy,0)returndx*dy
[docs]classGaussianSpatialModel(SpatialModel):r"""Two-dimensional Gaussian model. For more information see :ref:`gaussian-spatial-model`. Parameters ---------- lon_0, lat_0 : `~astropy.coordinates.Angle` Center position sigma : `~astropy.coordinates.Angle` Length of the major semiaxis of the Gaussian, in angular units. e : `float` Eccentricity of the Gaussian (:math:`0< e< 1`). phi : `~astropy.coordinates.Angle` Rotation angle :math:`\phi`: of the major semiaxis. Increases counter-clockwise from the North direction. frame : {"icrs", "galactic"} Center position coordinate frame """tag=["GaussianSpatialModel","gauss"]lon_0=Parameter("lon_0","0 deg")lat_0=Parameter("lat_0","0 deg",min=-90,max=90)sigma=Parameter("sigma","1 deg",min=0)e=Parameter("e",0,min=0,max=1,frozen=True)phi=Parameter("phi","0 deg",frozen=True)@propertydefevaluation_bin_size_min(self):"""Minimal evaluation bin size (`~astropy.coordinates.Angle`) chosen as sigma/3."""returnself.parameters["sigma"].quantity/3.0@propertydefevaluation_radius(self):r"""Evaluation radius (`~astropy.coordinates.Angle`). Set as :math:`5\sigma`. """return5*self.parameters["sigma"].quantity
[docs]defto_region(self,x_sigma=1.5,**kwargs):r"""Model outline at a given number of :math:`\sigma`. Parameters ---------- x_sigma : float Number of :math:`\sigma Default is :math:`1.5\sigma` which corresponds to about 68% containment for a 2D symmetric Gaussian. Returns ------- region : `~regions.EllipseSkyRegion` Model outline. """minor_axis=Angle(self.sigma.quantity*np.sqrt(1-self.e.quantity**2))returnEllipseSkyRegion(center=self.position,height=2*x_sigma*self.sigma.quantity,width=2*x_sigma*minor_axis,angle=self.phi.quantity,**kwargs,)
@propertydefevaluation_region(self):"""Evaluation region consistent with evaluation radius"""returnself.to_region(x_sigma=5)
[docs]classGeneralizedGaussianSpatialModel(SpatialModel):r"""Two-dimensional Generealized Gaussian model. For more information see :ref:`generalized-gaussian-spatial-model`. Parameters ---------- lon_0, lat_0 : `~astropy.coordinates.Angle` Center position r_0 : `~astropy.coordinates.Angle` Length of the major semiaxis, in angular units. eta : `float` Shape parameter whitin (0, 1]. Special cases for disk: ->0, Gaussian: 0.5, Laplace:1 e : `float` Eccentricity (:math:`0< e< 1`). phi : `~astropy.coordinates.Angle` Rotation angle :math:`\phi`: of the major semiaxis. Increases counter-clockwise from the North direction. frame : {"icrs", "galactic"} Center position coordinate frame """tag=["GeneralizedGaussianSpatialModel","gauss-general"]lon_0=Parameter("lon_0","0 deg")lat_0=Parameter("lat_0","0 deg",min=-90,max=90)r_0=Parameter("r_0","1 deg")eta=Parameter("eta",0.5,min=0.01,max=1.0)e=Parameter("e",0.0,min=0.0,max=1.0,frozen=True)phi=Parameter("phi","0 deg",frozen=True)
[docs]@staticmethoddefevaluate(lon,lat,lon_0,lat_0,r_0,eta,e,phi):sep=angular_separation(lon,lat,lon_0,lat_0)ifisinstance(eta,u.Quantity):eta=eta.value# gamma function does not allow quantitiesminor_axis,r_eff=compute_sigma_eff(lon_0,lat_0,lon,lat,phi,r_0,e)z=sep/r_effnorm=1/(2*np.pi*minor_axis*r_0*eta*scipy.special.gamma(2*eta))return(norm*np.exp(-(z**(1/eta)))).to("sr-1")
@propertydefevaluation_bin_size_min(self):"""Minimal evaluation bin size (`~astropy.coordinates.Angle`). The bin min size is defined as r_0/(3+8*eta)/(e+1). """returnself.r_0.quantity/(3+8*self.eta.value)/(self.e.value+1)@propertydefevaluation_radius(self):r"""Evaluation radius (`~astropy.coordinates.Angle`). The evaluation radius is defined as r_eval = r_0*(1+8*eta) so it verifies: r_eval -> r_0 if eta -> 0 r_eval = 5*r_0 > 5*sigma_gauss = 5*r_0/sqrt(2) ~ 3.5*r_0 if eta=0.5 r_eval = 9*r_0 > 5*sigma_laplace = 5*sqrt(2)*r_0 ~ 7*r_0 if eta = 1 r_eval -> inf if eta -> inf """returnself.r_0.quantity*(1+8*self.eta.value)
[docs]defto_region(self,x_r_0=1,**kwargs):"""Model outline at a given number of r_0. Parameters ---------- x_r_0 : float Number of r_0 (Default is 1). Returns ------- region : `~regions.EllipseSkyRegion` Model outline. """minor_axis=Angle(self.r_0.quantity*np.sqrt(1-self.e.quantity**2))returnEllipseSkyRegion(center=self.position,height=2*x_r_0*self.r_0.quantity,width=2*x_r_0*minor_axis,angle=self.phi.quantity,**kwargs,)
@propertydefevaluation_region(self):"""Evaluation region consistent with evaluation radius"""scale=self.evaluation_radius/self.r_0.quantityreturnself.to_region(x_r_0=scale)
[docs]classDiskSpatialModel(SpatialModel):r"""Constant disk model. For more information see :ref:`disk-spatial-model`. Parameters ---------- lon_0, lat_0 : `~astropy.coordinates.Angle` Center position r_0 : `~astropy.coordinates.Angle` :math:`a`: length of the major semiaxis, in angular units. e : `float` Eccentricity of the ellipse (:math:`0< e< 1`). phi : `~astropy.coordinates.Angle` Rotation angle :math:`\phi`: of the major semiaxis. Increases counter-clockwise from the North direction. edge_width : float Width of the edge. The width is defined as the range within which the smooth edge of the model drops from 95% to 5% of its amplitude. It is given as fraction of r_0. frame : {"icrs", "galactic"} Center position coordinate frame """tag=["DiskSpatialModel","disk"]lon_0=Parameter("lon_0","0 deg")lat_0=Parameter("lat_0","0 deg",min=-90,max=90)r_0=Parameter("r_0","1 deg",min=0)e=Parameter("e",0,min=0,max=1,frozen=True)phi=Parameter("phi","0 deg",frozen=True)edge_width=Parameter("edge_width",value=0.01,min=0,max=1,frozen=True)@propertydefevaluation_bin_size_min(self):"""Minimal evaluation bin size (`~astropy.coordinates.Angle`). The bin min size is defined as r_0*(1-edge_width)/10. """returnself.r_0.quantity*(1-self.edge_width.quantity)/10.0@propertydefevaluation_radius(self):"""Evaluation radius (`~astropy.coordinates.Angle`). Set to the length of the semi-major axis plus the edge width. """return1.1*self.r_0.quantity*(1+self.edge_width.quantity)@staticmethoddef_evaluate_norm_factor(r_0,e):"""Compute the normalization factor."""semi_minor=r_0*np.sqrt(1-e**2)defintegral_fcn(x,a,b):A=1/np.sin(a)**2B=1/np.sin(b)**2C=A-Bcs2=np.cos(x)**2return1-np.sqrt(1-1/(B+C*cs2))return(2*scipy.integrate.quad(lambdax:integral_fcn(x,r_0,semi_minor),0,np.pi)[0])**-1@staticmethoddef_evaluate_smooth_edge(x,width):value=(x/width).to_value("")edge_width_95=2.326174307353347return0.5*(1-scipy.special.erf(value*edge_width_95))
[docs]classShellSpatialModel(SpatialModel):r"""Shell model. For more information see :ref:`shell-spatial-model`. Parameters ---------- lon_0, lat_0 : `~astropy.coordinates.Angle` Center position radius : `~astropy.coordinates.Angle` Inner radius, :math:`r_{in}` width : `~astropy.coordinates.Angle` Shell width frame : {"icrs", "galactic"} Center position coordinate frame See Also -------- Shell2SpatialModel """tag=["ShellSpatialModel","shell"]lon_0=Parameter("lon_0","0 deg")lat_0=Parameter("lat_0","0 deg",min=-90,max=90)radius=Parameter("radius","1 deg")width=Parameter("width","0.2 deg")@propertydefevaluation_bin_size_min(self):"""Minimal evaluation bin size (`~astropy.coordinates.Angle`). The bin min size is defined as the shell width. """returnself.width.quantity@propertydefevaluation_radius(self):r"""Evaluation radius (`~astropy.coordinates.Angle`). Set to :math:`r_\text{out}`. """returnself.radius.quantity+self.width.quantity
[docs]@staticmethoddefevaluate(lon,lat,lon_0,lat_0,radius,width):"""Evaluate model."""sep=angular_separation(lon,lat,lon_0,lat_0)radius_out=radius+widthnorm=3/(2*np.pi*(radius_out**3-radius**3))withnp.errstate(invalid="ignore"):# np.where and np.select do not work with quantities, so we use the# workaround with indexingvalue=np.sqrt(radius_out**2-sep**2)mask=sep<radiusvalue[mask]=(value-np.sqrt(radius**2-sep**2))[mask]value[sep>radius_out]=0returnnorm*value
[docs]classShell2SpatialModel(SpatialModel):r"""Shell model with outer radius and relative width parametrization For more information see :ref:`shell2-spatial-model`. Parameters ---------- lon_0, lat_0 : `~astropy.coordinates.Angle` Center position r_0 : `~astropy.coordinates.Angle` Outer radius, :math:`r_{out}` eta : float Shell width relative to outer radius, r_0, should be within (0,1] frame : {"icrs", "galactic"} Center position coordinate frame See Also -------- ShellSpatialModel """tag=["Shell2SpatialModel","shell2"]lon_0=Parameter("lon_0","0 deg")lat_0=Parameter("lat_0","0 deg",min=-90,max=90)r_0=Parameter("r_0","1 deg")eta=Parameter("eta",0.2,min=0.02,max=1)@propertydefevaluation_bin_size_min(self):"""Minimal evaluation bin size (`~astropy.coordinates.Angle`). The bin min size is defined as r_0*eta. """returnself.eta.value*self.r_0.quantity@propertydefevaluation_radius(self):r"""Evaluation radius (`~astropy.coordinates.Angle`). Set to :math:`r_\text{out}`. """returnself.r_0.quantity@propertydefr_in(self):return(1-self.eta.quantity)*self.r_0.quantity
[docs]@staticmethoddefevaluate(lon,lat,lon_0,lat_0,r_0,eta):"""Evaluate model."""sep=angular_separation(lon,lat,lon_0,lat_0)r_in=(1-eta)*r_0norm=3/(2*np.pi*(r_0**3-r_in**3))withnp.errstate(invalid="ignore"):# np.where and np.select do not work with quantities, so we use the# workaround with indexingvalue=np.sqrt(r_0**2-sep**2)mask=sep<r_invalue[mask]=(value-np.sqrt(r_in**2-sep**2))[mask]value[sep>r_0]=0returnnorm*value
[docs]classConstantSpatialModel(SpatialModel):"""Spatially constant (isotropic) spatial model. For more information see :ref:`constant-spatial-model`. Parameters ---------- value : `~astropy.units.Quantity` Value """tag=["ConstantSpatialModel","const"]value=Parameter("value","1 sr-1",frozen=True)frame="icrs"evaluation_radius=Noneposition=None
[docs]defto_dict(self,full_output=False):"""Create dict for YAML serilisation"""# redefined to ignore frame attribute from parent classdata=super().to_dict(full_output)data["spatial"].pop("frame")data["spatial"]["parameters"]=[]returndata
[docs]classConstantFluxSpatialModel(SpatialModel):"""Spatially constant flux spatial model. For more information see :ref:`constant-spatial-model`. """tag=["ConstantFluxSpatialModel","const-flux"]frame="icrs"evaluation_radius=Noneposition=None
[docs]defto_dict(self,full_output=False):"""Create dict for YAML serilisation"""# redefined to ignore frame attribute from parent classdata=super().to_dict(full_output)data["spatial"].pop("frame")returndata
[docs]classTemplateSpatialModel(SpatialModel):"""Spatial sky map template model. For more information see :ref:`template-spatial-model`. Parameters ---------- map : `~gammapy.maps.Map` Map template. meta : dict, optional Meta information, meta['filename'] will be used for serialization normalize : bool Normalize the input map so that it integrates to unity. interp_kwargs : dict Interpolation keyword arguments passed to `gammapy.maps.Map.interp_by_coord`. Default arguments are {'method': 'linear', 'fill_value': 0}. Filename : str Name of the map file copy_data : bool Create a deepcopy of the map data or directly use the original. True by default, can be turned to False to save memory in case of large maps. """tag=["TemplateSpatialModel","template"]def__init__(self,map,meta=None,normalize=True,interp_kwargs=None,filename=None,copy_data=True,):if(map.data<0).any():log.warning("Map has negative values. Check and fix this!")iffilenameisnotNone:filename=str(make_path(filename))self.normalize=normalizeifnormalize:# Normalize the diffuse map model so that it integrates to unityifmap.geom.is_image:data_sum=map.data.sum()else:# Normalize in each energy bindata_sum=map.data.sum(axis=(1,2)).reshape((-1,1,1))data=map.data/data_sumdata/=map.geom.solid_angle().to_value("sr")map=map.copy(data=data,unit="sr-1")ifmap.unit.is_equivalent(""):map=map.copy(data=map.data,unit="sr-1")log.warning("Missing spatial template unit, assuming sr^-1")ifcopy_data:self._map=map.copy()else:self._map=map.copy(data=map.data)self.meta={}ifmetaisNoneelsemetainterp_kwargs={}ifinterp_kwargsisNoneelseinterp_kwargsinterp_kwargs.setdefault("method","linear")interp_kwargs.setdefault("fill_value",0)self._interp_kwargs=interp_kwargsself.filename=filenamesuper().__init__()@copy_covariancedefcopy(self,copy_data=False,**kwargs):"""Copy model Parameters ---------- copy_data : bool Whether to copy the data. **kwargs : dict Keyword arguments forwarded to `TemplateSpatialModel` Returns ------- model : `TemplateSpatialModel` Copied template spatial model. """kwargs.setdefault("map",self.map)kwargs.setdefault("meta",self.meta.copy())kwargs.setdefault("normalize",self.normalize)kwargs.setdefault("interp_kwargs",self._interp_kwargs)kwargs.setdefault("filename",self.filename)returnself.__class__(copy_data=copy_data,**kwargs)@propertydefmap(self):"""Template map (`~gammapy.maps.Map`)"""returnself._map@propertydefis_energy_dependent(self):return"energy_true"inself.map.geom.axes.names@propertydefevaluation_radius(self):"""Evaluation radius (`~astropy.coordinates.Angle`). Set to half of the maximal dimension of the map. """returnnp.max(self.map.geom.width)/2.0
[docs]@classmethoddefread(cls,filename,normalize=True,**kwargs):"""Read spatial template model from FITS image. If unit is not given in the FITS header the default is ``sr-1``. Parameters ---------- filename : str FITS image filename. normalize : bool Normalize the input map so that it integrates to unity. kwargs : dict Keyword arguments passed to `Map.read()`. """m=Map.read(filename,**kwargs)returncls(m,normalize=normalize,filename=filename)
[docs]defevaluate(self,lon,lat,energy=None):"""Evaluate the model at given coordinates. Note that, if the map data assume negative values, these are clipped to zero. """coord={"lon":lon.to_value("deg"),"lat":lat.to_value("deg"),}ifenergyisnotNone:coord["energy_true"]=energyval=self.map.interp_by_coord(coord,**self._interp_kwargs)val=np.clip(val,0,a_max=None)returnu.Quantity(val,self.map.unit,copy=False)
@propertydefposition(self):"""`~astropy.coordinates.SkyCoord`"""returnself.map.geom.center_skydir@propertydefposition_lonlat(self):"""Spatial model center position `(lon, lat)` in rad and frame of the model"""lon=self.position.data.lon.radlat=self.position.data.lat.radreturnlon,lat@propertydefframe(self):returnself.position.frame.name
[docs]defto_dict(self,full_output=False):"""Create dict for YAML serilisation"""data=super().to_dict(full_output)data["spatial"]["filename"]=self.filenamedata["spatial"]["normalize"]=self.normalizedata["spatial"]["unit"]=str(self.map.unit)returndata
[docs]defwrite(self,overwrite=False):ifself.filenameisNone:raiseIOError("Missing filename")elifos.path.isfile(self.filename)andnotoverwrite:log.warning("Template file already exits, and overwrite is False")else:self.map.write(self.filename,overwrite=overwrite)
[docs]defto_region(self,**kwargs):"""Model outline from template map boundary (`~regions.RectangleSkyRegion`)."""returnRectangleSkyRegion(center=self.map.geom.center_skydir,width=self.map.geom.width[0][0],height=self.map.geom.width[1][0],**kwargs,)