Source code for m4opt.constraints._positional

"""Basic positional astronomy constraints."""

from abc import abstractmethod
from typing import override

from astropy import units as u
from astropy.coordinates import (
    ICRS,
    AltAz,
    Angle,
    EarthLocation,
    HADec,
    SkyCoord,
    UnitSphericalRepresentation,
)
from astropy.time import Time

from ._core import Constraint


class AngleConstraint(Constraint):
    _key: str

    def __init__(
        self,
        min: u.Quantity[u.physical.angle] | Angle,
        max: u.Quantity[u.physical.angle] | Angle,
    ):
        self._min = min
        self._max = max

    @abstractmethod
    def _frame(self, observer_location: EarthLocation, obstime: Time):
        """Frame for this constraint"""

    def _get_angle(
        self, observer_location: EarthLocation, target_coord: SkyCoord, obstime: Time
    ):
        return getattr(
            target_coord.transform_to(
                self._frame(observer_location, obstime)
            ).represent_as(UnitSphericalRepresentation),
            self._key,
        )

    @override
    def __call__(self, *args):
        angle = self._get_angle(*args)
        return (self._min <= angle) & (angle <= self._max)


class AltAzConstraint(AngleConstraint):
    """Constrain an angle in the :class:`~astropy.coordinates.AltAz` frame."""

    @override
    def _frame(self, observer_location, obstime):
        return AltAz(obstime=obstime, location=observer_location)


class HADecConstraint(AngleConstraint):
    """Constrain an angle in the :class:`~astropy.coordinates.HADec` frame."""

    @override
    def _frame(self, observer_location, obstime):
        return HADec(obstime=obstime, location=observer_location)


class ICRSConstraint(AngleConstraint):
    """Constrain an angle in the :class:`~astropy.coordinates.ICRS` frame."""

    @override
    def _frame(self, *_):
        return ICRS()


class LongitudeConstraint(AngleConstraint):
    """Constrain a generic longitude-like angle.

    Notes
    -----
    The allowed interval extends from the minimum angle to the maximum angle.
    For example, if the minimum and maximum angle are 10° and 30° respectively,
    then the constraint will return true over an interval of 20°. However, if
    the minimum and maximum angle are 30° and 10°, then the constraint will
    return true over an interval of 340°.
    """

    _key = "lon"

    @override
    def __init__(self, min, max):
        super().__init__(Angle(min).wrap_at(max), max)

    @override
    def _get_angle(self, *args):
        return super()._get_angle(*args).wrap_at(self._max)


class LatitudeConstraint(AngleConstraint):
    """Constrain a generic latitude-like angle.

    Notes
    -----
    If the maximum angle is less than the minimum angle, then they are swapped.
    """

    _key = "lat"

    @override
    def __init__(self, *args):
        super().__init__(*sorted(args))


[docs] class AltitudeConstraint(LatitudeConstraint, AltAzConstraint): """Constrain the altitude of the target. See Also -------- AzimuthConstraint """
[docs] class AzimuthConstraint(LongitudeConstraint, AltAzConstraint): """Constrain the azimuth of the target. See Also -------- AltitudeConstraint """
[docs] class RightAscensionConstraint(LongitudeConstraint, ICRSConstraint): """Constrain the ICRS right ascension of the target. See Also -------- DeclinationConstraint """
[docs] class DeclinationConstraint(LatitudeConstraint, ICRSConstraint): """Constrain the ICRS declination of the target. Notes ----- If the maximum angle is less than the minimum angle, then they are swapped. See Also -------- RightAscensionConstraint """
[docs] class HourAngleConstraint(LongitudeConstraint, HADecConstraint): """Constrain the hour angle of the target. See Also -------- RightAscensionConstraint """