# Source code for gwcs.geometry

```# Licensed under a 3-clause BSD style license - see LICENSE.rst
"""
Models for general analytical geometry transformations.
"""

import numbers
import numpy as np
from astropy.modeling.core import Model
from astropy import units as u

__all__ = ['ToDirectionCosines', 'FromDirectionCosines',
'SphericalToCartesian', 'CartesianToSpherical']

[docs]
class ToDirectionCosines(Model):
"""
Transform a vector to direction cosines.
"""
_separable = False

n_inputs = 3
n_outputs = 4

def __init__(self, **kwargs):
super().__init__(**kwargs)
self.inputs = ('x', 'y', 'z')
self.outputs = ('cosa', 'cosb', 'cosc', 'length')

[docs]
def evaluate(self, x, y, z):
vabs = np.sqrt(1. + x**2 + y**2)
cosa = x / vabs
cosb = y / vabs
cosc = 1. / vabs
return cosa, cosb, cosc, vabs

def inverse(self):
return FromDirectionCosines()

[docs]
class FromDirectionCosines(Model):
"""
Transform directional cosines to vector.
"""
_separable = False

n_inputs = 4
n_outputs = 3

def __init__(self, **kwargs):
super().__init__(**kwargs)
self.inputs = ('cosa', 'cosb', 'cosc', 'length')
self.outputs = ('x', 'y', 'z')

[docs]
def evaluate(self, cosa, cosb, cosc, length):
return cosa * length, cosb * length, cosc * length

def inverse(self):

[docs]
class SphericalToCartesian(Model):
"""
Convert spherical coordinates on a unit sphere to cartesian coordinates.
Spherical coordinates when not provided as ``Quantity`` are assumed
to be in degrees with ``lon`` being the *longitude (or azimuthal) angle*
``[0, 360)`` (or ``[-180, 180)``) and angle ``lat`` is the *latitude*
(or *elevation angle*) in range ``[-90, 90]``.

"""
_separable = False

_input_units_allow_dimensionless = True

n_inputs = 2
n_outputs = 3

def __init__(self, wrap_lon_at=360, **kwargs):
"""
Parameters
----------
wrap_lon_at : {360, 180}, optional
An **integer number** that specifies the range of the longitude
(azimuthal) angle. When ``wrap_lon_at`` is 180, the longitude angle
will have a range of ``[-180, 180)`` and when ``wrap_lon_at``
is 360 (default), the longitude angle will have a range of
``[0, 360)``.

"""
super().__init__(**kwargs)
self.inputs = ('lon', 'lat')
self.outputs = ('x', 'y', 'z')
self.wrap_lon_at = wrap_lon_at

@property
def wrap_lon_at(self):
""" An **integer number** that specifies the range of the longitude
(azimuthal) angle.

Allowed values are 180 and 360. When ``wrap_lon_at``
is 180, the longitude angle will have a range of ``[-180, 180)`` and
when ``wrap_lon_at`` is 360 (default), the longitude angle will have a
range of ``[0, 360)``.

"""
return self._wrap_lon_at

@wrap_lon_at.setter
def wrap_lon_at(self, wrap_angle):
if not (isinstance(wrap_angle, numbers.Integral) and wrap_angle in [180, 360]):
raise ValueError("'wrap_lon_at' must be an integer number: 180 or 360")
self._wrap_lon_at = wrap_angle

[docs]
def evaluate(self, lon, lat):
if isinstance(lon, u.Quantity) != isinstance(lat, u.Quantity):
raise TypeError("All arguments must be of the same type "
"(i.e., quantity or not).")

cs = np.cos(lat)
x = cs * np.cos(lon)
y = cs * np.sin(lon)
z = np.sin(lat)

return x, y, z

def inverse(self):
return CartesianToSpherical(wrap_lon_at=self._wrap_lon_at)

@property
def input_units(self):
return {'lon': u.deg, 'lat': u.deg}

[docs]
class CartesianToSpherical(Model):
"""
Convert cartesian coordinates to spherical coordinates on a unit sphere.
Output spherical coordinates are in degrees. When input cartesian
coordinates are quantities (``Quantity`` objects), output angles
will also be quantities in degrees. Angle ``lon`` is the *longitude*
(or *azimuthal angle*) in range ``[0, 360)`` (or ``[-180, 180)``) and
angle ``lat`` is the *latitude* (or *elevation angle*) in the
range ``[-90, 90]``.

"""
_separable = False

_input_units_allow_dimensionless = True

n_inputs = 3
n_outputs = 2

def __init__(self, wrap_lon_at=360, **kwargs):
"""
Parameters
----------
wrap_lon_at : {360, 180}, optional
An **integer number** that specifies the range of the longitude
(azimuthal) angle. When ``wrap_lon_at`` is 180, the longitude angle
will have a range of ``[-180, 180)`` and when ``wrap_lon_at``
is 360 (default), the longitude angle will have a range of
``[0, 360)``.

"""
super().__init__(**kwargs)
self.inputs = ('x', 'y', 'z')
self.outputs = ('lon', 'lat')
self.wrap_lon_at = wrap_lon_at

@property
def wrap_lon_at(self):
""" An **integer number** that specifies the range of the longitude
(azimuthal) angle.

Allowed values are 180 and 360. When ``wrap_lon_at``
is 180, the longitude angle will have a range of ``[-180, 180)`` and
when ``wrap_lon_at`` is 360 (default), the longitude angle will have a
range of ``[0, 360)``.

"""
return self._wrap_lon_at

@wrap_lon_at.setter
def wrap_lon_at(self, wrap_angle):
if not (isinstance(wrap_angle, numbers.Integral) and wrap_angle in [180, 360]):
raise ValueError("'wrap_lon_at' must be an integer number: 180 or 360")
self._wrap_lon_at = wrap_angle

[docs]
def evaluate(self, x, y, z):
nquant = [isinstance(i, u.Quantity) for i in (x, y, z)].count(True)
if nquant in [1, 2]:
raise TypeError("All arguments must be of the same type "
"(i.e., quantity or not).")

h = np.hypot(x, y)