Source code for giant.catalogues.utilities

# Copyright 2021 United States Government as represented by the Administrator of the National Aeronautics and Space
# Administration.  No copyright is claimed in the United States under Title 17, U.S. Code. All Other Rights Reserved.


r"""
This submodule provides utility constants and functions for working with star data in GIANT.

Most of these utilities are focused on conversions of either units or of representations (ie a bearing to a unit
vector) with the exception of applying proper motion and computing the distance between 2 bearings.  For more details on
what is contained refer to the following summary tables and the documentation for each constant/function.
"""

import datetime

from typing import Tuple, Union

import numpy as np

import pandas as pd

from giant._typing import SCALAR_OR_ARRAY, ARRAY_LIKE, Real


__all__ = ['DEG2RAD', 'RAD2DEG', 'DEG2MAS', 'MAS2DEG', 'RAD2MAS', 'MAS2RAD', 'PARSEC2KM', 'STAR_DIST',
           'SI_DAYS_PER_YEAR', 'SI_SECONDS_PER_DAY', 'MJD_EPOCH', 'radec_to_unit', 'unit_to_radec',
           'timedelta_to_si_years', 'datetime_to_mjd_years', 'apply_proper_motion', 'radec_distance']
"""
Things to import if someone wants to do from giant.catalogues.utilities import *
"""

# CONSTANTS.

DEG2RAD: float = np.pi / 180  # rad/deg
r"""
This constant converts from units of degrees to units of radians through multiplication.

That is ``angle_rad = angle_deg*DEG2RAD`` where ``angle_rad`` is the angle in radians and ``angle_deg`` is the angle in 
degrees.

Mathematically this is :math:`\frac{\pi}{180}`.
"""

RAD2DEG: float = 180 / np.pi  # deg/rad
r"""
This constant converts from units of radians to units of degrees through multiplication.

That is ``angle_deg = angle_rad*RAD2DEG`` where ``angle_rad`` is the angle in radians and ``angle_deg`` is the angle in 
degrees.

Mathematically this is :math:`\frac{180}{\pi}`.
"""

DEG2MAS: float = 3600 * 1000  # mas/deg
r"""
This constant converts from units of degrees to units of milli-arc-seconds through multiplication.

That is ``angle_mas = angle_deg*DEG2MAS`` where ``angle_mas`` is the angle in milli-arc-seconds and ``angle_deg`` is the 
angle in degrees.

Mathematically this is :math:`3600000`.
"""

MAS2DEG: float = 1 / DEG2MAS  # deg/mas
r"""
This constant converts from units of milli-arc-seconds to units of degrees through multiplication.

That is ``angle_deg = angle_mas*MAS2DEG`` where ``angle_mas`` is the angle in milli-arc-seconds and ``angle_deg`` is the 
angle in degrees.

Mathematically this is :math:`\frac{1}{3600000}`.
"""

RAD2MAS: float = RAD2DEG * DEG2MAS
r"""
This constant converts from units of radians to units of milli-arc-seconds through multiplication.

That is ``angle_mas = angle_rad*RAD2MAS`` where ``angle_mas`` is the angle in milli-arc-seconds and ``angle_rad`` is the 
angle in radians.

Mathematically this is :math:`\frac{180}{3600000\pi}`.
"""

MAS2RAD: float = 1 / RAD2MAS
r"""
This constant converts from units of milli-arc-seconds to units of radians through multiplication.

That is ``angle_rad = angle_mas*MAS2RAD`` where ``angle_mas`` is the angle in milli-arc-seconds and ``angle_rad`` is the 
angle in radians.

Mathematically this is :math:`\frac{3600000\pi}{180}`.
"""

PARSEC2KM: float = 30856775814913.673  # km
r"""
This constant converts from units of parsecs to units of kilometers through multiplication.

That is ``distance_km = distance_parsec*PARSEC2KM`` where ``distance_km`` is the distance in kilometers and 
``distance_parsec`` is the distance in parsecs.

Mathematically this is :math:`\frac{3600000\pi}{180}`.
"""

STAR_DIST: float = 5.428047027e15  # km
"""
The average distance of stars from the UCAC4 catalogue in kilometers.  

This value is used to set the distance for stars for which there is no distance information available.
"""

SI_DAYS_PER_YEAR: float = 365.25  # days
"""
This constant provides the number of SI days in a SI year.
"""

SI_SECONDS_PER_DAY: int = 86400  # seconds
"""
This constant provides the number of SI seconds in a SI day.
"""

MJD_EPOCH: datetime.datetime = datetime.datetime(1858, 11, 17)  # November 17, 1858
"""
This constant provides the standard modified Julian date epoch, November 17, 1858, as a datetime
"""


