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