# Licensed under a 3-clause BSD style license - see LICENSE.rstimportcopyfromfunctoolsimportlru_cacheimportnumpyasnpimportastropy.unitsasufromastropy.convolutionimportTophat2DKernelfromastropy.coordinatesimportAngle,SkyCoordfromastropy.ioimportfitsfromastropy.nddataimportCutout2Dfromastropy.nddata.utilsimportoverlap_slicesfromastropy.utilsimportlazypropertyfromastropy.wcsimportWCSfromastropy.wcs.utilsimport(celestial_frame_to_wcs,proj_plane_pixel_scales,wcs_to_celestial_frame,)fromregionsimportRectangleSkyRegionfromgammapy.utils.arrayimportround_up_to_even,round_up_to_oddfrom..axesimportMapAxesfrom..coordimportMapCoord,skycoord_to_lonlatfrom..geomimportGeom,get_shape,pix_tuple_to_idxfrom..utilsimportINVALID_INDEX,_check_binsz,_check_width__all__=["WcsGeom"]defcast_to_shape(param,shape,dtype):"""Cast a tuple of parameter arrays to a given shape."""ifnotisinstance(param,tuple):param=[param]param=[np.array(p,ndmin=1,dtype=dtype)forpinparam]iflen(param)==1:param=[param[0].copy(),param[0].copy()]fori,pinenumerate(param):ifp.size>1andp.shape!=shape:raiseValueErrorifp.shape==shape:continueparam[i]=p*np.ones(shape,dtype=dtype)returntuple(param)defget_resampled_wcs(wcs,factor,downsampled):"""Get resampled WCS object."""wcs=wcs.deepcopy()ifnotdownsampled:factor=1.0/factorwcs.wcs.cdelt*=factorwcs.wcs.crpix=(wcs.wcs.crpix-0.5)/factor+0.5returnwcs
[docs]classWcsGeom(Geom):"""Geometry class for WCS maps. This class encapsulates both the WCS transformation object and the image extent (number of pixels in each dimension). Provides methods for accessing the properties of the WCS object and performing transformations between pixel and world coordinates. Parameters ---------- wcs : `~astropy.wcs.WCS` WCS projection object. npix : tuple Number of pixels in each spatial dimension. cdelt : tuple Pixel size in each image plane. If None then a constant pixel size will be used. crpix : tuple Reference pixel coordinate in each image plane. axes : list Axes for non-spatial dimensions. """_slice_spatial_axes=slice(0,2)_slice_non_spatial_axes=slice(2,None)is_hpx=Falseis_region=Falsedef__init__(self,wcs,npix=None,cdelt=None,crpix=None,axes=None):self._wcs=wcsself._frame=wcs_to_celestial_frame(wcs).nameself._projection=wcs.wcs.ctype[0][5:]self._axes=MapAxes.from_default(axes,n_spatial_axes=2)ifnpixisNone:ifwcs.array_shapeisnotNone:npix=wcs.array_shape[::-1]else:npix=(0,0)ifcdeltisNone:cdelt=tuple(np.abs(self.wcs.wcs.cdelt))# Shape to use for WCS transformationswcs_shape=max([get_shape(t)fortin[npix,cdelt]])self._npix=cast_to_shape(npix,wcs_shape,int)self._cdelt=cast_to_shape(cdelt,wcs_shape,float)# By convention CRPIX is indexed from 1ifcrpixisNone:crpix=tuple(1.0+(np.array(self._npix)-1.0)/2.0)self._crpix=crpix# define cached methodsself.get_coord=lru_cache()(self.get_coord)self.get_pix=lru_cache()(self.get_pix)def__setstate__(self,state):forkey,valueinstate.items():ifkeyin["get_coord","get_pix"]:state[key]=lru_cache()(value)self.__dict__=state@propertydefdata_shape(self):"""Shape of the `~numpy.ndarray` matching this geometry."""returnself._shape[::-1]@propertydefaxes_names(self):"""All axes names."""return["lon","lat"]+self.axes.names@propertydefdata_shape_axes(self):"""Shape of data of the non-spatial axes and unit spatial axes."""returnself.axes.shape[::-1]+(1,1)@propertydefdata_shape_image(self):"""Shape of data of the spatial axes and unit non-spatial axes."""return(1,)*len(self.axes)+self.data_shape[len(self.axes):]@propertydef_shape(self):npix_shape=tuple([np.max(self.npix[0]),np.max(self.npix[1])])returnnpix_shape+self.axes.shape@propertydef_shape_edges(self):npix_shape=tuple([np.max(self.npix[0])+1,np.max(self.npix[1])+1])returnnpix_shape+self.axes.shape@propertydefshape_axes(self):"""Shape of non-spatial axes."""returnself._shape[self._slice_non_spatial_axes]@propertydefwcs(self):"""WCS projection object."""returnself._wcs@propertydefframe(self):"""Coordinate system of the projection. Galactic ("galactic") or Equatorial ("icrs"). """returnself._frame
@propertydefprojection(self):"""Map projection."""returnself._projection@propertydefis_allsky(self):"""Flag for all-sky maps."""ifnp.all(np.isclose(self._npix[0]*self._cdelt[0],360.0))andnp.all(np.isclose(self._npix[1]*self._cdelt[1],180.0)):returnTrueelse:returnFalse@propertydefis_regular(self):"""If geometry is regular in non-spatial dimensions as a boolean. - False for multi-resolution or irregular geometries. - True if all image planes have the same pixel geometry. """ifself.npix[0].size>1:returnFalseelse:returnTrue@propertydefwidth(self):"""Tuple with image dimension in degrees in longitude and latitude."""dlon=self._cdelt[0]*self._npix[0]dlat=self._cdelt[1]*self._npix[1]return(dlon,dlat)*u.deg@propertydefpixel_area(self):"""Pixel area in deg^2."""# FIXME: Correctly compute solid angle for projectionreturnself._cdelt[0]*self._cdelt[1]@propertydefnpix(self):"""Tuple with image dimension in pixels in longitude and latitude."""returnself._npix@propertydefaxes(self):"""List of non-spatial axes."""returnself._axes@propertydefndim(self):returnlen(self.data_shape)@propertydefcenter_coord(self):"""Map coordinate of the center of the geometry. Returns ------- coord : tuple """returnself.pix_to_coord(self.center_pix)@propertydefcenter_pix(self):"""Pixel coordinate of the center of the geometry. Returns ------- pix : tuple """returntuple((np.array(self.data_shape)-1.0)/2)[::-1]@propertydefcenter_skydir(self):"""Sky coordinate of the center of the geometry. Returns ------- pix : `~astropy.coordinates.SkyCoord` """returnSkyCoord.from_pixel(self.center_pix[0],self.center_pix[1],self.wcs)@propertydefpixel_scales(self):""" Pixel scale. Returns angles along each axis of the image at the CRPIX location once it is projected onto the plane of intermediate world coordinates. Returns ------- angle: `~astropy.coordinates.Angle` """returnAngle(proj_plane_pixel_scales(self.wcs),"deg")
[docs]@classmethoddefcreate(cls,npix=None,binsz=0.5,proj="CAR",frame="icrs",refpix=None,axes=None,skydir=None,width=None,):"""Create a WCS geometry object. Pixelization of the map is set with ``binsz`` and one of either ``npix`` or ``width`` arguments. For maps with non-spatial dimensions a different pixelization can be used for each image plane by passing a list or array argument for any of the pixelization parameters. If both npix and width are None then an all-sky geometry will be created. Parameters ---------- npix : int or tuple or list, optional Width of the map in pixels. A tuple will be interpreted as parameters for longitude and latitude axes. For maps with non-spatial dimensions, list input can be used to define a different map width in each image plane. This option supersedes width. Default is None. binsz : float or tuple or list, optional Map pixel size in degrees. A tuple will be interpreted as parameters for longitude and latitude axes. For maps with non-spatial dimensions, list input can be used to define a different bin size in each image plane. Default is 0.5 proj : string, optional Any valid WCS projection type. Default is 'CAR' (Plate-Carrée projection). See `WCS supported projections <https://docs.astropy.org/en/stable/wcs/supported_projections.html>`__ # noqa: E501 frame : {"icrs", "galactic"}, optional Coordinate system, either Galactic ("galactic") or Equatorial ("icrs"). Default is "icrs". refpix : tuple, optional Reference pixel of the projection. If None this will be set to the center of the map. Default is None. axes : list, optional List of non-spatial axes. skydir : tuple or `~astropy.coordinates.SkyCoord`, optional Sky position of map center. Can be either a SkyCoord object or a tuple of longitude and latitude in deg in the coordinate system of the map. Default is None. width : float or tuple or list or string, optional Width of the map in degrees. A tuple will be interpreted as parameters for longitude and latitude axes. For maps with non-spatial dimensions, list input can be used to define a different map width in each image plane. Default is None. Returns ------- geom : `~WcsGeom` A WCS geometry object. Examples -------- >>> from gammapy.maps import WcsGeom >>> from gammapy.maps import MapAxis >>> axis = MapAxis.from_bounds(0,1,2) >>> geom = WcsGeom.create(npix=(100,100), binsz=0.1) >>> geom = WcsGeom.create(npix=(100,100), binsz="0.1deg") >>> geom = WcsGeom.create(npix=[100,200], binsz=[0.1,0.05], axes=[axis]) >>> geom = WcsGeom.create(npix=[100,200], binsz=["0.1deg","0.05deg"], axes=[axis]) >>> geom = WcsGeom.create(width=[5.0,8.0], binsz=[0.1,0.05], axes=[axis]) >>> geom = WcsGeom.create(npix=([100,200],[100,200]), binsz=0.1, axes=[axis]) """ifskydirisNone:crval=(0.0,0.0)elifisinstance(skydir,tuple):crval=skydirelifisinstance(skydir,SkyCoord):xref,yref,frame=skycoord_to_lonlat(skydir,frame=frame)crval=(xref,yref)else:raiseValueError(f"Invalid type for skydir: {type(skydir)!r}")ifwidthisnotNone:width=_check_width(width)binsz=_check_binsz(binsz)shape=max([get_shape(t)fortin[npix,binsz,width]])binsz=cast_to_shape(binsz,shape,float)# If both npix and width are None then create an all-sky geometryifnpixisNoneandwidthisNone:width=(360.0,180.0)ifnpixisNone:width=cast_to_shape(width,shape,float)npix=(np.rint(width[0]/binsz[0]).astype(int),np.rint(width[1]/binsz[1]).astype(int),)else:npix=cast_to_shape(npix,shape,int)ifrefpixisNone:nxpix=int(npix[0].flat[0])nypix=int(npix[1].flat[0])refpix=((nxpix+1)/2.0,(nypix+1)/2.0)# get frame classframe=SkyCoord(np.nan,np.nan,frame=frame,unit="deg").framewcs=celestial_frame_to_wcs(frame,projection=proj)wcs.wcs.crpix=refpixwcs.wcs.crval=crvalcdelt=(-binsz[0].flat[0],binsz[1].flat[0])wcs.wcs.cdelt=cdeltwcs.array_shape=npix[1].flat[0],npix[0].flat[0]wcs.wcs.datfix()returncls(wcs,npix,cdelt=binsz,axes=axes)
@propertydeffootprint(self):"""Footprint of the geometry as a `~astropy.coordinates.SkyCoord`."""coords=self.wcs.calc_footprint()returnSkyCoord(coords,frame=self.frame,unit="deg")@propertydeffootprint_rectangle_sky_region(self):"""Footprint of the geometry as a `~regions.RectangleSkyRegion`."""width,height=self.widthreturnRectangleSkyRegion(center=self.center_skydir,width=width[0],height=height[0])
[docs]@classmethoddeffrom_aligned(cls,geom,skydir,width):"""Create an aligned geometry from an existing one. Parameters ---------- geom : `~WcsGeom` A reference WCS geometry object. skydir : tuple or `~astropy.coordinates.SkyCoord` Sky position of map center. Can be either a SkyCoord object or a tuple of longitude and latitude in degrees in the coordinate system of the map. width : float or tuple or list or string Width of the map in degrees. A tuple will be interpreted as parameters for longitude and latitude axes. For maps with non-spatial dimensions, list input can be used to define a different map width in each image plane. Returns ------- geom : `~WcsGeom` An aligned WCS geometry object with specified size and center. """width=_check_width(width)*u.degnpix=tuple(np.round(width/geom.pixel_scales).astype(int))xref,yref=geom.to_image().coord_to_pix(skydir)xref=int(np.floor(-xref[0]+npix[0]/2.0))+geom.wcs.wcs.crpix[0]yref=int(np.floor(-yref[0]+npix[1]/2.0))+geom.wcs.wcs.crpix[1]returncls.create(skydir=tuple(geom.wcs.wcs.crval),npix=npix,refpix=(xref,yref),frame=geom.frame,binsz=tuple(geom.pixel_scales.deg),axes=geom.axes,proj=geom.projection,)
[docs]@classmethoddeffrom_header(cls,header,hdu_bands=None,format="gadf"):"""Create a WCS geometry object from a FITS header. Parameters ---------- header : `~astropy.io.fits.Header` The FITS header. hdu_bands : `~astropy.io.fits.BinTableHDU`, optional The BANDS table HDU. Default is None. format : {'gadf', 'fgst-ccube','fgst-template'}, optional FITS format convention. Default is "gadf". Returns ------- wcs : `~WcsGeom` WCS geometry object. """wcs=WCS(header,naxis=2).sub(2)axes=MapAxes.from_table_hdu(hdu_bands,format=format)shape=axes.shapeifhdu_bandsisnotNoneand"NPIX"inhdu_bands.columns.names:npix=hdu_bands.data.field("NPIX").reshape(shape+(2,))npix=(npix[...,0],npix[...,1])cdelt=hdu_bands.data.field("CDELT").reshape(shape+(2,))cdelt=(cdelt[...,0],cdelt[...,1])elif"WCSSHAPE"inheader:wcs_shape=eval(header["WCSSHAPE"])npix=(wcs_shape[0],wcs_shape[1])cdelt=Nonewcs.array_shape=npixelse:npix=(header["NAXIS1"],header["NAXIS2"])cdelt=Nonereturncls(wcs,npix,cdelt=cdelt,axes=axes)
def_get_pix_all(self,idx=None,mode="center",sparse=False,axis_name=("lon","lat")):"""Get index coordinate array without footprint of the projection applied."""pix_all=[]forname,nbininzip(self.axes_names,self._shape):ifmode=="edges"andnameinaxis_name:pix=np.arange(-0.5,nbin,dtype=float)else:pix=np.arange(nbin,dtype=float)pix_all.append(pix)# TODO: improve varying bin size coordinate handlingifidxisnotNone:pix_all=pix_all[self._slice_spatial_axes]+[float(t)fortinidx]returnnp.meshgrid(*pix_all[::-1],indexing="ij",sparse=sparse)[::-1]
[docs]defget_pix(self,idx=None,mode="center"):"""Get map pixel coordinates from the geometry. Parameters ---------- mode : {'center', 'edges'}, optional Get center or edge pix coordinates for the spatial axes. Default is "center". Returns ------- coord : tuple Map pixel coordinate tuple. """pix=self._get_pix_all(idx=idx,mode=mode)coords=self.pix_to_coord(pix)m=np.isfinite(coords[0])for_inpix:_[~m]=INVALID_INDEX.floatreturnpix
[docs]defget_coord(self,idx=None,mode="center",frame=None,sparse=False,axis_name=None):"""Get map coordinates from the geometry. Parameters ---------- mode : {'center', 'edges'}, optional Get center or edge coordinates for the spatial axes. Default is "center". frame : str or `~astropy.coordinates.Frame`, optional Coordinate frame. Default is None. sparse : bool, optional Compute sparse coordinates. Default is False. axis_name : str, optional If mode = "edges", the edges will be returned for this axis. Default is None. Returns ------- coord : `~MapCoord` Map coordinate object. """ifaxis_nameisNone:axis_name=("lon","lat")ifframeisNone:frame=self.framepix=self._get_pix_all(idx=idx,mode=mode,sparse=sparse,axis_name=axis_name)data=self.pix_to_coord(pix)coords=MapCoord.create(data=data,frame=self.frame,axis_names=self.axes.names)returncoords.to_frame(frame)
[docs]defcoord_to_pix(self,coords):coords=MapCoord.create(coords,frame=self.frame,axis_names=self.axes.names)ifcoords.size==0:returntuple([np.array([])foriinrange(coords.ndim)])# Variable Bin Sizeifnotself.is_regular:idxs=self.axes.coord_to_idx(coords,clip=True)crpix=[t[idxs]fortinself._crpix]cdelt=[t[idxs]fortinself._cdelt]pix=world2pix(self.wcs,cdelt,crpix,(coords.lon,coords.lat))pix=list(pix)else:pix=self._wcs.wcs_world2pix(coords.lon,coords.lat,0)pix+=self.axes.coord_to_pix(coords)returntuple(pix)
[docs]defpix_to_coord(self,pix):# Variable Bin Sizeifnotself.is_regular:idxs=pix_tuple_to_idx(pix[self._slice_non_spatial_axes])crpix=[t[idxs]fortinself._crpix]cdelt=[t[idxs]fortinself._cdelt]coords=pix2world(self.wcs,cdelt,crpix,pix[self._slice_spatial_axes])else:coords=self._wcs.wcs_pix2world(pix[0],pix[1],0)coords=(u.Quantity(coords[0],unit="deg",copy=False),u.Quantity(coords[1],unit="deg",copy=False),)coords+=self.axes.pix_to_coord(pix[self._slice_non_spatial_axes])returncoords
[docs]defdownsample(self,factor,axis_name=None):ifaxis_nameisNone:ifnp.any(np.mod(self.npix,factor)>0):raiseValueError(f"Spatial shape not divisible by factor {factor!r} in all axes."f" You need to pad prior to calling downsample.")npix=(self.npix[0]/factor,self.npix[1]/factor)cdelt=(self._cdelt[0]*factor,self._cdelt[1]*factor)wcs=get_resampled_wcs(self.wcs,factor,True)returnself._init_copy(wcs=wcs,npix=npix,cdelt=cdelt)else:ifnotself.is_regular:raiseNotImplementedError("Upsampling in non-spatial axes not supported for irregular geometries")axes=self.axes.downsample(factor=factor,axis_name=axis_name)returnself._init_copy(axes=axes)
[docs]defupsample(self,factor,axis_name=None):ifaxis_nameisNone:npix=(self.npix[0]*factor,self.npix[1]*factor)cdelt=(self._cdelt[0]/factor,self._cdelt[1]/factor)wcs=get_resampled_wcs(self.wcs,factor,False)returnself._init_copy(wcs=wcs,npix=npix,cdelt=cdelt)else:ifnotself.is_regular:raiseNotImplementedError("Upsampling in non-spatial axes not supported for irregular geometries")axes=self.axes.upsample(factor=factor,axis_name=axis_name)returnself._init_copy(axes=axes)
[docs]defto_binsz(self,binsz):"""Change pixel size of the geometry. Parameters ---------- binsz : float or tuple or list New pixel size in degree. Returns ------- geom : `WcsGeom` Geometry with new pixel size. """returnself.create(skydir=self.center_skydir,binsz=binsz,width=self.width,proj=self.projection,frame=self.frame,axes=copy.deepcopy(self.axes),)
[docs]defsolid_angle(self):"""Solid angle array as a `~astropy.units.Quantity` in ``sr``. The array has the same dimension as the WcsGeom object if the spatial shape is not unique along the extra axis, otherwise the array shape matches the spatial dimensions. To return solid angles for the spatial dimensions only use:: WcsGeom.to_image().solid_angle() """returnself._solid_angle
@lazypropertydef_solid_angle(self):ifself.is_regular:coord=self.to_image().get_coord(mode="edges").skycoordelse:coord=self.get_coord(mode="edges").skycoord# define pixel cornerslow_left=coord[...,:-1,:-1]low_right=coord[...,1:,:-1]up_left=coord[...,:-1,1:]up_right=coord[...,1:,1:]# compute side lengthslow=low_left.separation(low_right)left=low_left.separation(up_left)up=up_left.separation(up_right)right=low_right.separation(up_right)# compute enclosed anglesangle_low_right=low_right.position_angle(up_right)-low_right.position_angle(low_left)angle_up_left=up_left.position_angle(up_right)-low_left.position_angle(up_left)# compute area assuming a planar trianglearea_low_right=0.5*low*right*np.sin(angle_low_right)area_up_left=0.5*up*left*np.sin(angle_up_left)# TODO: for non-negative cdelt a negative solid angle is returned# find out why and fix properlyvalue=np.abs(u.Quantity(area_low_right+area_up_left,"sr",copy=False))ifself.is_regular:value=value.reshape(self.data_shape_image)returnvalue
[docs]defbin_volume(self):"""Bin volume as a `~astropy.units.Quantity`."""returnself._bin_volume
@lazypropertydef_bin_volume(self):"""Cached property of bin volume."""value=self.to_image().solid_angle()ifnotself.is_image:value=value*self.axes.bin_volume()returnvalue
[docs]defseparation(self,center):"""Compute sky separation with respect to a given center. Parameters ---------- center : `~astropy.coordinates.SkyCoord` Center position. Returns ------- separation : `~astropy.coordinates.Angle` Separation angle array (2D). """coord=self.to_image().get_coord()returncenter.separation(coord.skycoord)
[docs]defcutout(self,position,width,mode="trim",odd_npix=False):""" Create a cutout around a given position. Parameters ---------- position : `~astropy.coordinates.SkyCoord` Center position of the cutout region. width : tuple of `~astropy.coordinates.Angle` Angular sizes of the region in (lon, lat) in that specific order. If only one value is passed, a square region is extracted. mode : {'trim', 'partial', 'strict'}, optional Mode option for Cutout2D, for details see `~astropy.nddata.utils.Cutout2D`. Default is "trim". odd_npix : bool, optional Force width to odd number of pixels. Default is False. Returns ------- cutout : `~gammapy.maps.WcsNDMap` Cutout map. """width=_check_width(width)*u.degbinsz=self.pixel_scaleswidth_npix=np.clip((width/binsz).to_value(""),1,None)ifodd_npix:width_npix=round_up_to_odd(width_npix)dummy_data=np.empty(self.to_image().data_shape,dtype=bool)c2d=Cutout2D(data=dummy_data,wcs=self.wcs,position=position,# Cutout2D takes size with order (lat, lon)size=width_npix[::-1],mode=mode,)returnself._init_copy(wcs=c2d.wcs,npix=c2d.shape[::-1])
[docs]defboundary_mask(self,width):"""Create a mask applying binary erosion with a given width from geometry edges. Parameters ---------- width : tuple of `~astropy.units.Quantity` Angular sizes of the margin in (lon, lat) in that specific order. If only one value is passed, the same margin is applied in (lon, lat). Returns ------- mask_map : `~gammapy.maps.WcsNDMap` of boolean type Boundary mask. """from.ndmapimportWcsNDMapdata=np.ones(self.data_shape,dtype=bool)returnWcsNDMap.from_geom(self,data=data).binary_erode(width=2*u.Quantity(width),kernel="box")
[docs]defregion_mask(self,regions,inside=True):"""Create a mask from a given list of regions. The mask is filled such that a pixel inside the region is filled with "True". To invert the mask, e.g. to create a mask with exclusion regions the tilde (~) operator can be used (see example below). Parameters ---------- regions : str, `~regions.Region` or list of `~regions.Region` Region or list of regions (pixel or sky regions accepted). A region can be defined as a string ind DS9 format as well. See http://ds9.si.edu/doc/ref/region.html for details. inside : bool, optional For ``inside=True``, set pixels in the region to True. For ``inside=False``, set pixels in the region to False. Default is True. Returns ------- mask_map : `~gammapy.maps.WcsNDMap` of boolean type Boolean region mask. Examples -------- Make an exclusion mask for a circular region:: from regions import CircleSkyRegion from astropy.coordinates import SkyCoord, Angle from gammapy.maps import WcsNDMap, WcsGeom pos = SkyCoord(0, 0, unit='deg') geom = WcsGeom.create(skydir=pos, npix=100, binsz=0.1) region = CircleSkyRegion( SkyCoord(3, 2, unit='deg'), Angle(1, 'deg'), ) # the Gammapy convention for exclusion regions is to take the inverse mask = ~geom.region_mask([region]) Note how we made a list with a single region, since this method expects a list of regions. """fromgammapy.mapsimportMap,RegionGeomifnotself.is_regular:raiseValueError("Multi-resolution maps not supported yet")geom=RegionGeom.from_regions(regions,wcs=self.wcs)idx=self.get_idx()mask=geom.contains_wcs_pix(idx)ifnotinside:np.logical_not(mask,out=mask)returnMap.from_geom(self,data=mask)
[docs]defregion_weights(self,regions,oversampling_factor=10):"""Compute regions weights. Parameters ---------- regions : str, `~regions.Region` or list of `~regions.Region` Region or list of regions (pixel or sky regions accepted). A region can be defined as a string ind DS9 format as well. See http://ds9.si.edu/doc/ref/region.html for details. oversampling_factor : int, optional Over-sampling factor to compute the region weights. Default is 10. Returns ------- map : `~gammapy.maps.WcsNDMap` of boolean type Weights region mask. """geom=self.upsample(factor=oversampling_factor)m=geom.region_mask(regions=regions)m.data=m.data.astype(float)returnm.downsample(factor=oversampling_factor,preserve_counts=False)
[docs]defbinary_structure(self,width,kernel="disk"):"""Get binary structure. Parameters ---------- width : `~astropy.units.Quantity`, str or float If a float is given it interpreted as width in pixels. If an (angular) quantity is given it converted to pixels using ``geom.wcs.wcs.cdelt``. The width corresponds to radius in case of a disk kernel, and the side length in case of a box kernel. kernel : {'disk', 'box'}, optional Kernel shape. Default is "disk". Returns ------- structure : `~numpy.ndarray` Binary structure. """width=u.Quantity(width)ifwidth.unit.is_equivalent("deg"):width=width/self.pixel_scaleswidth=round_up_to_odd(width.to_value(""))ifkernel=="disk":disk=Tophat2DKernel(width[0])disk.normalize("peak")structure=disk.arrayelifkernel=="box":structure=np.ones(width)else:raiseValueError(f"Invalid kernel: {kernel!r}")shape=(1,)*len(self.axes)+structure.shapereturnstructure.reshape(shape)
[docs]defto_odd_npix(self,max_radius=None):"""Create a new geometry object with an odd number of pixels and a maximum size. This is useful for PSF kernel creation. Parameters ---------- max_radius : `~astropy.units.Quantity`, optional Maximum radius of the geometry (half the width). Default is None. Returns ------- geom : `WcsGeom` Geometry with odd number of pixels. """ifmax_radiusisNone:width=self.width.max()else:width=2*u.Quantity(max_radius)binsz=self.pixel_scales.max()width_npix=(width/binsz).to_value("")npix=round_up_to_odd(width_npix)returnWcsGeom.create(skydir=self.center_skydir,binsz=binsz,npix=npix,proj=self.projection,frame=self.frame,axes=self.axes,)
[docs]defto_even_npix(self):"""Create a new geometry object with an even number of pixels and a maximum size. Returns ------- geom : `WcsGeom` Geometry with odd number of pixels. """width=self.width.max()binsz=self.pixel_scales.max()width_npix=(width/binsz).to_value("")npix=round_up_to_even(width_npix)returnWcsGeom.create(skydir=self.center_skydir,binsz=binsz,npix=npix,proj=self.projection,frame=self.frame,axes=self.axes,)
[docs]defis_aligned(self,other,tolerance=1e-6):"""Check if WCS and extra axes are aligned. Parameters ---------- other : `WcsGeom` Other geometry. tolerance : float, optional Tolerance for the comparison. Default is 1e-6. Returns ------- aligned : bool Whether geometries are aligned. """foraxis,otheraxisinzip(self.axes,other.axes):ifaxis!=otheraxis:returnFalse# check WCS consistency with a priori tolerance of 1e-6returnself.wcs.wcs.compare(other.wcs.wcs,cmp=2,tolerance=tolerance)
[docs]defis_allclose(self,other,rtol_axes=1e-6,atol_axes=1e-6,rtol_wcs=1e-6):"""Compare two data IRFs for equivalency. Parameters ---------- other : `WcsGeom` Geom to compare against. rtol_axes : float, optional Relative tolerance for the axes comparison. Default is 1e-6. atol_axes : float, optional Relative tolerance for the axes comparison. Default is 1e-6. rtol_wcs : float, optional Relative tolerance for the WCS comparison. Default is 1e-6. Returns ------- is_allclose : bool Whether the geometry is all close. """ifnotisinstance(other,self.__class__):returnTypeError(f"Cannot compare {type(self)} and {type(other)}")ifself.data_shape!=other.data_shape:returnFalseaxes_eq=self.axes.is_allclose(other.axes,rtol=rtol_axes,atol=atol_axes)# check WCS consistency with a priori tolerance of 1e-6# cmp=1 parameter ensures no comparison with ancillary information# see https://github.com/astropy/astropy/pull/4522/fileswcs_eq=self.wcs.wcs.compare(other.wcs.wcs,cmp=1,tolerance=rtol_wcs)returnaxes_eqandwcs_eq
def__eq__(self,other):ifnotisinstance(other,self.__class__):returnFalseifnot(self.is_regularandother.is_regular):raiseNotImplementedError("Geom comparison is not possible for irregular geometries.")returnself.is_allclose(other=other,rtol_wcs=1e-6,rtol_axes=1e-6)def__ne__(self,other):returnnotself.__eq__(other)def__hash__(self):returnid(self)
defpix2world(wcs,cdelt,crpix,pix):"""Perform pixel to world coordinate transformation. For a WCS projection with a given pixel size (CDELT) and reference pixel (CRPIX). This method can be used to perform WCS transformations for projections with different pixelizations but the same reference coordinate (CRVAL), projection type, and coordinate system. Parameters ---------- wcs : `~astropy.wcs.WCS` WCS transform object. cdelt : tuple Tuple of X/Y pixel size in deg. Each element should have the same length as ``pix``. crpix : tuple Tuple of reference pixel parameters in X and Y dimensions. Each element should have the same length as ``pix``. pix : tuple Tuple of pixel coordinates. """pix_ratio=[np.abs(wcs.wcs.cdelt[0]/cdelt[0]),np.abs(wcs.wcs.cdelt[1]/cdelt[1]),]pix=((pix[0]-(crpix[0]-1.0))/pix_ratio[0]+wcs.wcs.crpix[0]-1.0,(pix[1]-(crpix[1]-1.0))/pix_ratio[1]+wcs.wcs.crpix[1]-1.0,)returnwcs.wcs_pix2world(pix[0],pix[1],0)defworld2pix(wcs,cdelt,crpix,coord):pix_ratio=[np.abs(wcs.wcs.cdelt[0]/cdelt[0]),np.abs(wcs.wcs.cdelt[1]/cdelt[1]),]pix=wcs.wcs_world2pix(coord[0],coord[1],0)return((pix[0]-(wcs.wcs.crpix[0]-1.0))*pix_ratio[0]+crpix[0]-1.0,(pix[1]-(wcs.wcs.crpix[1]-1.0))*pix_ratio[1]+crpix[1]-1.0,)