# UTILITY FUNCTIONS
[docs]def radec_to_unit(ra: SCALAR_OR_ARRAY, dec: SCALAR_OR_ARRAY) -> np.ndarray: r""" This utility converts (a) right ascension and declination pair(s) expressed in units of radians into (an) unit vector(s). The conversion to a unit vector is given by: .. math:: \hat{\mathbf{x}}=\left[\begin{array}{c}\text{cos}(\delta)\text{cos}(\alpha)\\ \text{cos}(\delta)\text{sin}(\alpha)\\ \text{sin}(\delta)\end{array}\right] where :math:`\alpha` is the right ascension, :math:`\delta` is the declination, and :math:`\hat{\mathbf{x}}` is the resulting unit vector. This method is vectorized, therefore multiple unit vectors can be created from multiple ra, dec pairs at the same time. When multiple conversions are performed, each unit vector is specified as a column in the array. This function performs broadcast rules using numpy conventions, therefore you can provide inputs with different shapes, so long as they are able to be broadcast (you could add them together and numpy wouldn't complain). If you provide >1D arrays then they will be raveled using c convention. :param ra: The right ascension(s) to convert to unit vector(s) in units of radians :param dec: The declination(s) to convert to unit vector(s) in units of radians :return: A 3xn array of unit vectors corresponding to the right ascension, declination pair(s) """ # make sure the arrays are the same length ra, dec = np.broadcast_arrays(ra, dec) ra = ra.ravel() # ensure our arrays are flat dec = dec.ravel() # ensure our arrays are flat unit_x = np.cos(dec) * np.cos(ra) unit_y = np.cos(dec) * np.sin(ra) unit_z = np.sin(dec) return np.vstack([unit_x, unit_y, unit_z]).squeeze()
[docs]def unit_to_radec(unit: ARRAY_LIKE) -> Tuple[SCALAR_OR_ARRAY, SCALAR_OR_ARRAY]: r""" This function converts a unit vector(s) into a right ascension/declination bearing(s). The right ascension is defined as the angle between the x axis and the projection of the unit vector onto the xy-plane and is output between 0 and pi. The declination is defined as the angle between the xy-plane and the unit vector and is output between -pi/2 and pi/2 (positive values indicate the vector has a positive z component and negative values indicate the vector has a negative z component. These are computed using .. math:: dec = \text{sin}^{-1}(z) \\ ra = \text{tan}^{-1}\left(\frac{y}{x}\right) Note that the vector input should be along the first axis (or as columns if there are multiple vectors), and that the vector(s) needs to be of unit length or the results from this function will be invalid. If the input contains more than 1 vector, then the output will be 2 arrays. Otherwise, if it is a single vector, the output will be 2 floats as a tuple. :param unit: The unit vector to be converted to a ra/dec bearing :return: The right ascension(s) and declination(s) as a tuple in units of radians """ if np.shape(unit)[0] != 3: raise ValueError('The length of the first axis must be 3') dec = np.arcsin(unit[2]) ra = np.arctan2(unit[1], unit[0]) ra_check = ra < 0 if np.ndim(ra): ra[ra_check] += 2 * np.pi elif ra_check: ra += 2 * np.pi return ra, dec
[docs]def timedelta_to_si_years(delta: datetime.timedelta) -> float: """ This function converts a python timedelta object to a fractional number of SI years. The timedelta object is first converted to seconds using method ``total_seconds``, and this is then converted to SI years. :param delta: The python timedelta object to be converted to fractional SI years :return: The length of time covered by the time delta in units of fractional SI years """ return delta.total_seconds() / SI_SECONDS_PER_DAY / SI_DAYS_PER_YEAR
[docs]def datetime_to_mjd_years(date: datetime) -> float: """ This function converts a python datetime objects to the number of SI years since the MJD Epoch of November 17, 1858. This is computed by computing the time delta between the :attr:`.MJD_EPOCH` and the input datetime object, and then using :func:`.timedelta_to_si_years` to convert to the fractional years since the epoch. :param date: the python datetime object to be converted to MJD years :return: the number of SI years since November 17, 1858 """ return timedelta_to_si_years(date - MJD_EPOCH)
[docs]def apply_proper_motion(star_records: pd.DataFrame, new_time: Union[Real, datetime.datetime], copy: bool = True) -> pd.DataFrame: """ This function adjusts the right ascension and declination of stars to a new time. The right ascension and declination are updated using the corresponding proper motion of the stars. The formulation used here assumes constant linear velocity as described in section 1.2.8 of "The Hipparcos and Tycho2 Catalogues". The bearing measurement is converted to a unit vector, which is then updated using vector addition with the delta applied along the vectors of increasing right ascension and increasing declination. This model also allows for consideration of a radial velocity, but that is currently not implemented. The stars input into this method should be a pandas dataframe with the GIANT format. Specifically, this function requires the dataframe to have columns of ``['ra', 'dec', 'ra_proper_motion', 'dec_proper_motion', 'epoch']`` with units of degrees, degrees/year, and SI years (since January 1, 1) respectively. The updated bearing can be stored either in a copy of the dataframe, or in-place, depending on the ``copy`` key word argument. Either way the resulting dataframe is returned. The ``new_time`` parameter should either be a datetime object, or a float of the modified julian years for the desired time. The ``copy`` flag states whether to return a copy of the dataframe with the updates applied (recommended), or to make the updates in place. :param star_records: a pandas dataframe containing the bearing and proper motion of star records to be updated :param new_time: the new epoch to calculate the star positions at expressed as a mjy float or python datetime object :param copy: An option flag indicating whether to make a copy of star_records before applying proper motion :return: a pandas dataframe containing the star records with bearing values updated to the new epoch """ if copy: out = star_records.copy() else: out = star_records # convert the ra and dec values into radians ra0 = out['ra'].values * DEG2RAD # type: np.ndarray dec0 = out['dec'].values * DEG2RAD # type: np.ndarray # assume linear motion based on Hipparcos Vol 1 # compute the unit vector for each star r_unit = radec_to_unit(ra0, dec0).reshape(3, -1) # compute the unit vector in the direction of increasing right ascension p_unit = np.vstack([-np.sin(ra0), np.cos(ra0), np.zeros(ra0.shape)]) # compute the unit vector in the direction of increasing declination q_unit = np.vstack([-np.sin(dec0) * np.cos(ra0), -np.sin(dec0) * np.sin(ra0), np.cos(dec0)]) # compute the change in distance per year (future expansion) zeta0 = 0 start_time = out['epoch'].values # compute the time delta if isinstance(new_time, datetime.datetime): new_time = timedelta_to_si_years(new_time - datetime.datetime(1, 1, 1)) timedelta = new_time - start_time # compute the new direction vector r_unit_new = r_unit * (1 + zeta0 * timedelta) + (p_unit * out['ra_proper_motion'].values * DEG2RAD + q_unit * out['dec_proper_motion'].values * DEG2RAD) * timedelta r_unit_new /= np.linalg.norm(r_unit_new, axis=0, keepdims=True) # compute the new ra/dec and store it ra, dec = unit_to_radec(r_unit_new) out['ra'] = ra * RAD2DEG out['dec'] = dec * RAD2DEG # compute the new uncertainties for ra and dec out['ra_sigma'] = np.sqrt(out['ra_sigma'] ** 2 + timedelta ** 2 * out['ra_pm_sigma'] ** 2) out['dec_sigma'] = np.sqrt(out['dec_sigma'] ** 2 + timedelta ** 2 * out['dec_pm_sigma'] ** 2) # update the epochs out["epoch"] = new_time return out
[docs]def radec_distance(ra1: SCALAR_OR_ARRAY, dec1: SCALAR_OR_ARRAY, ra2: SCALAR_OR_ARRAY, dec2: SCALAR_OR_ARRAY) -> SCALAR_OR_ARRAY: r""" This function computes the great-circle angular distance in units of radians between ra/dec pairs. The distance is computed using .. math:: \text{cos}^{-1}\left(\text{cos}(\delta_1)\text{cos}(\delta_2)\text{cos}(\alpha_1-\alpha_2) + \text{sin}(\delta_1)\text{sin}(\delta_2)\right) where :math:`\delta_1` is the first declination, :math:`\delta_2` is the second declination, :math:`\alpha_1` is the first right ascension, and :math:`\alpha_2` is the second right ascension, all in radians. This function is vectorized and uses broadcasting rules, therefore you can specify the inputs as mixtures of scalars and arrays, as long as they can all be broadcast to a common shape. The output will be either a scalar (if all scalars are input) or an array if any of the inputs are an array. :param ra1: The right ascension values for the first parts of the pairs with units of radians :param dec1: The declination values for the first parts of the pairs with units of radians :param ra2: The right ascension values for the second parts of the pairs with units of radians :param dec2: The declination values for the second parts of the pairs with units of radians :return: The great circle angular distance between the points with units of radians """ return np.arccos(np.cos(dec1) * np.cos(dec2) * np.cos(ra1 - ra2) + np.sin(dec1) * np.sin(dec2))