# Licensed under a 3-clause BSD style license - see LICENSE.rst
"""
This module is intended to contain astronomy-related helper tools which are
not provided by external packages and/or to satisfy particular needs of
usage within ctapipe.
"""
import logging
from collections import namedtuple
from copy import deepcopy
from enum import Enum
import astropy.units as u
from astropy.coordinates import Angle, SkyCoord, UnitSphericalCosLatDifferential
from astropy.table import Table
from astropy.time import Time
from astropy.units import Quantity
log = logging.getLogger("main")
__all__ = ["get_star_catalog", "get_bright_stars", "StarCatalog", "CatalogInfo"]
# Define a namedtuple to hold the catalog information
CatalogInfo = namedtuple(
"CatalogInfo", ["directory", "coordinates", "columns", "record"]
)
[docs]
class StarCatalog(Enum):
"""
Enumeration of star catalogs with their respective metadata.
Each catalog entry is represented as a namedtuple `CatalogInfo` containing:
Attributes
----------
directory : str
The directory path of the catalog in the Vizier database.
coordinates : dict
A dictionary containing the coordinate frame, epoch, and column names for RA and DE.
columns : list of str
A list of columns to be retrieved from the catalog.
record : str
A name of the catalog file in the cache.
"""
Yale = CatalogInfo(
directory="V/50/catalog",
coordinates={
"frame": "icrs",
"epoch": "J2000.0",
"RA": {"column": "RAJ2000", "unit": "hourangle"},
"DE": {"column": "DEJ2000", "unit": "deg"},
},
#: Vmag is mandatory (used for initial magnitude cut)
columns=["RAJ2000", "DEJ2000", "pmRA", "pmDE", "Vmag", "HR"],
record="yale_bright_star_catalog",
) #: Yale bright star catalogue
Hipparcos = CatalogInfo(
directory="I/239/hip_main",
coordinates={
"frame": "icrs",
"epoch": "J1991.25",
"RA": {"column": "RAICRS", "unit": "deg"},
"DE": {"column": "DEICRS", "unit": "deg"},
},
#: Vmag is mandatory (used for initial magnitude cut)
columns=["RAICRS", "DEICRS", "pmRA", "pmDE", "Vmag", "BTmag", "HIP"],
record="hipparcos_star_catalog",
) #: HIPPARCOS catalogue
def select_stars(
stars: Table,
pointing: SkyCoord = None,
radius: Quantity = None,
magnitude_cut: float = None,
band: str = "Vmag",
) -> Table:
"""
Utility function to filter stars based on magnitude and/or location.
Parameters
----------
stars : astropy.table.Table
Table of stars, including magnitude and coordinates.
pointing : astropy.coordinates.SkyCoord, optional
Pointing direction in the sky. If None is given, the full sky is returned.
radius : astropy.units.Quantity, optional
Radius of the sky region around the pointing position. Default is the full sky.
magnitude_cut : float, optional
Return only stars above a given apparent magnitude. Default is None (all entries).
band : str, optional
Wavelength band to use for the magnitude cut. Options are 'Vmag'
or any other optical band column name, present in the catalog (e.g. Bmag or BTmag, etc.).
Default is 'Vmag'.
Returns
-------
astropy.table.Table
List of all stars after applying the cuts, with the same keys as the input table `stars`.
"""
stars_ = deepcopy(stars)
if magnitude_cut:
try:
stars_ = stars_[stars_[band] < magnitude_cut]
except KeyError:
raise ValueError(
f"The requested catalogue has no compatible magnitude for the {band} band"
)
if radius is not None:
if pointing:
stars_["separation"] = stars_["ra_dec"].separation(pointing)
stars_ = stars_[stars_["separation"] < radius]
else:
raise ValueError(
"Sky pointing, pointing=SkyCoord(), must be "
"provided if radius is given."
)
return stars_
[docs]
def get_star_catalog(
catalog: str | StarCatalog, magnitude_cutoff: float = 8.0, row_limit: int = 1000000
) -> Table:
"""
Utility function to download a star catalog for the get_bright_stars function.
Parameters
----------
catalog : str or ctapipe.utils.astro.StarCatalog
Name of the catalog to be used. Usable names are found in the Enum StarCatalog. Default: Yale.
magnitude_cutoff : float, optional
Maximum value for magnitude used in lookup. Default is 8.0.
row_limit : int, optional
Maximum number of rows for the star catalog lookup. Default is 1000000.
Returns
-------
astropy.table.Table
List of all stars after cuts with catalog numbers, magnitudes,
and coordinates as SkyCoord objects including proper motion.
"""
from astroquery.vizier import Vizier
if isinstance(catalog, str):
catalog = StarCatalog[catalog]
catalog_info = catalog.value
catalog_name = catalog.name
vizier = Vizier(
catalog=catalog_info.directory,
columns=catalog_info.columns,
row_limit=row_limit,
)
stars = vizier.query_constraints(Vmag=f"<{magnitude_cutoff}")[0]
header = {
"ORIGIN": "CTAPIPE",
"JEPOCH": float(catalog_info.coordinates["epoch"].replace("J", "")),
"RADESYS": catalog_info.coordinates["frame"].upper(),
"MAGCUT": magnitude_cutoff,
"BAND": "V",
"CATALOG": catalog_name,
"REFERENC": catalog_info.directory,
"COLUMNS": "_".join(catalog_info.columns),
}
stars.meta = header
return stars
[docs]
def get_bright_stars(
time: Time,
catalog: StarCatalog | str = "Yale",
pointing: SkyCoord | None = None,
radius: Quantity | None = None,
magnitude_cut: float | None = None,
band: str = "Vmag",
) -> Table:
"""
Get an astropy table of bright stars from the specified star catalog.
Parameters
----------
time : astropy.time.Time
Time to which proper motion is applied.
catalog : str or ctapipe.utils.astro.StarCatalog, optional
Name of the catalog to be used. Available catalogues are 'Yale' and 'Hipparcos'. Default is 'Yale'.
pointing : astropy.coordinates.SkyCoord, optional
Pointing direction in the sky. If None is given, the full sky is returned.
radius : astropy.units.Quantity, optional
Radius of the sky region around the pointing position. Default is the full sky.
magnitude_cut : float, optional
Return only stars above a given absolute magnitude. Default is None (all entries).
band : str, optional
Wavelength band to use for the magnitude cut. Options are 'Vmag'
or any other optical band column name, present in the catalog (e.g. Bmag or BTmag, etc.).
Default is 'Vmag'.
Returns
-------
astropy.table.Table
List of all stars after applying the cuts, with catalog numbers, magnitudes,
and coordinates as SkyCoord objects including proper motion.
"""
from importlib.resources import as_file, files
if isinstance(catalog, str):
catalog = StarCatalog[catalog]
cat = catalog.value
record = cat.record
with as_file(files("ctapipe").joinpath(f"resources/{record}.fits.gz")) as f:
stars = Table.read(f)
stars["ra_dec"] = SkyCoord(
ra=Angle(
stars[cat.coordinates["RA"]["column"]],
unit=u.Unit(cat.coordinates["RA"]["unit"]),
),
dec=Angle(
stars[cat.coordinates["DE"]["column"]],
unit=u.Unit(cat.coordinates["DE"]["unit"]),
),
pm_ra_cosdec=stars["pmRA"].quantity,
pm_dec=stars["pmDE"].quantity,
frame=cat.coordinates["frame"],
obstime=Time(cat.coordinates["epoch"]),
)
stars["ra_dec"] = stars["ra_dec"].apply_space_motion(new_obstime=time)
stars["ra_dec"].data.differentials["s"] = (
stars["ra_dec"]
.data.differentials["s"]
.represent_as(UnitSphericalCosLatDifferential)
)
stars.remove_columns(
[cat.coordinates["RA"]["column"], cat.coordinates["DE"]["column"]]
)
stars = select_stars(
stars, pointing=pointing, radius=radius, magnitude_cut=magnitude_cut, band=band
)
return stars