# Licensed under a 3-clause BSD style license - see LICENSE.rst
import astropy.units as u
import numpy as np
from astropy.coordinates import (
AltAz,
Angle,
BaseCoordinateFrame,
CoordinateAttribute,
DynamicMatrixTransform,
EarthLocationAttribute,
FunctionTransform,
ICRS,
RepresentationMapping,
SkyCoord,
SkyOffsetFrame,
TimeAttribute,
UnitSphericalRepresentation,
frame_transform_graph,
)
from astropy.coordinates.matrix_utilities import matrix_transpose, rotation_matrix
__all__ = ["FoVAltAzFrame", "FoVICRSFrame", "fov_to_sky", "sky_to_fov"]
reflect_lon_matrix = np.array([[1, 0, 0], [0, -1, 0], [0, 0, 1]])
[docs]
class FoVAltAzFrame(BaseCoordinateFrame):
"""
FoV coordinate frame. Centered on `origin` and aligned on AltAz frame at `location` and `obstime`.
Longitudes are reversed.
Attributes
----------
origin: `~astropy.coordinates.AltAz`
Origin of this frame as an Altaz coordinate
obstime: `~astropy.time.Time`
Observation time
location: `~astropy.coordinates.EarthLocation`
Location of the telescope/instrument/observatory
Examples
--------
.. testcode::
import astropy.units as u
from astropy.time import Time
from astropy.coordinates import AltAz, SkyCoord
from gammapy.utils.observers import observatory_locations
from gammapy.utils.coordinates import FoVAltAzFrame
location = observatory_locations.get("ctao_north")
obstime = Time("2025-01-01T00:00:00")
origin = AltAz(alt=80*u.deg, az=172*u.deg, location=location, obstime=obstime)
fov_frame = FoVAltAzFrame(origin=origin, location=location, obstime=obstime)
crab= SkyCoord(83.63333333, 22.01444444, unit="deg", frame="icrs")
crab.transform_to(fov_frame)
"""
frame_specific_representation_info = {
UnitSphericalRepresentation: [
RepresentationMapping("lon", "fov_lon"),
RepresentationMapping("lat", "fov_lat"),
]
}
default_representation = UnitSphericalRepresentation
origin = CoordinateAttribute(default=None, frame=AltAz)
obstime = TimeAttribute(default=None)
location = EarthLocationAttribute(default=None)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# make sure telescope coordinate is in range [-180°, 180°]
if isinstance(self._data, UnitSphericalRepresentation):
self._data.lon.wrap_angle = Angle(180, unit=u.deg)
@frame_transform_graph.transform(FunctionTransform, FoVAltAzFrame, FoVAltAzFrame)
def fov_to_fov(from_fov_altaz_coord, to_fov_altaz_frame):
"""Transform between two `FoVFrame`."""
intermediate_from = from_fov_altaz_coord.transform_to(from_fov_altaz_coord.origin)
intermediate_to = intermediate_from.transform_to(to_fov_altaz_frame.origin)
return intermediate_to.transform_to(to_fov_altaz_frame)
@frame_transform_graph.transform(DynamicMatrixTransform, AltAz, FoVAltAzFrame)
def altaz_to_fov_altaz(altaz_coord, fov_altaz_frame):
"""Convert a reference coordinate to a sky offset frame."""
# Define rotation matrices along the position angle vector, and
# relative to the origin.
origin = fov_altaz_frame.origin.represent_as(UnitSphericalRepresentation)
mat1 = rotation_matrix(-origin.lat, "y")
mat2 = rotation_matrix(origin.lon, "z")
return reflect_lon_matrix @ mat1 @ mat2
@frame_transform_graph.transform(DynamicMatrixTransform, FoVAltAzFrame, AltAz)
def fov_to_altaz(fov_altaz_coord, altaz_frame):
"""Convert an sky offset frame coordinate to the reference frame"""
# use the forward transform, but just invert it
mat = altaz_to_fov_altaz(altaz_frame, fov_altaz_coord)
return matrix_transpose(mat)
[docs]
class FoVICRSFrame(BaseCoordinateFrame):
"""
FoV coordinate frame aligned on ICRS frame. Centered on `origin` an ICRS coordinate and aligned on ICRS frame.
Longitudes are reversed.
Attributes
----------
origin: `~astropy.coordinates.ICRS`
Origin of this frame as an ICRS coordinate
Examples
--------
.. testcode::
import astropy.units as u
from astropy.time import Time
from astropy.coordinates import SkyCoord
from gammapy.utils.observers import observatory_locations
from gammapy.utils.coordinates import FoVICRSFrame
location = observatory_locations.get("ctao_north")
obstime = Time("2025-01-01T00:00:00")
origin = SkyCoord(85.63333333, 20.01444444, unit="deg", frame="icrs")
fov_frame = FoVICRSFrame(origin=origin)
crab = SkyCoord(83.63333333, 22.01444444, unit="deg", frame="icrs")
crab.transform_to(fov_frame)
"""
frame_specific_representation_info = {
UnitSphericalRepresentation: [
RepresentationMapping("lon", "fov_lon"),
RepresentationMapping("lat", "fov_lat"),
]
}
default_representation = UnitSphericalRepresentation
origin = CoordinateAttribute(default=None, frame=ICRS)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# make sure telescope coordinate is in range [-180°, 180°]
if isinstance(self._data, UnitSphericalRepresentation):
self._data.lon.wrap_angle = Angle(180, unit=u.deg)
@frame_transform_graph.transform(FunctionTransform, FoVICRSFrame, FoVICRSFrame)
def fov_icrs_to_fov_icrs(from_fov_icrs_coord, to_fov_icrs_frame):
"""Transform between two `FoVFrame`."""
intermediate_from = from_fov_icrs_coord.transform_to(from_fov_icrs_coord.origin)
intermediate_to = intermediate_from.transform_to(to_fov_icrs_frame.origin)
return intermediate_to.transform_to(to_fov_icrs_frame)
@frame_transform_graph.transform(DynamicMatrixTransform, ICRS, FoVICRSFrame)
def icrs_to_fov_icrs(icrs_coord, fov_icrs_frame):
"""Convert a reference coordinate to a sky offset frame."""
# Define rotation matrices along the position angle vector, and
# relative to the origin.
origin = fov_icrs_frame.origin.represent_as(UnitSphericalRepresentation)
mat1 = rotation_matrix(-origin.lat, "y")
mat2 = rotation_matrix(origin.lon, "z")
return reflect_lon_matrix @ mat1 @ mat2
@frame_transform_graph.transform(DynamicMatrixTransform, FoVICRSFrame, ICRS)
def fov_icrs_to_icrs(fov_icrs_coord, icrs_frame):
"""Convert an sky offset frame coordinate to the reference frame"""
# use the forward transform, but just invert it
mat = icrs_to_fov_icrs(icrs_frame, fov_icrs_coord)
return matrix_transpose(mat)
[docs]
def fov_to_sky(lon, lat, lon_pnt, lat_pnt):
"""Transform field-of-view coordinates to sky coordinates.
Parameters
----------
lon, lat : `~astropy.units.Quantity`
Field-of-view coordinate to be transformed.
lon_pnt, lat_pnt : `~astropy.units.Quantity`
Coordinate specifying the pointing position.
(i.e. the center of the field of view.)
Returns
-------
lon_t, lat_t : `~astropy.units.Quantity`
Transformed sky coordinate.
"""
# Create a frame that is centered on the pointing position
center = SkyCoord(lon_pnt, lat_pnt)
fov_frame = SkyOffsetFrame(origin=center)
# Define coordinate to be transformed.
# Need to switch the sign of the longitude angle here
# because this axis is reversed in our definition of the FoV-system
target_fov = SkyCoord(-lon, lat, frame=fov_frame)
# Transform into celestial system (need not be ICRS)
target_sky = target_fov.icrs
return target_sky.ra, target_sky.dec
[docs]
def sky_to_fov(lon, lat, lon_pnt, lat_pnt):
"""Transform sky coordinates to field-of-view coordinates.
Parameters
----------
lon, lat : `~astropy.units.Quantity`
Sky coordinate to be transformed.
lon_pnt, lat_pnt : `~astropy.units.Quantity`
Coordinate specifying the pointing position.
(i.e. the center of the field of view.)
Returns
-------
lon_t, lat_t : `~astropy.units.Quantity`
Transformed field-of-view coordinate.
"""
# Create a frame that is centered on the pointing position
center = SkyCoord(lon_pnt, lat_pnt)
fov_frame = SkyOffsetFrame(origin=center)
# Define coordinate to be transformed.
target_sky = SkyCoord(lon, lat)
# Transform into FoV-system
target_fov = target_sky.transform_to(fov_frame)
# Switch sign of longitude angle since this axis is
# reversed in our definition of the FoV-system
return -target_fov.lon, target_fov.lat