1# Licensed under a 3-clause BSD style license - see LICENSE.rst
2# -*- coding: utf-8 -*-
3# pylint: disable=invalid-name
4"""
5Implements projections--particularly sky projections defined in WCS Paper II
6[1]_.
7
8All angles are set and and displayed in degrees but internally computations are
9performed in radians. All functions expect inputs and outputs degrees.
10
11References
12----------
13.. [1] Calabretta, M.R., Greisen, E.W., 2002, A&A, 395, 1077 (Paper II)
14"""
15
16
17import abc
18
19import numpy as np
20
21from astropy import units as u
22
23from .core import Model
24from .parameters import Parameter, InputParameterError
25from . import _projections
26from .utils import _to_radian, _to_orig_unit
27
28
29projcodes = [
30    'AZP', 'SZP', 'TAN', 'STG', 'SIN', 'ARC', 'ZEA', 'AIR', 'CYP',
31    'CEA', 'CAR', 'MER', 'SFL', 'PAR', 'MOL', 'AIT', 'COP', 'COE',
32    'COD', 'COO', 'BON', 'PCO', 'TSC', 'CSC', 'QSC', 'HPX', 'XPH'
33]
34
35
36__all__ = ['Projection', 'Pix2SkyProjection', 'Sky2PixProjection',
37           'Zenithal', 'Cylindrical', 'PseudoCylindrical', 'Conic',
38           'PseudoConic', 'QuadCube', 'HEALPix',
39           'AffineTransformation2D',
40           'projcodes',
41
42           'Pix2Sky_ZenithalPerspective', 'Sky2Pix_ZenithalPerspective',
43           'Pix2Sky_SlantZenithalPerspective', 'Sky2Pix_SlantZenithalPerspective',
44           'Pix2Sky_Gnomonic', 'Sky2Pix_Gnomonic',
45           'Pix2Sky_Stereographic', 'Sky2Pix_Stereographic',
46           'Pix2Sky_SlantOrthographic', 'Sky2Pix_SlantOrthographic',
47           'Pix2Sky_ZenithalEquidistant', 'Sky2Pix_ZenithalEquidistant',
48           'Pix2Sky_ZenithalEqualArea', 'Sky2Pix_ZenithalEqualArea',
49           'Pix2Sky_Airy', 'Sky2Pix_Airy',
50           'Pix2Sky_CylindricalPerspective', 'Sky2Pix_CylindricalPerspective',
51           'Pix2Sky_CylindricalEqualArea', 'Sky2Pix_CylindricalEqualArea',
52           'Pix2Sky_PlateCarree', 'Sky2Pix_PlateCarree',
53           'Pix2Sky_Mercator', 'Sky2Pix_Mercator',
54           'Pix2Sky_SansonFlamsteed', 'Sky2Pix_SansonFlamsteed',
55           'Pix2Sky_Parabolic', 'Sky2Pix_Parabolic',
56           'Pix2Sky_Molleweide', 'Sky2Pix_Molleweide',
57           'Pix2Sky_HammerAitoff', 'Sky2Pix_HammerAitoff',
58           'Pix2Sky_ConicPerspective', 'Sky2Pix_ConicPerspective',
59           'Pix2Sky_ConicEqualArea', 'Sky2Pix_ConicEqualArea',
60           'Pix2Sky_ConicEquidistant', 'Sky2Pix_ConicEquidistant',
61           'Pix2Sky_ConicOrthomorphic', 'Sky2Pix_ConicOrthomorphic',
62           'Pix2Sky_BonneEqualArea', 'Sky2Pix_BonneEqualArea',
63           'Pix2Sky_Polyconic', 'Sky2Pix_Polyconic',
64           'Pix2Sky_TangentialSphericalCube', 'Sky2Pix_TangentialSphericalCube',
65           'Pix2Sky_COBEQuadSphericalCube', 'Sky2Pix_COBEQuadSphericalCube',
66           'Pix2Sky_QuadSphericalCube', 'Sky2Pix_QuadSphericalCube',
67           'Pix2Sky_HEALPix', 'Sky2Pix_HEALPix',
68           'Pix2Sky_HEALPixPolar', 'Sky2Pix_HEALPixPolar',
69
70           # The following are short FITS WCS aliases
71           'Pix2Sky_AZP', 'Sky2Pix_AZP',
72           'Pix2Sky_SZP', 'Sky2Pix_SZP',
73           'Pix2Sky_TAN', 'Sky2Pix_TAN',
74           'Pix2Sky_STG', 'Sky2Pix_STG',
75           'Pix2Sky_SIN', 'Sky2Pix_SIN',
76           'Pix2Sky_ARC', 'Sky2Pix_ARC',
77           'Pix2Sky_ZEA', 'Sky2Pix_ZEA',
78           'Pix2Sky_AIR', 'Sky2Pix_AIR',
79           'Pix2Sky_CYP', 'Sky2Pix_CYP',
80           'Pix2Sky_CEA', 'Sky2Pix_CEA',
81           'Pix2Sky_CAR', 'Sky2Pix_CAR',
82           'Pix2Sky_MER', 'Sky2Pix_MER',
83           'Pix2Sky_SFL', 'Sky2Pix_SFL',
84           'Pix2Sky_PAR', 'Sky2Pix_PAR',
85           'Pix2Sky_MOL', 'Sky2Pix_MOL',
86           'Pix2Sky_AIT', 'Sky2Pix_AIT',
87           'Pix2Sky_COP', 'Sky2Pix_COP',
88           'Pix2Sky_COE', 'Sky2Pix_COE',
89           'Pix2Sky_COD', 'Sky2Pix_COD',
90           'Pix2Sky_COO', 'Sky2Pix_COO',
91           'Pix2Sky_BON', 'Sky2Pix_BON',
92           'Pix2Sky_PCO', 'Sky2Pix_PCO',
93           'Pix2Sky_TSC', 'Sky2Pix_TSC',
94           'Pix2Sky_CSC', 'Sky2Pix_CSC',
95           'Pix2Sky_QSC', 'Sky2Pix_QSC',
96           'Pix2Sky_HPX', 'Sky2Pix_HPX',
97           'Pix2Sky_XPH', 'Sky2Pix_XPH'
98           ]
99
100
101class Projection(Model):
102    """Base class for all sky projections."""
103
104    # Radius of the generating sphere.
105    # This sets the circumference to 360 deg so that arc length is measured in deg.
106    r0 = 180 * u.deg / np.pi
107
108    _separable = False
109
110    @property
111    @abc.abstractmethod
112    def inverse(self):
113        """
114        Inverse projection--all projection models must provide an inverse.
115        """
116
117
118class Pix2SkyProjection(Projection):
119    """Base class for all Pix2Sky projections."""
120
121    n_inputs = 2
122    n_outputs = 2
123
124    _input_units_strict = True
125    _input_units_allow_dimensionless = True
126
127    def __init__(self, *args, **kwargs):
128        super().__init__(*args, **kwargs)
129        self.inputs = ('x', 'y')
130        self.outputs = ('phi', 'theta')
131
132    @property
133    def input_units(self):
134        return {self.inputs[0]: u.deg,
135                self.inputs[1]: u.deg}
136
137    @property
138    def return_units(self):
139        return {self.outputs[0]: u.deg,
140                self.outputs[1]: u.deg}
141
142
143class Sky2PixProjection(Projection):
144    """Base class for all Sky2Pix projections."""
145
146    n_inputs = 2
147    n_outputs = 2
148
149    _input_units_strict = True
150    _input_units_allow_dimensionless = True
151
152    def __init__(self, *args, **kwargs):
153        super().__init__(*args, **kwargs)
154        self.inputs = ('phi', 'theta')
155        self.outputs = ('x', 'y')
156
157    @property
158    def input_units(self):
159        return {self.inputs[0]: u.deg,
160                self.inputs[1]: u.deg}
161
162    @property
163    def return_units(self):
164        return {self.outputs[0]: u.deg,
165                self.outputs[1]: u.deg}
166
167
168class Zenithal(Projection):
169    r"""Base class for all Zenithal projections.
170
171    Zenithal (or azimuthal) projections map the sphere directly onto a
172    plane.  All zenithal projections are specified by defining the
173    radius as a function of native latitude, :math:`R_\theta`.
174
175    The pixel-to-sky transformation is defined as:
176
177    .. math::
178        \phi &= \arg(-y, x) \\
179        R_\theta &= \sqrt{x^2 + y^2}
180
181    and the inverse (sky-to-pixel) is defined as:
182
183    .. math::
184        x &= R_\theta \sin \phi \\
185        y &= R_\theta \cos \phi
186    """
187
188    _separable = False
189
190
191class Pix2Sky_ZenithalPerspective(Pix2SkyProjection, Zenithal):
192    r"""
193    Zenithal perspective projection - pixel to sky.
194
195    Corresponds to the ``AZP`` projection in FITS WCS.
196
197    .. math::
198        \phi &= \arg(-y \cos \gamma, x) \\
199        \theta &= \left\{\genfrac{}{}{0pt}{}{\psi - \omega}{\psi + \omega + 180^{\circ}}\right.
200
201    where:
202
203    .. math::
204        \psi &= \arg(\rho, 1) \\
205        \omega &= \sin^{-1}\left(\frac{\rho \mu}{\sqrt{\rho^2 + 1}}\right) \\
206        \rho &= \frac{R}{\frac{180^{\circ}}{\pi}(\mu + 1) + y \sin \gamma} \\
207        R &= \sqrt{x^2 + y^2 \cos^2 \gamma}
208
209    Parameters
210    --------------
211    mu : float
212        Distance from point of projection to center of sphere
213        in spherical radii, μ.  Default is 0.
214
215    gamma : float
216        Look angle γ in degrees.  Default is 0°.
217    """
218
219    mu = Parameter(default=0.0,
220                   description="Distance from point of projection to center of sphere")
221    gamma = Parameter(default=0.0, getter=_to_orig_unit, setter=_to_radian,
222                      description="Look angle γ in degrees (Default = 0°)")
223
224    def __init__(self, mu=mu.default, gamma=gamma.default, **kwargs):
225        # units : mu - in spherical radii, gamma - in deg
226        super().__init__(mu, gamma, **kwargs)
227
228    @mu.validator
229    def mu(self, value):
230        if np.any(value == -1):
231            raise InputParameterError(
232                "Zenithal perspective projection is not defined for mu = -1")
233
234    @property
235    def inverse(self):
236        return Sky2Pix_ZenithalPerspective(self.mu.value, self.gamma.value)
237
238    @classmethod
239    def evaluate(cls, x, y, mu, gamma):
240        return _projections.azpx2s(x, y, mu, _to_orig_unit(gamma))
241
242
243Pix2Sky_AZP = Pix2Sky_ZenithalPerspective
244
245
246class Sky2Pix_ZenithalPerspective(Sky2PixProjection, Zenithal):
247    r"""
248    Zenithal perspective projection - sky to pixel.
249
250    Corresponds to the ``AZP`` projection in FITS WCS.
251
252    .. math::
253        x &= R \sin \phi \\
254        y &= -R \sec \gamma \cos \theta
255
256    where:
257
258    .. math::
259        R = \frac{180^{\circ}}{\pi} \frac{(\mu + 1) \cos \theta}{(\mu + \sin \theta) + \cos \theta \cos \phi \tan \gamma}
260
261    Parameters
262    ----------
263    mu : float
264        Distance from point of projection to center of sphere
265        in spherical radii, μ. Default is 0.
266
267    gamma : float
268        Look angle γ in degrees. Default is 0°.
269    """
270
271    mu = Parameter(default=0.0, description="Distance from point of projection to center of sphere")
272    gamma = Parameter(default=0.0, getter=_to_orig_unit, setter=_to_radian, description="Look angle γ in degrees (Default=0°)")
273
274    @mu.validator
275    def mu(self, value):
276        if np.any(value == -1):
277            raise InputParameterError(
278                "Zenithal perspective projection is not defined for mu = -1")
279
280    @property
281    def inverse(self):
282        return Pix2Sky_AZP(self.mu.value, self.gamma.value)
283
284    @classmethod
285    def evaluate(cls, phi, theta, mu, gamma):
286        return _projections.azps2x(
287            phi, theta, mu, _to_orig_unit(gamma))
288
289
290Sky2Pix_AZP = Sky2Pix_ZenithalPerspective
291
292
293class Pix2Sky_SlantZenithalPerspective(Pix2SkyProjection, Zenithal):
294    r"""
295    Slant zenithal perspective projection - pixel to sky.
296
297    Corresponds to the ``SZP`` projection in FITS WCS.
298
299    Parameters
300    --------------
301    mu : float
302        Distance from point of projection to center of sphere
303        in spherical radii, μ.  Default is 0.
304
305    phi0 : float
306        The longitude φ₀ of the reference point, in degrees.  Default
307        is 0°.
308
309    theta0 : float
310        The latitude θ₀ of the reference point, in degrees.  Default
311        is 90°.
312    """
313
314    def _validate_mu(mu):
315        if np.asarray(mu == -1).any():
316            raise InputParameterError(
317                "Zenithal perspective projection is not defined for mu = -1")
318        return mu
319
320    mu = Parameter(default=0.0, setter=_validate_mu, description="Distance from point of projection to center of sphere")
321    phi0 = Parameter(default=0.0, getter=_to_orig_unit, setter=_to_radian, description="The longitude φ₀ of the reference point in degrees (Default=0°)")
322    theta0 = Parameter(default=90.0, getter=_to_orig_unit, setter=_to_radian, description="The latitude θ₀ of the reference point, in degrees (Default=0°)")
323
324    @property
325    def inverse(self):
326        return Sky2Pix_SlantZenithalPerspective(
327            self.mu.value, self.phi0.value, self.theta0.value)
328
329    @classmethod
330    def evaluate(cls, x, y, mu, phi0, theta0):
331        return _projections.szpx2s(
332            x, y, mu, _to_orig_unit(phi0), _to_orig_unit(theta0))
333
334
335Pix2Sky_SZP = Pix2Sky_SlantZenithalPerspective
336
337
338class Sky2Pix_SlantZenithalPerspective(Sky2PixProjection, Zenithal):
339    r"""
340    Zenithal perspective projection - sky to pixel.
341
342    Corresponds to the ``SZP`` projection in FITS WCS.
343
344    Parameters
345    ----------
346    mu : float
347        distance from point of projection to center of sphere
348        in spherical radii, μ.  Default is 0.
349
350    phi0 : float
351        The longitude φ₀ of the reference point, in degrees.  Default
352        is 0°.
353
354    theta0 : float
355        The latitude θ₀ of the reference point, in degrees.  Default
356        is 90°.
357    """
358
359    def _validate_mu(mu):
360        if np.asarray(mu == -1).any():
361            raise InputParameterError("Zenithal perspective projection is not defined for mu = -1")
362        return mu
363
364    mu = Parameter(default=0.0, setter=_validate_mu,
365                   description="Distance from point of projection to center of sphere")
366    phi0 = Parameter(default=0.0, getter=_to_orig_unit, setter=_to_radian,
367                     description="The longitude φ₀ of the reference point in degrees")
368    theta0 = Parameter(default=0.0, getter=_to_orig_unit, setter=_to_radian,
369                       description="The latitude θ₀ of the reference point, in degrees")
370
371    @property
372    def inverse(self):
373        return Pix2Sky_SlantZenithalPerspective(
374            self.mu.value, self.phi0.value, self.theta0.value)
375
376    @classmethod
377    def evaluate(cls, phi, theta, mu, phi0, theta0):
378        return _projections.szps2x(
379            phi, theta, mu, _to_orig_unit(phi0), _to_orig_unit(theta0))
380
381
382Sky2Pix_SZP = Sky2Pix_SlantZenithalPerspective
383
384
385class Pix2Sky_Gnomonic(Pix2SkyProjection, Zenithal):
386    r"""
387    Gnomonic projection - pixel to sky.
388
389    Corresponds to the ``TAN`` projection in FITS WCS.
390
391    See `Zenithal` for a definition of the full transformation.
392
393    .. math::
394        \theta = \tan^{-1}\left(\frac{180^{\circ}}{\pi R_\theta}\right)
395    """
396
397    @property
398    def inverse(self):
399        return Sky2Pix_Gnomonic()
400
401    @classmethod
402    def evaluate(cls, x, y):
403        return _projections.tanx2s(x, y)
404
405
406Pix2Sky_TAN = Pix2Sky_Gnomonic
407
408
409class Sky2Pix_Gnomonic(Sky2PixProjection, Zenithal):
410    r"""
411    Gnomonic Projection - sky to pixel.
412
413    Corresponds to the ``TAN`` projection in FITS WCS.
414
415    See `Zenithal` for a definition of the full transformation.
416
417    .. math::
418        R_\theta = \frac{180^{\circ}}{\pi}\cot \theta
419    """
420
421    @property
422    def inverse(self):
423        return Pix2Sky_Gnomonic()
424
425    @classmethod
426    def evaluate(cls, phi, theta):
427        return _projections.tans2x(phi, theta)
428
429
430Sky2Pix_TAN = Sky2Pix_Gnomonic
431
432
433class Pix2Sky_Stereographic(Pix2SkyProjection, Zenithal):
434    r"""
435    Stereographic Projection - pixel to sky.
436
437    Corresponds to the ``STG`` projection in FITS WCS.
438
439    See `Zenithal` for a definition of the full transformation.
440
441    .. math::
442        \theta = 90^{\circ} - 2 \tan^{-1}\left(\frac{\pi R_\theta}{360^{\circ}}\right)
443    """
444
445    @property
446    def inverse(self):
447        return Sky2Pix_Stereographic()
448
449    @classmethod
450    def evaluate(cls, x, y):
451        return _projections.stgx2s(x, y)
452
453
454Pix2Sky_STG = Pix2Sky_Stereographic
455
456
457class Sky2Pix_Stereographic(Sky2PixProjection, Zenithal):
458    r"""
459    Stereographic Projection - sky to pixel.
460
461    Corresponds to the ``STG`` projection in FITS WCS.
462
463    See `Zenithal` for a definition of the full transformation.
464
465    .. math::
466        R_\theta = \frac{180^{\circ}}{\pi}\frac{2 \cos \theta}{1 + \sin \theta}
467    """
468
469    @property
470    def inverse(self):
471        return Pix2Sky_Stereographic()
472
473    @classmethod
474    def evaluate(cls, phi, theta):
475        return _projections.stgs2x(phi, theta)
476
477
478Sky2Pix_STG = Sky2Pix_Stereographic
479
480
481class Pix2Sky_SlantOrthographic(Pix2SkyProjection, Zenithal):
482    r"""
483    Slant orthographic projection - pixel to sky.
484
485    Corresponds to the ``SIN`` projection in FITS WCS.
486
487    See `Zenithal` for a definition of the full transformation.
488
489    The following transformation applies when :math:`\xi` and
490    :math:`\eta` are both zero.
491
492    .. math::
493        \theta = \cos^{-1}\left(\frac{\pi}{180^{\circ}}R_\theta\right)
494
495    The parameters :math:`\xi` and :math:`\eta` are defined from the
496    reference point :math:`(\phi_c, \theta_c)` as:
497
498    .. math::
499        \xi &= \cot \theta_c \sin \phi_c \\
500        \eta &= - \cot \theta_c \cos \phi_c
501
502    Parameters
503    ----------
504    xi : float
505        Obliqueness parameter, ξ.  Default is 0.0.
506
507    eta : float
508        Obliqueness parameter, η.  Default is 0.0.
509    """
510
511    xi = Parameter(default=0.0, description="Obliqueness parameter")
512    eta = Parameter(default=0.0, description="Obliqueness parameter")
513
514    @property
515    def inverse(self):
516        return Sky2Pix_SlantOrthographic(self.xi.value, self.eta.value)
517
518    @classmethod
519    def evaluate(cls, x, y, xi, eta):
520        return _projections.sinx2s(x, y, xi, eta)
521
522
523Pix2Sky_SIN = Pix2Sky_SlantOrthographic
524
525
526class Sky2Pix_SlantOrthographic(Sky2PixProjection, Zenithal):
527    r"""
528    Slant orthographic projection - sky to pixel.
529
530    Corresponds to the ``SIN`` projection in FITS WCS.
531
532    See `Zenithal` for a definition of the full transformation.
533
534    The following transformation applies when :math:`\xi` and
535    :math:`\eta` are both zero.
536
537    .. math::
538        R_\theta = \frac{180^{\circ}}{\pi}\cos \theta
539
540    But more specifically are:
541
542    .. math::
543        x &= \frac{180^\circ}{\pi}[\cos \theta \sin \phi + \xi(1 - \sin \theta)] \\
544        y &= \frac{180^\circ}{\pi}[\cos \theta \cos \phi + \eta(1 - \sin \theta)]
545    """
546
547    xi = Parameter(default=0.0)
548    eta = Parameter(default=0.0)
549
550    @property
551    def inverse(self):
552        return Pix2Sky_SlantOrthographic(self.xi.value, self.eta.value)
553
554    @classmethod
555    def evaluate(cls, phi, theta, xi, eta):
556        return _projections.sins2x(phi, theta, xi, eta)
557
558
559Sky2Pix_SIN = Sky2Pix_SlantOrthographic
560
561
562class Pix2Sky_ZenithalEquidistant(Pix2SkyProjection, Zenithal):
563    r"""
564    Zenithal equidistant projection - pixel to sky.
565
566    Corresponds to the ``ARC`` projection in FITS WCS.
567
568    See `Zenithal` for a definition of the full transformation.
569
570    .. math::
571        \theta = 90^\circ - R_\theta
572    """
573    @property
574    def inverse(self):
575        return Sky2Pix_ZenithalEquidistant()
576
577    @classmethod
578    def evaluate(cls, x, y):
579        return _projections.arcx2s(x, y)
580
581
582Pix2Sky_ARC = Pix2Sky_ZenithalEquidistant
583
584
585class Sky2Pix_ZenithalEquidistant(Sky2PixProjection, Zenithal):
586    r"""
587    Zenithal equidistant projection - sky to pixel.
588
589    Corresponds to the ``ARC`` projection in FITS WCS.
590
591    See `Zenithal` for a definition of the full transformation.
592
593    .. math::
594        R_\theta = 90^\circ - \theta
595    """
596    @property
597    def inverse(self):
598        return Pix2Sky_ZenithalEquidistant()
599
600    @classmethod
601    def evaluate(cls, phi, theta):
602        return _projections.arcs2x(phi, theta)
603
604
605Sky2Pix_ARC = Sky2Pix_ZenithalEquidistant
606
607
608class Pix2Sky_ZenithalEqualArea(Pix2SkyProjection, Zenithal):
609    r"""
610    Zenithal equidistant projection - pixel to sky.
611
612    Corresponds to the ``ZEA`` projection in FITS WCS.
613
614    See `Zenithal` for a definition of the full transformation.
615
616    .. math::
617        \theta = 90^\circ - 2 \sin^{-1} \left(\frac{\pi R_\theta}{360^\circ}\right)
618    """
619    @property
620    def inverse(self):
621        return Sky2Pix_ZenithalEqualArea()
622
623    @classmethod
624    def evaluate(cls, x, y):
625        return _projections.zeax2s(x, y)
626
627
628Pix2Sky_ZEA = Pix2Sky_ZenithalEqualArea
629
630
631class Sky2Pix_ZenithalEqualArea(Sky2PixProjection, Zenithal):
632    r"""
633    Zenithal equidistant projection - sky to pixel.
634
635    Corresponds to the ``ZEA`` projection in FITS WCS.
636
637    See `Zenithal` for a definition of the full transformation.
638
639    .. math::
640        R_\theta &= \frac{180^\circ}{\pi} \sqrt{2(1 - \sin\theta)} \\
641                 &= \frac{360^\circ}{\pi} \sin\left(\frac{90^\circ - \theta}{2}\right)
642    """
643    @property
644    def inverse(self):
645        return Pix2Sky_ZenithalEqualArea()
646
647    @classmethod
648    def evaluate(cls, phi, theta):
649        return _projections.zeas2x(phi, theta)
650
651
652Sky2Pix_ZEA = Sky2Pix_ZenithalEqualArea
653
654
655class Pix2Sky_Airy(Pix2SkyProjection, Zenithal):
656    r"""
657    Airy projection - pixel to sky.
658
659    Corresponds to the ``AIR`` projection in FITS WCS.
660
661    See `Zenithal` for a definition of the full transformation.
662
663    Parameters
664    ----------
665    theta_b : float
666        The latitude :math:`\theta_b` at which to minimize the error,
667        in degrees.  Default is 90°.
668    """
669    theta_b = Parameter(default=90.0)
670
671    @property
672    def inverse(self):
673        return Sky2Pix_Airy(self.theta_b.value)
674
675    @classmethod
676    def evaluate(cls, x, y, theta_b):
677        return _projections.airx2s(x, y, theta_b)
678
679
680Pix2Sky_AIR = Pix2Sky_Airy
681
682
683class Sky2Pix_Airy(Sky2PixProjection, Zenithal):
684    r"""
685    Airy - sky to pixel.
686
687    Corresponds to the ``AIR`` projection in FITS WCS.
688
689    See `Zenithal` for a definition of the full transformation.
690
691    .. math::
692        R_\theta = -2 \frac{180^\circ}{\pi}\left(\frac{\ln(\cos \xi)}{\tan \xi} + \frac{\ln(\cos \xi_b)}{\tan^2 \xi_b} \tan \xi \right)
693
694    where:
695
696    .. math::
697        \xi &= \frac{90^\circ - \theta}{2} \\
698        \xi_b &= \frac{90^\circ - \theta_b}{2}
699
700    Parameters
701    ----------
702    theta_b : float
703        The latitude :math:`\theta_b` at which to minimize the error,
704        in degrees.  Default is 90°.
705    """
706    theta_b = Parameter(default=90.0, description="The latitude at which to minimize the error,in degrees")
707
708    @property
709    def inverse(self):
710        return Pix2Sky_Airy(self.theta_b.value)
711
712    @classmethod
713    def evaluate(cls, phi, theta, theta_b):
714        return _projections.airs2x(phi, theta, theta_b)
715
716
717Sky2Pix_AIR = Sky2Pix_Airy
718
719
720class Cylindrical(Projection):
721    r"""Base class for Cylindrical projections.
722
723    Cylindrical projections are so-named because the surface of
724    projection is a cylinder.
725    """
726    _separable = True
727
728
729class Pix2Sky_CylindricalPerspective(Pix2SkyProjection, Cylindrical):
730    r"""
731    Cylindrical perspective - pixel to sky.
732
733    Corresponds to the ``CYP`` projection in FITS WCS.
734
735    .. math::
736        \phi &= \frac{x}{\lambda} \\
737        \theta &= \arg(1, \eta) + \sin{-1}\left(\frac{\eta \mu}{\sqrt{\eta^2 + 1}}\right)
738
739    where:
740
741    .. math::
742        \eta = \frac{\pi}{180^{\circ}}\frac{y}{\mu + \lambda}
743
744    Parameters
745    ----------
746    mu : float
747        Distance from center of sphere in the direction opposite the
748        projected surface, in spherical radii, μ. Default is 1.
749
750    lam : float
751        Radius of the cylinder in spherical radii, λ. Default is 1.
752    """
753
754    mu = Parameter(default=1.0)
755    lam = Parameter(default=1.0)
756
757    @mu.validator
758    def mu(self, value):
759        if np.any(value == -self.lam):
760            raise InputParameterError(
761                "CYP projection is not defined for mu = -lambda")
762
763    @lam.validator
764    def lam(self, value):
765        if np.any(value == -self.mu):
766            raise InputParameterError(
767                "CYP projection is not defined for lambda = -mu")
768
769    @property
770    def inverse(self):
771        return Sky2Pix_CylindricalPerspective(self.mu.value, self.lam.value)
772
773    @classmethod
774    def evaluate(cls, x, y, mu, lam):
775        return _projections.cypx2s(x, y, mu, lam)
776
777
778Pix2Sky_CYP = Pix2Sky_CylindricalPerspective
779
780
781class Sky2Pix_CylindricalPerspective(Sky2PixProjection, Cylindrical):
782    r"""
783    Cylindrical Perspective - sky to pixel.
784
785    Corresponds to the ``CYP`` projection in FITS WCS.
786
787    .. math::
788        x &= \lambda \phi \\
789        y &= \frac{180^{\circ}}{\pi}\left(\frac{\mu + \lambda}{\mu + \cos \theta}\right)\sin \theta
790
791    Parameters
792    ----------
793    mu : float
794        Distance from center of sphere in the direction opposite the
795        projected surface, in spherical radii, μ.  Default is 0.
796
797    lam : float
798        Radius of the cylinder in spherical radii, λ.  Default is 0.
799    """
800
801    mu = Parameter(default=1.0, description="Distance from center of sphere in spherical radii")
802    lam = Parameter(default=1.0, description="Radius of the cylinder in spherical radii")
803
804    @mu.validator
805    def mu(self, value):
806        if np.any(value == -self.lam):
807            raise InputParameterError(
808                "CYP projection is not defined for mu = -lambda")
809
810    @lam.validator
811    def lam(self, value):
812        if np.any(value == -self.mu):
813            raise InputParameterError(
814                "CYP projection is not defined for lambda = -mu")
815
816    @property
817    def inverse(self):
818        return Pix2Sky_CylindricalPerspective(self.mu, self.lam)
819
820    @classmethod
821    def evaluate(cls, phi, theta, mu, lam):
822        return _projections.cyps2x(phi, theta, mu, lam)
823
824
825Sky2Pix_CYP = Sky2Pix_CylindricalPerspective
826
827
828class Pix2Sky_CylindricalEqualArea(Pix2SkyProjection, Cylindrical):
829    r"""
830    Cylindrical equal area projection - pixel to sky.
831
832    Corresponds to the ``CEA`` projection in FITS WCS.
833
834    .. math::
835        \phi &= x \\
836        \theta &= \sin^{-1}\left(\frac{\pi}{180^{\circ}}\lambda y\right)
837
838    Parameters
839    ----------
840    lam : float
841        Radius of the cylinder in spherical radii, λ.  Default is 1.
842    """
843
844    lam = Parameter(default=1)
845
846    @property
847    def inverse(self):
848        return Sky2Pix_CylindricalEqualArea(self.lam)
849
850    @classmethod
851    def evaluate(cls, x, y, lam):
852        return _projections.ceax2s(x, y, lam)
853
854
855Pix2Sky_CEA = Pix2Sky_CylindricalEqualArea
856
857
858class Sky2Pix_CylindricalEqualArea(Sky2PixProjection, Cylindrical):
859    r"""
860    Cylindrical equal area projection - sky to pixel.
861
862    Corresponds to the ``CEA`` projection in FITS WCS.
863
864    .. math::
865        x &= \phi \\
866        y &= \frac{180^{\circ}}{\pi}\frac{\sin \theta}{\lambda}
867
868    Parameters
869    ----------
870    lam : float
871        Radius of the cylinder in spherical radii, λ.  Default is 0.
872    """
873
874    lam = Parameter(default=1)
875
876    @property
877    def inverse(self):
878        return Pix2Sky_CylindricalEqualArea(self.lam)
879
880    @classmethod
881    def evaluate(cls, phi, theta, lam):
882        return _projections.ceas2x(phi, theta, lam)
883
884
885Sky2Pix_CEA = Sky2Pix_CylindricalEqualArea
886
887
888class Pix2Sky_PlateCarree(Pix2SkyProjection, Cylindrical):
889    r"""
890    Plate carrée projection - pixel to sky.
891
892    Corresponds to the ``CAR`` projection in FITS WCS.
893
894    .. math::
895        \phi &= x \\
896        \theta &= y
897    """
898
899    @property
900    def inverse(self):
901        return Sky2Pix_PlateCarree()
902
903    @staticmethod
904    def evaluate(x, y):
905        # The intermediate variables are only used here for clarity
906        phi = np.array(x, copy=True)
907        theta = np.array(y, copy=True)
908
909        return phi, theta
910
911
912Pix2Sky_CAR = Pix2Sky_PlateCarree
913
914
915class Sky2Pix_PlateCarree(Sky2PixProjection, Cylindrical):
916    r"""
917    Plate carrée projection - sky to pixel.
918
919    Corresponds to the ``CAR`` projection in FITS WCS.
920
921    .. math::
922        x &= \phi \\
923        y &= \theta
924    """
925
926    @property
927    def inverse(self):
928        return Pix2Sky_PlateCarree()
929
930    @staticmethod
931    def evaluate(phi, theta):
932        # The intermediate variables are only used here for clarity
933        x = np.array(phi, copy=True)
934        y = np.array(theta, copy=True)
935
936        return x, y
937
938
939Sky2Pix_CAR = Sky2Pix_PlateCarree
940
941
942class Pix2Sky_Mercator(Pix2SkyProjection, Cylindrical):
943    r"""
944    Mercator - pixel to sky.
945
946    Corresponds to the ``MER`` projection in FITS WCS.
947
948    .. math::
949        \phi &= x \\
950        \theta &= 2 \tan^{-1}\left(e^{y \pi / 180^{\circ}}\right)-90^{\circ}
951    """
952
953    @property
954    def inverse(self):
955        return Sky2Pix_Mercator()
956
957    @classmethod
958    def evaluate(cls, x, y):
959        return _projections.merx2s(x, y)
960
961
962Pix2Sky_MER = Pix2Sky_Mercator
963
964
965class Sky2Pix_Mercator(Sky2PixProjection, Cylindrical):
966    r"""
967    Mercator - sky to pixel.
968
969    Corresponds to the ``MER`` projection in FITS WCS.
970
971    .. math::
972        x &= \phi \\
973        y &= \frac{180^{\circ}}{\pi}\ln \tan \left(\frac{90^{\circ} + \theta}{2}\right)
974    """
975
976    @property
977    def inverse(self):
978        return Pix2Sky_Mercator()
979
980    @classmethod
981    def evaluate(cls, phi, theta):
982        return _projections.mers2x(phi, theta)
983
984
985Sky2Pix_MER = Sky2Pix_Mercator
986
987
988class PseudoCylindrical(Projection):
989    r"""Base class for pseudocylindrical projections.
990
991    Pseudocylindrical projections are like cylindrical projections
992    except the parallels of latitude are projected at diminishing
993    lengths toward the polar regions in order to reduce lateral
994    distortion there.  Consequently, the meridians are curved.
995    """
996
997    _separable = True
998
999
1000class Pix2Sky_SansonFlamsteed(Pix2SkyProjection, PseudoCylindrical):
1001    r"""
1002    Sanson-Flamsteed projection - pixel to sky.
1003
1004    Corresponds to the ``SFL`` projection in FITS WCS.
1005
1006    .. math::
1007        \phi &= \frac{x}{\cos y} \\
1008        \theta &= y
1009    """
1010
1011    @property
1012    def inverse(self):
1013        return Sky2Pix_SansonFlamsteed()
1014
1015    @classmethod
1016    def evaluate(cls, x, y):
1017        return _projections.sflx2s(x, y)
1018
1019
1020Pix2Sky_SFL = Pix2Sky_SansonFlamsteed
1021
1022
1023class Sky2Pix_SansonFlamsteed(Sky2PixProjection, PseudoCylindrical):
1024    r"""
1025    Sanson-Flamsteed projection - sky to pixel.
1026
1027    Corresponds to the ``SFL`` projection in FITS WCS.
1028
1029    .. math::
1030        x &= \phi \cos \theta \\
1031        y &= \theta
1032    """
1033
1034    @property
1035    def inverse(self):
1036        return Pix2Sky_SansonFlamsteed()
1037
1038    @classmethod
1039    def evaluate(cls, phi, theta):
1040        return _projections.sfls2x(phi, theta)
1041
1042
1043Sky2Pix_SFL = Sky2Pix_SansonFlamsteed
1044
1045
1046class Pix2Sky_Parabolic(Pix2SkyProjection, PseudoCylindrical):
1047    r"""
1048    Parabolic projection - pixel to sky.
1049
1050    Corresponds to the ``PAR`` projection in FITS WCS.
1051
1052    .. math::
1053        \phi &= \frac{180^\circ}{\pi} \frac{x}{1 - 4(y / 180^\circ)^2} \\
1054        \theta &= 3 \sin^{-1}\left(\frac{y}{180^\circ}\right)
1055    """
1056
1057    _separable = False
1058
1059    @property
1060    def inverse(self):
1061        return Sky2Pix_Parabolic()
1062
1063    @classmethod
1064    def evaluate(cls, x, y):
1065        return _projections.parx2s(x, y)
1066
1067
1068Pix2Sky_PAR = Pix2Sky_Parabolic
1069
1070
1071class Sky2Pix_Parabolic(Sky2PixProjection, PseudoCylindrical):
1072    r"""
1073    Parabolic projection - sky to pixel.
1074
1075    Corresponds to the ``PAR`` projection in FITS WCS.
1076
1077    .. math::
1078        x &= \phi \left(2\cos\frac{2\theta}{3} - 1\right) \\
1079        y &= 180^\circ \sin \frac{\theta}{3}
1080    """
1081
1082    _separable = False
1083
1084    @property
1085    def inverse(self):
1086        return Pix2Sky_Parabolic()
1087
1088    @classmethod
1089    def evaluate(cls, phi, theta):
1090        return _projections.pars2x(phi, theta)
1091
1092
1093Sky2Pix_PAR = Sky2Pix_Parabolic
1094
1095
1096class Pix2Sky_Molleweide(Pix2SkyProjection, PseudoCylindrical):
1097    r"""
1098    Molleweide's projection - pixel to sky.
1099
1100    Corresponds to the ``MOL`` projection in FITS WCS.
1101
1102    .. math::
1103        \phi &= \frac{\pi x}{2 \sqrt{2 - \left(\frac{\pi}{180^\circ}y\right)^2}} \\
1104        \theta &= \sin^{-1}\left(\frac{1}{90^\circ}\sin^{-1}\left(\frac{\pi}{180^\circ}\frac{y}{\sqrt{2}}\right) + \frac{y}{180^\circ}\sqrt{2 - \left(\frac{\pi}{180^\circ}y\right)^2}\right)
1105    """
1106
1107    _separable = False
1108
1109    @property
1110    def inverse(self):
1111        return Sky2Pix_Molleweide()
1112
1113    @classmethod
1114    def evaluate(cls, x, y):
1115        return _projections.molx2s(x, y)
1116
1117
1118Pix2Sky_MOL = Pix2Sky_Molleweide
1119
1120
1121class Sky2Pix_Molleweide(Sky2PixProjection, PseudoCylindrical):
1122    r"""
1123    Molleweide's projection - sky to pixel.
1124
1125    Corresponds to the ``MOL`` projection in FITS WCS.
1126
1127    .. math::
1128        x &= \frac{2 \sqrt{2}}{\pi} \phi \cos \gamma \\
1129        y &= \sqrt{2} \frac{180^\circ}{\pi} \sin \gamma
1130
1131    where :math:`\gamma` is defined as the solution of the
1132    transcendental equation:
1133
1134    .. math::
1135
1136        \sin \theta = \frac{\gamma}{90^\circ} + \frac{\sin 2 \gamma}{\pi}
1137    """
1138
1139    _separable = False
1140
1141    @property
1142    def inverse(self):
1143        return Pix2Sky_Molleweide()
1144
1145    @classmethod
1146    def evaluate(cls, phi, theta):
1147        return _projections.mols2x(phi, theta)
1148
1149
1150Sky2Pix_MOL = Sky2Pix_Molleweide
1151
1152
1153class Pix2Sky_HammerAitoff(Pix2SkyProjection, PseudoCylindrical):
1154    r"""
1155    Hammer-Aitoff projection - pixel to sky.
1156
1157    Corresponds to the ``AIT`` projection in FITS WCS.
1158
1159    .. math::
1160        \phi &= 2 \arg \left(2Z^2 - 1, \frac{\pi}{180^\circ} \frac{Z}{2}x\right) \\
1161        \theta &= \sin^{-1}\left(\frac{\pi}{180^\circ}yZ\right)
1162    """
1163
1164    _separable = False
1165
1166    @property
1167    def inverse(self):
1168        return Sky2Pix_HammerAitoff()
1169
1170    @classmethod
1171    def evaluate(cls, x, y):
1172        return _projections.aitx2s(x, y)
1173
1174
1175Pix2Sky_AIT = Pix2Sky_HammerAitoff
1176
1177
1178class Sky2Pix_HammerAitoff(Sky2PixProjection, PseudoCylindrical):
1179    r"""
1180    Hammer-Aitoff projection - sky to pixel.
1181
1182    Corresponds to the ``AIT`` projection in FITS WCS.
1183
1184    .. math::
1185        x &= 2 \gamma \cos \theta \sin \frac{\phi}{2} \\
1186        y &= \gamma \sin \theta
1187
1188    where:
1189
1190    .. math::
1191        \gamma = \frac{180^\circ}{\pi} \sqrt{\frac{2}{1 + \cos \theta \cos(\phi / 2)}}
1192    """
1193
1194    _separable = False
1195
1196    @property
1197    def inverse(self):
1198        return Pix2Sky_HammerAitoff()
1199
1200    @classmethod
1201    def evaluate(cls, phi, theta):
1202        return _projections.aits2x(phi, theta)
1203
1204
1205Sky2Pix_AIT = Sky2Pix_HammerAitoff
1206
1207
1208class Conic(Projection):
1209    r"""Base class for conic projections.
1210
1211    In conic projections, the sphere is thought to be projected onto
1212    the surface of a cone which is then opened out.
1213
1214    In a general sense, the pixel-to-sky transformation is defined as:
1215
1216    .. math::
1217
1218        \phi &= \arg\left(\frac{Y_0 - y}{R_\theta}, \frac{x}{R_\theta}\right) / C \\
1219        R_\theta &= \mathrm{sign} \theta_a \sqrt{x^2 + (Y_0 - y)^2}
1220
1221    and the inverse (sky-to-pixel) is defined as:
1222
1223    .. math::
1224        x &= R_\theta \sin (C \phi) \\
1225        y &= R_\theta \cos (C \phi) + Y_0
1226
1227    where :math:`C` is the "constant of the cone":
1228
1229    .. math::
1230        C = \frac{180^\circ \cos \theta}{\pi R_\theta}
1231    """
1232    sigma = Parameter(default=90.0, getter=_to_orig_unit, setter=_to_radian)
1233    delta = Parameter(default=0.0, getter=_to_orig_unit, setter=_to_radian)
1234
1235    _separable = False
1236
1237
1238class Pix2Sky_ConicPerspective(Pix2SkyProjection, Conic):
1239    r"""
1240    Colles' conic perspective projection - pixel to sky.
1241
1242    Corresponds to the ``COP`` projection in FITS WCS.
1243
1244    See `Conic` for a description of the entire equation.
1245
1246    The projection formulae are:
1247
1248    .. math::
1249        C &= \sin \theta_a \\
1250        R_\theta &= \frac{180^\circ}{\pi} \cos \eta [ \cot \theta_a - \tan(\theta - \theta_a)] \\
1251        Y_0 &= \frac{180^\circ}{\pi} \cos \eta \cot \theta_a
1252
1253    Parameters
1254    ----------
1255    sigma : float
1256        :math:`(\theta_1 + \theta_2) / 2`, where :math:`\theta_1` and
1257        :math:`\theta_2` are the latitudes of the standard parallels,
1258        in degrees.  Default is 90.
1259
1260    delta : float
1261        :math:`(\theta_1 - \theta_2) / 2`, where :math:`\theta_1` and
1262        :math:`\theta_2` are the latitudes of the standard parallels,
1263        in degrees.  Default is 0.
1264    """
1265    @property
1266    def inverse(self):
1267        return Sky2Pix_ConicPerspective(self.sigma.value, self.delta.value)
1268
1269    @classmethod
1270    def evaluate(cls, x, y, sigma, delta):
1271        return _projections.copx2s(x, y, _to_orig_unit(sigma), _to_orig_unit(delta))
1272
1273
1274Pix2Sky_COP = Pix2Sky_ConicPerspective
1275
1276
1277class Sky2Pix_ConicPerspective(Sky2PixProjection, Conic):
1278    r"""
1279    Colles' conic perspective projection - sky to pixel.
1280
1281    Corresponds to the ``COP`` projection in FITS WCS.
1282
1283    See `Conic` for a description of the entire equation.
1284
1285    The projection formulae are:
1286
1287    .. math::
1288        C &= \sin \theta_a \\
1289        R_\theta &= \frac{180^\circ}{\pi} \cos \eta [ \cot \theta_a - \tan(\theta - \theta_a)] \\
1290        Y_0 &= \frac{180^\circ}{\pi} \cos \eta \cot \theta_a
1291
1292    Parameters
1293    ----------
1294    sigma : float
1295        :math:`(\theta_1 + \theta_2) / 2`, where :math:`\theta_1` and
1296        :math:`\theta_2` are the latitudes of the standard parallels,
1297        in degrees.  Default is 90.
1298
1299    delta : float
1300        :math:`(\theta_1 - \theta_2) / 2`, where :math:`\theta_1` and
1301        :math:`\theta_2` are the latitudes of the standard parallels,
1302        in degrees.  Default is 0.
1303    """
1304    @property
1305    def inverse(self):
1306        return Pix2Sky_ConicPerspective(self.sigma.value, self.delta.value)
1307
1308    @classmethod
1309    def evaluate(cls, phi, theta, sigma, delta):
1310        return _projections.cops2x(phi, theta,
1311                                   _to_orig_unit(sigma), _to_orig_unit(delta))
1312
1313
1314Sky2Pix_COP = Sky2Pix_ConicPerspective
1315
1316
1317class Pix2Sky_ConicEqualArea(Pix2SkyProjection, Conic):
1318    r"""
1319    Alber's conic equal area projection - pixel to sky.
1320
1321    Corresponds to the ``COE`` projection in FITS WCS.
1322
1323    See `Conic` for a description of the entire equation.
1324
1325    The projection formulae are:
1326
1327    .. math::
1328        C &= \gamma / 2 \\
1329        R_\theta &= \frac{180^\circ}{\pi} \frac{2}{\gamma} \sqrt{1 + \sin \theta_1 \sin \theta_2 - \gamma \sin \theta} \\
1330        Y_0 &= \frac{180^\circ}{\pi} \frac{2}{\gamma} \sqrt{1 + \sin \theta_1 \sin \theta_2 - \gamma \sin((\theta_1 + \theta_2)/2)}
1331
1332    where:
1333
1334    .. math::
1335        \gamma = \sin \theta_1 + \sin \theta_2
1336
1337    Parameters
1338    ----------
1339    sigma : float
1340        :math:`(\theta_1 + \theta_2) / 2`, where :math:`\theta_1` and
1341        :math:`\theta_2` are the latitudes of the standard parallels,
1342        in degrees.  Default is 90.
1343
1344    delta : float
1345        :math:`(\theta_1 - \theta_2) / 2`, where :math:`\theta_1` and
1346        :math:`\theta_2` are the latitudes of the standard parallels,
1347        in degrees.  Default is 0.
1348    """
1349    @property
1350    def inverse(self):
1351        return Sky2Pix_ConicEqualArea(self.sigma.value, self.delta.value)
1352
1353    @classmethod
1354    def evaluate(cls, x, y, sigma, delta):
1355        return _projections.coex2s(x, y, _to_orig_unit(sigma), _to_orig_unit(delta))
1356
1357
1358Pix2Sky_COE = Pix2Sky_ConicEqualArea
1359
1360
1361class Sky2Pix_ConicEqualArea(Sky2PixProjection, Conic):
1362    r"""
1363    Alber's conic equal area projection - sky to pixel.
1364
1365    Corresponds to the ``COE`` projection in FITS WCS.
1366
1367    See `Conic` for a description of the entire equation.
1368
1369    The projection formulae are:
1370
1371    .. math::
1372        C &= \gamma / 2 \\
1373        R_\theta &= \frac{180^\circ}{\pi} \frac{2}{\gamma} \sqrt{1 + \sin \theta_1 \sin \theta_2 - \gamma \sin \theta} \\
1374        Y_0 &= \frac{180^\circ}{\pi} \frac{2}{\gamma} \sqrt{1 + \sin \theta_1 \sin \theta_2 - \gamma \sin((\theta_1 + \theta_2)/2)}
1375
1376    where:
1377
1378    .. math::
1379        \gamma = \sin \theta_1 + \sin \theta_2
1380
1381    Parameters
1382    ----------
1383    sigma : float
1384        :math:`(\theta_1 + \theta_2) / 2`, where :math:`\theta_1` and
1385        :math:`\theta_2` are the latitudes of the standard parallels,
1386        in degrees.  Default is 90.
1387
1388    delta : float
1389        :math:`(\theta_1 - \theta_2) / 2`, where :math:`\theta_1` and
1390        :math:`\theta_2` are the latitudes of the standard parallels,
1391        in degrees.  Default is 0.
1392    """
1393    @property
1394    def inverse(self):
1395        return Pix2Sky_ConicEqualArea(self.sigma.value, self.delta.value)
1396
1397    @classmethod
1398    def evaluate(cls, phi, theta, sigma, delta):
1399        return _projections.coes2x(phi, theta,
1400                                   _to_orig_unit(sigma), _to_orig_unit(delta))
1401
1402
1403Sky2Pix_COE = Sky2Pix_ConicEqualArea
1404
1405
1406class Pix2Sky_ConicEquidistant(Pix2SkyProjection, Conic):
1407    r"""
1408    Conic equidistant projection - pixel to sky.
1409
1410    Corresponds to the ``COD`` projection in FITS WCS.
1411
1412    See `Conic` for a description of the entire equation.
1413
1414    The projection formulae are:
1415
1416    .. math::
1417
1418        C &= \frac{180^\circ}{\pi} \frac{\sin\theta_a\sin\eta}{\eta} \\
1419        R_\theta &= \theta_a - \theta + \eta\cot\eta\cot\theta_a \\
1420        Y_0 = \eta\cot\eta\cot\theta_a
1421
1422    Parameters
1423    ----------
1424    sigma : float
1425        :math:`(\theta_1 + \theta_2) / 2`, where :math:`\theta_1` and
1426        :math:`\theta_2` are the latitudes of the standard parallels,
1427        in degrees.  Default is 90.
1428
1429    delta : float
1430        :math:`(\theta_1 - \theta_2) / 2`, where :math:`\theta_1` and
1431        :math:`\theta_2` are the latitudes of the standard parallels,
1432        in degrees.  Default is 0.
1433    """
1434    @property
1435    def inverse(self):
1436        return Sky2Pix_ConicEquidistant(self.sigma.value, self.delta.value)
1437
1438    @classmethod
1439    def evaluate(cls, x, y, sigma, delta):
1440        return _projections.codx2s(x, y, _to_orig_unit(sigma), _to_orig_unit(delta))
1441
1442
1443Pix2Sky_COD = Pix2Sky_ConicEquidistant
1444
1445
1446class Sky2Pix_ConicEquidistant(Sky2PixProjection, Conic):
1447    r"""
1448    Conic equidistant projection - sky to pixel.
1449
1450    Corresponds to the ``COD`` projection in FITS WCS.
1451
1452    See `Conic` for a description of the entire equation.
1453
1454    The projection formulae are:
1455
1456    .. math::
1457
1458        C &= \frac{180^\circ}{\pi} \frac{\sin\theta_a\sin\eta}{\eta} \\
1459        R_\theta &= \theta_a - \theta + \eta\cot\eta\cot\theta_a \\
1460        Y_0 = \eta\cot\eta\cot\theta_a
1461
1462    Parameters
1463    ----------
1464    sigma : float
1465        :math:`(\theta_1 + \theta_2) / 2`, where :math:`\theta_1` and
1466        :math:`\theta_2` are the latitudes of the standard parallels,
1467        in degrees.  Default is 90.
1468
1469    delta : float
1470        :math:`(\theta_1 - \theta_2) / 2`, where :math:`\theta_1` and
1471        :math:`\theta_2` are the latitudes of the standard parallels,
1472        in degrees.  Default is 0.
1473    """
1474    @property
1475    def inverse(self):
1476        return Pix2Sky_ConicEquidistant(self.sigma.value, self.delta.value)
1477
1478    @classmethod
1479    def evaluate(cls, phi, theta, sigma, delta):
1480        return _projections.cods2x(phi, theta,
1481                                   _to_orig_unit(sigma), _to_orig_unit(delta))
1482
1483
1484Sky2Pix_COD = Sky2Pix_ConicEquidistant
1485
1486
1487class Pix2Sky_ConicOrthomorphic(Pix2SkyProjection, Conic):
1488    r"""
1489    Conic orthomorphic projection - pixel to sky.
1490
1491    Corresponds to the ``COO`` projection in FITS WCS.
1492
1493    See `Conic` for a description of the entire equation.
1494
1495    The projection formulae are:
1496
1497    .. math::
1498
1499        C &= \frac{\ln \left( \frac{\cos\theta_2}{\cos\theta_1} \right)}
1500                  {\ln \left[ \frac{\tan\left(\frac{90^\circ-\theta_2}{2}\right)}
1501                                   {\tan\left(\frac{90^\circ-\theta_1}{2}\right)} \right] } \\
1502        R_\theta &= \psi \left[ \tan \left( \frac{90^\circ - \theta}{2} \right) \right]^C \\
1503        Y_0 &= \psi \left[ \tan \left( \frac{90^\circ - \theta_a}{2} \right) \right]^C
1504
1505    where:
1506
1507    .. math::
1508
1509        \psi = \frac{180^\circ}{\pi} \frac{\cos \theta}
1510               {C\left[\tan\left(\frac{90^\circ-\theta}{2}\right)\right]^C}
1511
1512    Parameters
1513    ----------
1514    sigma : float
1515        :math:`(\theta_1 + \theta_2) / 2`, where :math:`\theta_1` and
1516        :math:`\theta_2` are the latitudes of the standard parallels,
1517        in degrees.  Default is 90.
1518
1519    delta : float
1520        :math:`(\theta_1 - \theta_2) / 2`, where :math:`\theta_1` and
1521        :math:`\theta_2` are the latitudes of the standard parallels,
1522        in degrees.  Default is 0.
1523    """
1524    @property
1525    def inverse(self):
1526        return Sky2Pix_ConicOrthomorphic(self.sigma.value, self.delta.value)
1527
1528    @classmethod
1529    def evaluate(cls, x, y, sigma, delta):
1530        return _projections.coox2s(x, y, _to_orig_unit(sigma), _to_orig_unit(delta))
1531
1532
1533Pix2Sky_COO = Pix2Sky_ConicOrthomorphic
1534
1535
1536class Sky2Pix_ConicOrthomorphic(Sky2PixProjection, Conic):
1537    r"""
1538    Conic orthomorphic projection - sky to pixel.
1539
1540    Corresponds to the ``COO`` projection in FITS WCS.
1541
1542    See `Conic` for a description of the entire equation.
1543
1544    The projection formulae are:
1545
1546    .. math::
1547
1548        C &= \frac{\ln \left( \frac{\cos\theta_2}{\cos\theta_1} \right)}
1549                  {\ln \left[ \frac{\tan\left(\frac{90^\circ-\theta_2}{2}\right)}
1550                                   {\tan\left(\frac{90^\circ-\theta_1}{2}\right)} \right] } \\
1551        R_\theta &= \psi \left[ \tan \left( \frac{90^\circ - \theta}{2} \right) \right]^C \\
1552        Y_0 &= \psi \left[ \tan \left( \frac{90^\circ - \theta_a}{2} \right) \right]^C
1553
1554    where:
1555
1556    .. math::
1557
1558        \psi = \frac{180^\circ}{\pi} \frac{\cos \theta}
1559               {C\left[\tan\left(\frac{90^\circ-\theta}{2}\right)\right]^C}
1560
1561    Parameters
1562    ----------
1563    sigma : float
1564        :math:`(\theta_1 + \theta_2) / 2`, where :math:`\theta_1` and
1565        :math:`\theta_2` are the latitudes of the standard parallels,
1566        in degrees.  Default is 90.
1567
1568    delta : float
1569        :math:`(\theta_1 - \theta_2) / 2`, where :math:`\theta_1` and
1570        :math:`\theta_2` are the latitudes of the standard parallels,
1571        in degrees.  Default is 0.
1572    """
1573    @property
1574    def inverse(self):
1575        return Pix2Sky_ConicOrthomorphic(self.sigma.value, self.delta.value)
1576
1577    @classmethod
1578    def evaluate(cls, phi, theta, sigma, delta):
1579        return _projections.coos2x(phi, theta,
1580                                   _to_orig_unit(sigma), _to_orig_unit(delta))
1581
1582
1583Sky2Pix_COO = Sky2Pix_ConicOrthomorphic
1584
1585
1586class PseudoConic(Projection):
1587    r"""Base class for pseudoconic projections.
1588
1589    Pseudoconics are a subclass of conics with concentric parallels.
1590    """
1591
1592
1593class Pix2Sky_BonneEqualArea(Pix2SkyProjection, PseudoConic):
1594    r"""
1595    Bonne's equal area pseudoconic projection - pixel to sky.
1596
1597    Corresponds to the ``BON`` projection in FITS WCS.
1598
1599    .. math::
1600
1601        \phi &= \frac{\pi}{180^\circ} A_\phi R_\theta / \cos \theta \\
1602        \theta &= Y_0 - R_\theta
1603
1604    where:
1605
1606    .. math::
1607
1608        R_\theta &= \mathrm{sign} \theta_1 \sqrt{x^2 + (Y_0 - y)^2} \\
1609        A_\phi &= \arg\left(\frac{Y_0 - y}{R_\theta}, \frac{x}{R_\theta}\right)
1610
1611    Parameters
1612    ----------
1613    theta1 : float
1614        Bonne conformal latitude, in degrees.
1615    """
1616    theta1 = Parameter(default=0.0, getter=_to_orig_unit, setter=_to_radian)
1617    _separable = True
1618
1619    @property
1620    def inverse(self):
1621        return Sky2Pix_BonneEqualArea(self.theta1.value)
1622
1623    @classmethod
1624    def evaluate(cls, x, y, theta1):
1625        return _projections.bonx2s(x, y, _to_orig_unit(theta1))
1626
1627
1628Pix2Sky_BON = Pix2Sky_BonneEqualArea
1629
1630
1631class Sky2Pix_BonneEqualArea(Sky2PixProjection, PseudoConic):
1632    r"""
1633    Bonne's equal area pseudoconic projection - sky to pixel.
1634
1635    Corresponds to the ``BON`` projection in FITS WCS.
1636
1637    .. math::
1638        x &= R_\theta \sin A_\phi \\
1639        y &= -R_\theta \cos A_\phi + Y_0
1640
1641    where:
1642
1643    .. math::
1644        A_\phi &= \frac{180^\circ}{\pi R_\theta} \phi \cos \theta \\
1645        R_\theta &= Y_0 - \theta \\
1646        Y_0 &= \frac{180^\circ}{\pi} \cot \theta_1 + \theta_1
1647
1648    Parameters
1649    ----------
1650    theta1 : float
1651        Bonne conformal latitude, in degrees.
1652    """
1653    theta1 = Parameter(default=0.0, getter=_to_orig_unit, setter=_to_radian, description="Bonne conformal latitude, in degrees")
1654    _separable = True
1655
1656    @property
1657    def inverse(self):
1658        return Pix2Sky_BonneEqualArea(self.theta1.value)
1659
1660    @classmethod
1661    def evaluate(cls, phi, theta, theta1):
1662        return _projections.bons2x(phi, theta,
1663                                   _to_orig_unit(theta1))
1664
1665
1666Sky2Pix_BON = Sky2Pix_BonneEqualArea
1667
1668
1669class Pix2Sky_Polyconic(Pix2SkyProjection, PseudoConic):
1670    r"""
1671    Polyconic projection - pixel to sky.
1672
1673    Corresponds to the ``PCO`` projection in FITS WCS.
1674    """
1675    _separable = False
1676
1677    @property
1678    def inverse(self):
1679        return Sky2Pix_Polyconic()
1680
1681    @classmethod
1682    def evaluate(cls, x, y):
1683        return _projections.pcox2s(x, y)
1684
1685
1686Pix2Sky_PCO = Pix2Sky_Polyconic
1687
1688
1689class Sky2Pix_Polyconic(Sky2PixProjection, PseudoConic):
1690    r"""
1691    Polyconic projection - sky to pixel.
1692
1693    Corresponds to the ``PCO`` projection in FITS WCS.
1694    """
1695    _separable = False
1696
1697    @property
1698    def inverse(self):
1699        return Pix2Sky_Polyconic()
1700
1701    @classmethod
1702    def evaluate(cls, phi, theta):
1703        return _projections.pcos2x(phi, theta)
1704
1705
1706Sky2Pix_PCO = Sky2Pix_Polyconic
1707
1708
1709class QuadCube(Projection):
1710    r"""Base class for quad cube projections.
1711
1712    Quadrilateralized spherical cube (quad-cube) projections belong to
1713    the class of polyhedral projections in which the sphere is
1714    projected onto the surface of an enclosing polyhedron.
1715
1716    The six faces of the quad-cube projections are numbered and laid
1717    out as::
1718
1719              0
1720        4 3 2 1 4 3 2
1721              5
1722
1723    """
1724
1725
1726class Pix2Sky_TangentialSphericalCube(Pix2SkyProjection, QuadCube):
1727    r"""
1728    Tangential spherical cube projection - pixel to sky.
1729
1730    Corresponds to the ``TSC`` projection in FITS WCS.
1731    """
1732    _separable = False
1733
1734    @property
1735    def inverse(self):
1736        return Sky2Pix_TangentialSphericalCube()
1737
1738    @classmethod
1739    def evaluate(cls, x, y):
1740        return _projections.tscx2s(x, y)
1741
1742
1743Pix2Sky_TSC = Pix2Sky_TangentialSphericalCube
1744
1745
1746class Sky2Pix_TangentialSphericalCube(Sky2PixProjection, QuadCube):
1747    r"""
1748    Tangential spherical cube projection - sky to pixel.
1749
1750    Corresponds to the ``PCO`` projection in FITS WCS.
1751    """
1752    _separable = False
1753
1754    @property
1755    def inverse(self):
1756        return Pix2Sky_TangentialSphericalCube()
1757
1758    @classmethod
1759    def evaluate(cls, phi, theta):
1760        return _projections.tscs2x(phi, theta)
1761
1762
1763Sky2Pix_TSC = Sky2Pix_TangentialSphericalCube
1764
1765
1766class Pix2Sky_COBEQuadSphericalCube(Pix2SkyProjection, QuadCube):
1767    r"""
1768    COBE quadrilateralized spherical cube projection - pixel to sky.
1769
1770    Corresponds to the ``CSC`` projection in FITS WCS.
1771    """
1772    _separable = False
1773
1774    @property
1775    def inverse(self):
1776        return Sky2Pix_COBEQuadSphericalCube()
1777
1778    @classmethod
1779    def evaluate(cls, x, y):
1780        return _projections.cscx2s(x, y)
1781
1782
1783Pix2Sky_CSC = Pix2Sky_COBEQuadSphericalCube
1784
1785
1786class Sky2Pix_COBEQuadSphericalCube(Sky2PixProjection, QuadCube):
1787    r"""
1788    COBE quadrilateralized spherical cube projection - sky to pixel.
1789
1790    Corresponds to the ``CSC`` projection in FITS WCS.
1791    """
1792    _separable = False
1793
1794    @property
1795    def inverse(self):
1796        return Pix2Sky_COBEQuadSphericalCube()
1797
1798    @classmethod
1799    def evaluate(cls, phi, theta):
1800        return _projections.cscs2x(phi, theta)
1801
1802
1803Sky2Pix_CSC = Sky2Pix_COBEQuadSphericalCube
1804
1805
1806class Pix2Sky_QuadSphericalCube(Pix2SkyProjection, QuadCube):
1807    r"""
1808    Quadrilateralized spherical cube projection - pixel to sky.
1809
1810    Corresponds to the ``QSC`` projection in FITS WCS.
1811    """
1812    _separable = False
1813
1814    @property
1815    def inverse(self):
1816        return Sky2Pix_QuadSphericalCube()
1817
1818    @classmethod
1819    def evaluate(cls, x, y):
1820        return _projections.qscx2s(x, y)
1821
1822
1823Pix2Sky_QSC = Pix2Sky_QuadSphericalCube
1824
1825
1826class Sky2Pix_QuadSphericalCube(Sky2PixProjection, QuadCube):
1827    r"""
1828    Quadrilateralized spherical cube projection - sky to pixel.
1829
1830    Corresponds to the ``QSC`` projection in FITS WCS.
1831    """
1832    _separable = False
1833
1834    @property
1835    def inverse(self):
1836        return Pix2Sky_QuadSphericalCube()
1837
1838    @classmethod
1839    def evaluate(cls, phi, theta):
1840        return _projections.qscs2x(phi, theta)
1841
1842
1843Sky2Pix_QSC = Sky2Pix_QuadSphericalCube
1844
1845
1846class HEALPix(Projection):
1847    r"""Base class for HEALPix projections.
1848    """
1849
1850
1851class Pix2Sky_HEALPix(Pix2SkyProjection, HEALPix):
1852    r"""
1853    HEALPix - pixel to sky.
1854
1855    Corresponds to the ``HPX`` projection in FITS WCS.
1856
1857    Parameters
1858    ----------
1859    H : float
1860        The number of facets in longitude direction.
1861
1862    X : float
1863        The number of facets in latitude direction.
1864    """
1865    _separable = True
1866
1867    H = Parameter(default=4.0, description="The number of facets in longitude direction.")
1868    X = Parameter(default=3.0, description="The number of facets in latitude direction.")
1869
1870    @property
1871    def inverse(self):
1872        return Sky2Pix_HEALPix(self.H.value, self.X.value)
1873
1874    @classmethod
1875    def evaluate(cls, x, y, H, X):
1876        return _projections.hpxx2s(x, y, H, X)
1877
1878
1879Pix2Sky_HPX = Pix2Sky_HEALPix
1880
1881
1882class Sky2Pix_HEALPix(Sky2PixProjection, HEALPix):
1883    r"""
1884    HEALPix projection - sky to pixel.
1885
1886    Corresponds to the ``HPX`` projection in FITS WCS.
1887
1888    Parameters
1889    ----------
1890    H : float
1891        The number of facets in longitude direction.
1892
1893    X : float
1894        The number of facets in latitude direction.
1895    """
1896    _separable = True
1897
1898    H = Parameter(default=4.0, description="The number of facets in longitude direction.")
1899    X = Parameter(default=3.0, description="The number of facets in latitude direction.")
1900
1901    @property
1902    def inverse(self):
1903        return Pix2Sky_HEALPix(self.H.value, self.X.value)
1904
1905    @classmethod
1906    def evaluate(cls, phi, theta, H, X):
1907        return _projections.hpxs2x(phi, theta, H, X)
1908
1909
1910Sky2Pix_HPX = Sky2Pix_HEALPix
1911
1912
1913class Pix2Sky_HEALPixPolar(Pix2SkyProjection, HEALPix):
1914    r"""
1915    HEALPix polar, aka "butterfly" projection - pixel to sky.
1916
1917    Corresponds to the ``XPH`` projection in FITS WCS.
1918    """
1919    _separable = False
1920
1921    @property
1922    def inverse(self):
1923        return Sky2Pix_HEALPixPolar()
1924
1925    @classmethod
1926    def evaluate(cls, x, y):
1927        return _projections.xphx2s(x, y)
1928
1929
1930Pix2Sky_XPH = Pix2Sky_HEALPixPolar
1931
1932
1933class Sky2Pix_HEALPixPolar(Sky2PixProjection, HEALPix):
1934    r"""
1935    HEALPix polar, aka "butterfly" projection - pixel to sky.
1936
1937    Corresponds to the ``XPH`` projection in FITS WCS.
1938    """
1939    _separable = False
1940
1941    @property
1942    def inverse(self):
1943        return Pix2Sky_HEALPixPolar()
1944
1945    @classmethod
1946    def evaluate(cls, phi, theta):
1947        return _projections.xphs2x(phi, theta)
1948
1949
1950Sky2Pix_XPH = Sky2Pix_HEALPixPolar
1951
1952
1953class AffineTransformation2D(Model):
1954    """
1955    Perform an affine transformation in 2 dimensions.
1956
1957    Parameters
1958    ----------
1959    matrix : array
1960        A 2x2 matrix specifying the linear transformation to apply to the
1961        inputs
1962
1963    translation : array
1964        A 2D vector (given as either a 2x1 or 1x2 array) specifying a
1965        translation to apply to the inputs
1966    """
1967
1968    n_inputs = 2
1969    n_outputs = 2
1970
1971    standard_broadcasting = False
1972
1973    _separable = False
1974
1975    matrix = Parameter(default=[[1.0, 0.0], [0.0, 1.0]])
1976    translation = Parameter(default=[0.0, 0.0])
1977
1978    @matrix.validator
1979    def matrix(self, value):
1980        """Validates that the input matrix is a 2x2 2D array."""
1981
1982        if np.shape(value) != (2, 2):
1983            raise InputParameterError(
1984                "Expected transformation matrix to be a 2x2 array")
1985
1986    @translation.validator
1987    def translation(self, value):
1988        """
1989        Validates that the translation vector is a 2D vector.  This allows
1990        either a "row" vector or a "column" vector where in the latter case the
1991        resultant Numpy array has ``ndim=2`` but the shape is ``(1, 2)``.
1992        """
1993
1994        if not ((np.ndim(value) == 1 and np.shape(value) == (2,)) or
1995                (np.ndim(value) == 2 and np.shape(value) == (1, 2))):
1996            raise InputParameterError(
1997                "Expected translation vector to be a 2 element row or column "
1998                "vector array")
1999
2000    def __init__(self, matrix=matrix, translation=translation, **kwargs):
2001        super().__init__(matrix=matrix, translation=translation, **kwargs)
2002        self.inputs = ("x", "y")
2003        self.outputs = ("x", "y")
2004
2005    @property
2006    def inverse(self):
2007        """
2008        Inverse transformation.
2009
2010        Raises `~astropy.modeling.InputParameterError` if the transformation cannot be inverted.
2011        """
2012
2013        det = np.linalg.det(self.matrix.value)
2014
2015        if det == 0:
2016            raise InputParameterError(
2017                "Transformation matrix is singular; {} model does not "
2018                "have an inverse".format(self.__class__.__name__))
2019
2020        matrix = np.linalg.inv(self.matrix.value)
2021        if self.matrix.unit is not None:
2022            matrix = matrix * self.matrix.unit
2023        # If matrix has unit then translation has unit, so no need to assign it.
2024        translation = -np.dot(matrix, self.translation.value)
2025        return self.__class__(matrix=matrix, translation=translation)
2026
2027    @classmethod
2028    def evaluate(cls, x, y, matrix, translation):
2029        """
2030        Apply the transformation to a set of 2D Cartesian coordinates given as
2031        two lists--one for the x coordinates and one for a y coordinates--or a
2032        single coordinate pair.
2033
2034        Parameters
2035        ----------
2036        x, y : array, float
2037              x and y coordinates
2038        """
2039        if x.shape != y.shape:
2040            raise ValueError("Expected input arrays to have the same shape")
2041
2042        shape = x.shape or (1,)
2043        # Use asarray to ensure loose the units.
2044        inarr = np.vstack([np.asarray(x).ravel(),
2045                           np.asarray(y).ravel(),
2046                           np.ones(x.size, x.dtype)])
2047
2048        if inarr.shape[0] != 3 or inarr.ndim != 2:
2049            raise ValueError("Incompatible input shapes")
2050
2051        augmented_matrix = cls._create_augmented_matrix(matrix, translation)
2052        result = np.dot(augmented_matrix, inarr)
2053        x, y = result[0], result[1]
2054        x.shape = y.shape = shape
2055
2056        return x, y
2057
2058    @staticmethod
2059    def _create_augmented_matrix(matrix, translation):
2060        unit = None
2061        if any([hasattr(translation, 'unit'), hasattr(matrix, 'unit')]):
2062            if not all([hasattr(translation, 'unit'), hasattr(matrix, 'unit')]):
2063                raise ValueError("To use AffineTransformation with quantities, "
2064                                 "both matrix and unit need to be quantities.")
2065            unit = translation.unit
2066            # matrix should have the same units as translation
2067            if not (matrix.unit / translation.unit) == u.dimensionless_unscaled:
2068                raise ValueError("matrix and translation must have the same units.")
2069
2070        augmented_matrix = np.empty((3, 3), dtype=float)
2071        augmented_matrix[0:2, 0:2] = matrix
2072        augmented_matrix[0:2, 2:].flat = translation
2073        augmented_matrix[2] = [0, 0, 1]
2074        if unit is not None:
2075            return augmented_matrix * unit
2076        return augmented_matrix
2077
2078    @property
2079    def input_units(self):
2080        if self.translation.unit is None and self.matrix.unit is None:
2081            return None
2082        elif self.translation.unit is not None:
2083            return dict(zip(self.inputs, [self.translation.unit] * 2))
2084        else:
2085            return dict(zip(self.inputs, [self.matrix.unit] * 2))
2086