Source code for harmonia.surveyor.coordinates

"""
Coordinates (:mod:`~harmonia.surveyor.coordinates`)
===========================================================================

Handle the survey coordinate systems.

.. autosummary::

    cartesian_to_spherical
    spherical_to_cartesian
    sky_to_spherical
    spherical_to_sky
    to_box_coords

|

"""
# pylint: disable=assignment-from-no-return
import warnings
from functools import wraps

import numpy as np


[docs]def cartesian_to_spherical(cartesian_coords): r"""Convert 3-d Cartesian coordinate arrays to spherical coordinate arrays. The coordinate transformation is given by .. math:: r = \sqrt{x^2 + y^2 + z^2} \,, \quad \theta = \arccos(z/r) \,, \quad \phi = \arctan(y/x) \,, where the image of :math:`\arccos` is :math:`[0, \pi]`, and :math:`\arctan` has an extended image set :math:`[0, 2\pi]`. Parameters ---------- cartesian_coords : float, array_like Cartesian coordinates. Returns ------- spherical_coords : float :class:`numpy.ndarray` Spherical coordinates. """ c_coords = np.atleast_2d(cartesian_coords) if not _is_coord_3d(c_coords): raise ValueError("`cartesian_coords` is not 3-d.") spherical_coords = np.zeros(c_coords.shape) spherical_coords[:, 0] = np.linalg.norm(c_coords, axis=1) spherical_coords[:, 1] = np.arccos(c_coords[:, 2] / spherical_coords[:, 0]) spherical_coords[:, 2] = np.mod( np.arctan2(c_coords[:, 1], c_coords[:, 0]), 2*np.pi ) return spherical_coords
[docs]def spherical_to_cartesian(spherical_coords): r"""Convert 3-d spherical coordinate arrays to Cartesian coordinate arrays. The coordinate transformation is given by .. math:: x = r \sin\theta \cos\phi \,, \quad y = r \sin\theta \sin\phi \,, \quad z = r \cos\theta \,. Parameters ---------- spherical_coords : float, array_like Spherical coordinates. Returns ------- cartesian_coords : float :class:`numpy.ndarray` Cartesian coordinates. """ s_coords = np.atleast_2d(spherical_coords) if not _is_coord_3d(s_coords): raise ValueError("`spherical_coords` is not 3-d.") cartesian_coords = np.zeros(s_coords.shape) cartesian_coords[:, 0] = np.sin(s_coords[:, 1]) * np.cos(s_coords[:, 2]) cartesian_coords[:, 1] = np.sin(s_coords[:, 1]) * np.sin(s_coords[:, 2]) cartesian_coords[:, 2] = np.cos(s_coords[:, 1]) cartesian_coords *= (s_coords[:, 0])[:, None] return cartesian_coords
[docs]def sky_to_spherical(sky_coords, z_to_r=None): r"""Convert 3-d (or 2-d) sky coordinate arrays (Z, DEC, RA) (or (DEC, RA)) to spherical coordinate arrays. The spherical surface coordinate transformation is given by .. math:: \theta = \frac{\pi}{180} (90 - \delta) \,, \quad \phi = \frac{\pi}{180} \alpha \,. where :math:`\delta` is the declination (DEC) and :math:`\alpha` the right ascension (RA) both given in degrees. The radial coordinate transformation from redshifts is given by :meth:`cosmo.comoving_distance` if the input coordinates are 3-d and a redshift-to-distance conversion function is provided. Parameters ---------- sky_coords : float, array_like Sky coordinates (2-d or 3-d). z_to_r : callable or None, optional Redshift-to-distance conversion (default is `None`). Returns ------- spherical_coords : float :class:`numpy.ndarray` Spherical surface coordinates. """ sky_coords = np.atleast_2d(sky_coords) if _is_coord_3d(sky_coords): if not callable(z_to_r): raise TypeError( "`z_to_r` must be provided for radial " "coordinate transformation." ) spherical_coords = np.zeros(sky_coords.shape) spherical_coords[:, 0] = z_to_r(sky_coords[:, 0]) spherical_coords[:, 1] = np.deg2rad(90 - sky_coords[:, 1]) spherical_coords[:, 2] = np.deg2rad(sky_coords[:, 2]) return spherical_coords if _is_coord_2d(sky_coords): spherical_coords = np.zeros(sky_coords.shape) spherical_coords[:, 0] = np.deg2rad(90 - sky_coords[:, 0]) spherical_coords[:, 1] = np.deg2rad(sky_coords[:, 1]) return spherical_coords raise ValueError("Check dimensions of input `sky_coords`.")
[docs]def spherical_to_sky(spherical_coords, z_from_r=None): r"""Convert 3-d spherical coordinate arrays to sky coordinate arrays. The spherical surface coordinate transformation is given by .. math:: \delta = 90 - \frac{180}{\pi} \theta \,, \quad \alpha = \frac{180}{\pi} \phi \,. where :math:`\delta` is the declination (DEC) and :math:`\alpha` the right ascension (RA) both given in degrees. The radial coordinate transformation is given by a distance-to-redshift conversion if it provided. Parameters ---------- sky_coords : float, array_like Sky coordinates. z_from_r : callable or None, optional Distance-to-redshift conversion (default is `None`). Returns ------- sky_coords : float :class:`numpy.ndarray` Sky coordinates. """ spherical_coords = np.atleast_2d(spherical_coords) if not _is_coord_3d(spherical_coords): raise ValueError("`spherical_coords` is not 3-d.") sky_coords = np.zeros(spherical_coords.shape) sky_coords[:, 1] = 90 - np.rad2deg(spherical_coords[:, 1]) sky_coords[:, 2] = np.rad2deg(spherical_coords[:, 2]) if z_from_r is None: sky_coords = np.delete(sky_coords, 0, axis=1) return sky_coords sky_coords[:, 0] = z_from_r(spherical_coords[:, 0]) return sky_coords
def _is_coord_2d(coords): try: return np.size(coords, axis=-1) == 2 except IndexError: # scalars return False def _is_coord_3d(coords): try: return np.size(coords, axis=-1) == 3 except IndexError: # scalars return False # pylint: disable=unused-argument
[docs]def to_box_coords(native_coord_system, box_centre=None, conversion_kwargs=None): """Convert a function defined for a native 3-d curvilinear coordinate system to an equivalent function defined for a box in Cartesian coordinates. Parameters ---------- native_coord_system : {'null', 'spherical', 'sky'}, optional Native coordinate system of the function. If 'cartesian' (with the implicit assumption that the origin is at the box centre), the function is unconverted unless `box_shift` is also provided. box_centre : float, array_like or Nonw If provided, translate the box coordinates so that the (positive) `box_centre` is moved to(0, 0, 0). This is needed to when the wrapped function needs to accept box coordinates with the origin placed at its corner whilst the `native_coord_system` has the origin at the centre of the box (e.g. 'spherical'). conversion_kwargs : dict or None, optional Additional parameters to use in conversion, e.g. `z_from_r` if `native_coords` is 'sky' (default is `None`). Returns ------- callable The original function now accepting Cartesian coordinates. """ def decorator(func): @wraps(func) def wrapper(*coord_arrs, **kwargs): nonlocal native_coord_system # Offer the option to do nothing. if box_centre is None and native_coord_system == 'null': return func(*coord_arrs, **kwargs) native_coord_system = 'cartesian' \ if native_coord_system == 'null' \ else native_coord_system if len(coord_arrs) == 1: unpacked = False elif len(coord_arrs) == 3: unpacked = True else: raise ValueError( "Wrapped coordinate function must only have positional " "arguments that are 3-d coordinate arrays." ) if unpacked: coords = np.column_stack(coord_arrs) else: coords, = coord_arrs if box_centre is None: cart_coords = np.atleast_2d(coords) else: if np.any(np.sign(box_centre) < 0): warnings.warn( "`box_shift` is usually positive to ensure " "the origin of the Cartesian coordinates " "is at a corner of the box." ) cart_coords = np.subtract(coords, box_centre) if native_coord_system == 'cartesian': native_coords = cart_coords elif native_coord_system == 'spherical': native_coords = cartesian_to_spherical(cart_coords) elif native_coord_system == 'sky': native_coords = spherical_to_sky( cartesian_to_spherical(cart_coords), **conversion_kwargs ) else: raise ValueError( "Unsupported `native_coord_system`: " f"{native_coord_system}." ) if unpacked: native_coord_arrs = np.hsplit(native_coords, 3) else: native_coord_arrs = (native_coords,) return func(*native_coord_arrs, **kwargs) return wrapper return decorator