importcopyimportnumpyasnpfromastropyimportunitsasufromastropy.coordinatesimportSkyCoord__all__=["MapCoord"]defskycoord_to_lonlat(skycoord,frame=None):"""Convert SkyCoord to lon, lat, frame. Returns ------- lon : `~numpy.ndarray` Longitude in degrees. lat : `~numpy.ndarray` Latitude in degrees. """ifframe:skycoord=skycoord.transform_to(frame)returnskycoord.data.lon.deg,skycoord.data.lat.deg,skycoord.frame.name
[docs]classMapCoord:"""Represents a sequence of n-dimensional map coordinates. Contains coordinates for 2 spatial dimensions and an arbitrary number of additional non-spatial dimensions. For further information see :ref:`mapcoord`. Parameters ---------- data : `dict` of `~numpy.ndarray` Dictionary of coordinate arrays. frame : {"icrs", "galactic", None} Spatial coordinate system. If None then the coordinate system will be set to the native coordinate system of the geometry. match_by_name : bool Match coordinates to axes by name? If false coordinates will be matched by index. """def__init__(self,data,frame=None,match_by_name=True):if"lon"notindataor"lat"notindata:raiseValueError("data dictionary must contain axes named 'lon' and 'lat'.")self._data={k:np.atleast_1d(v)fork,vindata.items()}self._frame=frameself._match_by_name=match_by_namedef__getitem__(self,key):ifisinstance(key,str):returnself._data[key]else:returnlist(self._data.values())[key]def__setitem__(self,key,value):# TODO: check for broadcastability?self._data[key]=valuedef__iter__(self):returniter(self._data.values())@propertydefndim(self):"""Number of dimensions."""returnlen(self._data)@propertydefshape(self):"""Coordinate array shape."""arrays=[_for_inself._data.values()]returnnp.broadcast(*arrays).shape@propertydefsize(self):returnnp.prod(self.shape)@propertydeflon(self):"""Longitude coordinate in degrees."""returnself._data["lon"]@propertydeflat(self):"""Latitude coordinate in degrees."""returnself._data["lat"]@propertydeftheta(self):"""Theta co-latitude angle in radians."""theta=u.Quantity(self.lat,unit="deg",copy=False).to_value("rad")returnnp.pi/2.0-theta@propertydefphi(self):"""Phi longitude angle in radians."""phi=u.Quantity(self.lon,unit="deg",copy=False).to_value("rad")returnphi@propertydefframe(self):"""Coordinate system (str)."""returnself._frame@propertydefmatch_by_name(self):"""Boolean flag: axis lookup by name (True) or index (False)."""returnself._match_by_name@propertydefskycoord(self):returnSkyCoord(self.lon,self.lat,unit="deg",frame=self.frame)@classmethoddef_from_lonlat(cls,coords,frame=None,axis_names=None):"""Create a `~MapCoord` from a tuple of coordinate vectors. The first two elements of the tuple should be longitude and latitude in degrees. Parameters ---------- coords : tuple Tuple of `~numpy.ndarray`. Returns ------- coord : `~MapCoord` A coordinates object. """ifaxis_namesisNone:axis_names=[f"axis{idx}"foridxinrange(len(coords)-2)]ifisinstance(coords,(list,tuple)):coords_dict={"lon":coords[0],"lat":coords[1]}forname,cinzip(axis_names,coords[2:]):coords_dict[name]=celse:raiseValueError("Unrecognized input type.")returncls(coords_dict,frame=frame,match_by_name=False)@classmethoddef_from_tuple(cls,coords,frame=None,axis_names=None):"""Create from tuple of coordinate vectors."""ifisinstance(coords[0],(list,np.ndarray))ornp.isscalar(coords[0]):returncls._from_lonlat(coords,frame=frame,axis_names=axis_names)elifisinstance(coords[0],SkyCoord):lon,lat,frame=skycoord_to_lonlat(coords[0],frame=frame)coords=(lon,lat)+coords[1:]returncls._from_lonlat(coords,frame=frame,axis_names=axis_names)else:raiseTypeError(f"Type not supported: {type(coords)!r}")@classmethoddef_from_dict(cls,coords,frame=None):"""Create from a dictionary of coordinate vectors."""if"lon"incoordsand"lat"incoords:returncls(coords,frame=frame)elif"skycoord"incoords:lon,lat,frame=skycoord_to_lonlat(coords["skycoord"],frame=frame)coords_dict={"lon":lon,"lat":lat}fork,vincoords.items():ifk=="skycoord":continuecoords_dict[k]=vreturncls(coords_dict,frame=frame)else:raiseValueError("coords dict must contain 'lon'/'lat' or 'skycoord'.")
[docs]@classmethoddefcreate(cls,data,frame=None,axis_names=None):"""Create a new `~MapCoord` object. This method can be used to create either unnamed (with tuple input) or named (via dict input) axes. Parameters ---------- data : tuple, dict, `~gammapy.maps.MapCoord` or `~astropy.coordinates.SkyCoord` Object containing coordinate arrays. frame : {"icrs", "galactic", None}, optional Set the coordinate system for longitude and latitude. If None longitude and latitude will be assumed to be in the coordinate system native to a given map geometry. axis_names : list of str Axis names use if a tuple is provided Examples -------- >>> from astropy.coordinates import SkyCoord >>> from gammapy.maps import MapCoord >>> lon, lat = [1, 2], [2, 3] >>> skycoord = SkyCoord(lon, lat, unit='deg') >>> energy = [1000] >>> c = MapCoord.create((lon,lat)) >>> c = MapCoord.create((skycoord,)) >>> c = MapCoord.create((lon,lat,energy)) >>> c = MapCoord.create(dict(lon=lon,lat=lat)) >>> c = MapCoord.create(dict(lon=lon,lat=lat,energy=energy)) >>> c = MapCoord.create(dict(skycoord=skycoord,energy=energy)) """ifisinstance(data,cls):ifdata.frameisNoneorframe==data.frame:returndataelse:returndata.to_frame(frame)elifisinstance(data,dict):returncls._from_dict(data,frame=frame)elifisinstance(data,(list,tuple)):returncls._from_tuple(data,frame=frame,axis_names=axis_names)elifisinstance(data,SkyCoord):returncls._from_tuple((data,),frame=frame,axis_names=axis_names)else:raiseTypeError(f"Unsupported input type: {type(data)!r}")
[docs]defto_frame(self,frame):"""Convert to a different coordinate frame. Parameters ---------- frame : {"icrs", "galactic"} Coordinate system, either Galactic ("galactic") or Equatorial ("icrs"). Returns ------- coords : `~MapCoord` A coordinates object. """ifframe==self.frame:returncopy.deepcopy(self)else:lon,lat,frame=skycoord_to_lonlat(self.skycoord,frame=frame)data=copy.deepcopy(self._data)ifisinstance(self.lon,u.Quantity):lon=u.Quantity(lon,unit="deg",copy=False)ifisinstance(self.lon,u.Quantity):lat=u.Quantity(lat,unit="deg",copy=False)data["lon"]=londata["lat"]=latreturnself.__class__(data,frame,self._match_by_name)
[docs]defapply_mask(self,mask):"""Return a masked copy of this coordinate object. Parameters ---------- mask : `~numpy.ndarray` Boolean mask. Returns ------- coords : `~MapCoord` A coordinates object. """try:data={k:v[mask]fork,vinself._data.items()}exceptIndexError:data={}forname,coordinself._data.items():ifnamein["lon","lat"]:data[name]=np.squeeze(coord)[mask]else:data[name]=np.squeeze(coord,axis=-1)returnself.__class__(data,self.frame,self._match_by_name)