Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement APE 14 low-level FITS-WCS API and high-level WCS class #7326

Merged
merged 42 commits into from
Oct 23, 2018
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
6e132b1
Started implementing wrapper for FITS WCS object
astrofrog Mar 1, 2018
aa91946
Improve determination of axis_correlation_matrix
astrofrog Mar 1, 2018
fff3b27
Implemented world_axis_physical_types and world_axis_units
astrofrog Mar 23, 2018
52a28e0
Added solar UCDs
astrofrog Mar 23, 2018
a5e3024
Started implementing the world_axis_object_components and world_axis_…
astrofrog Mar 23, 2018
d408f5f
Fix missing frame name
astrofrog Mar 23, 2018
f664e1e
Started implementing high-level WCS API
astrofrog Mar 26, 2018
4f47dba
Import HighLevelWCS into top-level astropy.wcs [ci skip]
astrofrog Mar 26, 2018
ab4cbf6
Don't serialize coordinate frame
astrofrog Mar 27, 2018
6d7be91
Started adding tests for FITS-WCS implementation of low-level WCS API
astrofrog Jul 28, 2018
9dbf526
Add test for correlation terms in PC matrix
astrofrog Jul 28, 2018
b030393
Added initial tests for high-level API
astrofrog Jul 29, 2018
897cac2
Don't unnecessarily serialize classes
astrofrog Jul 29, 2018
77e05db
Added a time cube example
astrofrog Jul 29, 2018
30bab1f
Avoid circular imports
astrofrog Jul 29, 2018
004452d
Fix newlines
astrofrog Jul 29, 2018
5ca21c5
Re-arrange files
astrofrog Sep 17, 2018
e73030a
Split high-level API into the actual API definition, a mix-in class w…
astrofrog Sep 17, 2018
d14c522
Reorganize files and fix tests
astrofrog Sep 28, 2018
7832553
Make astropy.wcs.WCS have the low- and high-level API
astrofrog Sep 28, 2018
2f98155
Move existing high-level WCS tests to test_fitswcs.py
astrofrog Sep 28, 2018
9acba0b
Improvements to documentation
astrofrog Sep 28, 2018
cbb832e
Respect serialized_classes property
astrofrog Sep 28, 2018
b78dde2
Add test for default/empty WCS and for an unrecognized unit
astrofrog Sep 28, 2018
ff11621
Added more tests
astrofrog Sep 28, 2018
dad4f1b
Added changelog entry
astrofrog Sep 28, 2018
ade0a01
add check that input to HighLevelWCS is a low level WCS
eteq Aug 2, 2018
15f44de
Added a new property WCS.has_distortion which indicates whether disto…
astrofrog Sep 30, 2018
3d08a56
Added test for axis_correlation_matrix to make sure things work corre…
astrofrog Sep 30, 2018
a3a9e5b
Added test for high-level WCS wrapper
astrofrog Oct 3, 2018
173d17c
Added tests for special cases in high level WCS
astrofrog Oct 3, 2018
cb93b25
Make rounding consistent with the gwcs package
astrofrog Oct 3, 2018
bfa5114
Include inherited members in API docs
astrofrog Oct 3, 2018
8b44696
Fix documentation warnings
astrofrog Oct 3, 2018
8e7e342
Remove unnecessary lists
astrofrog Oct 4, 2018
dc67dd2
Added FutureWarning about time coordinates
astrofrog Oct 19, 2018
105c521
Add the ability to extend the CTYPE to UCD1+ mapping using custom_cty…
astrofrog Oct 22, 2018
2daf2a6
Added docstring
astrofrog Oct 22, 2018
55ec9c3
Cache _get_components_and_classes
astrofrog Oct 22, 2018
fc090a3
Split changelog entry
astrofrog Oct 22, 2018
2d5d5e2
Added custom UCD1+ term for CTYPE=BETA
astrofrog Oct 22, 2018
616b9d6
Added a test for wrapping an invalid base WCS object
astrofrog Oct 22, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,12 @@ astropy.wcs
- Added the abstract base class for the low-level WCS API described in APE 14
(https://doi.org/10.5281/zenodo.1188875). [#7325]

- Added the abstract base class for the high-level WCS API described in APE 14
astrofrog marked this conversation as resolved.
Show resolved Hide resolved
(https://doi.org/10.5281/zenodo.1188875) as well as the high-level wrapper
class for low-level WCS objects. [#7326]

- Added a new property ``WCS.has_distortion``. [#7326]

API Changes
-----------

Expand Down
1 change: 1 addition & 0 deletions astropy/wcs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
if not _ASTROPY_SETUP_:
raise


def get_include():
"""
Get the path to astropy.wcs's C header files.
Expand Down
2 changes: 1 addition & 1 deletion astropy/wcs/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from .. import units as u

from .wcs import WCS, WCSSUB_LONGITUDE, WCSSUB_LATITUDE
from .wcs import WCS, WCSSUB_LONGITUDE, WCSSUB_LATITUDE, WCSSUB_CELESTIAL

__doctest_skip__ = ['wcs_to_celestial_frame', 'celestial_frame_to_wcs']

Expand Down
18 changes: 14 additions & 4 deletions astropy/wcs/wcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@
from ..utils.compat import possible_filename
from ..utils.exceptions import AstropyWarning, AstropyUserWarning, AstropyDeprecationWarning

# Mix-in class that provides the APE 14 API
from .wcsapi.fitswcs import FITSWCSAPIMixin

__all__ = ['FITSFixedWarning', 'WCS', 'find_all_wcs',
'DistortionLookupTable', 'Sip', 'Tabprm', 'Wcsprm',
'WCSBase', 'validate', 'WcsError', 'SingularMatrixError',
Expand Down Expand Up @@ -212,7 +215,7 @@ class FITSFixedWarning(AstropyWarning):
pass


class WCS(WCSBase):
class WCS(FITSWCSAPIMixin, WCSBase):
"""WCS objects perform standard WCS transformations, and correct for
`SIP`_ and `distortion paper`_ table-lookup transformations, based
on the WCS keywords and supplementary data read from a FITS file.
Expand Down Expand Up @@ -1635,9 +1638,7 @@ def _all_world2pix(self, world, origin, tolerance, maxiter, adaptive,
# (when any of the non-CD-matrix-based corrections are
# present). If not required return the initial
# approximation (pix0).
if self.sip is None and \
self.cpdis1 is None and self.cpdis2 is None and \
self.det2im1 is None and self.det2im2 is None:
if not self.has_distortion:
# No non-WCS corrections detected so
# simply return initial approximation:
return pix0
Expand Down Expand Up @@ -3041,6 +3042,15 @@ def has_celestial(self):
except InconsistentAxisTypesError:
return False

@property
def has_distortion(self):
"""
Returns `True` if any distortion terms are present.
"""
return (self.sip is not None or
self.cpdis1 is not None or self.cpdis2 is not None or
self.det2im1 is not None and self.det2im2 is not None)

@property
def pixel_scale_matrix(self):

Expand Down
2 changes: 2 additions & 0 deletions astropy/wcs/wcsapi/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
from .low_level_api import *
from .high_level_api import *
from .high_level_wcs_wrapper import *
204 changes: 204 additions & 0 deletions astropy/wcs/wcsapi/fitswcs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
# This file includes the definition of a mix-in class that provides the low-
# and high-level WCS API to the astropy.wcs.WCS object. We keep this code
# isolated in this mix-in class to avoid making the main wcs.py file too
# long.

import numpy as np

from ... import units as u

from .low_level_api import BaseLowLevelWCS
from .high_level_api import HighLevelWCSMixin

__all__ = ['FITSWCSAPIMixin']

# Mapping from CTYPE axis name to UCD1

CTYPE_TO_UCD1 = {

# Celestial coordinates
'RA': 'pos.eq.ra',
'DEC': 'pos.eq.dec',
'GLON': 'pos.galactic.lon',
'GLAT': 'pos.galactic.lat',
'ELON': 'pos.ecliptic.lon',
'ELAT': 'pos.ecliptic.lat',
'TLON': 'pos.bodyrc.lon',
'TLAT': 'pos.bodyrc.lat',
'HPLT': 'custom:pos.helioprojective.lat',
'HPLN': 'custom:pos.helioprojective.lon',

# Spectral coordinates (WCS paper 3)
'FREQ': 'em.freq', # Frequency
'ENER': 'em.energy', # Energy
'WAVN': 'em.wavenumber', # Wavenumber
'WAVE': 'em.wl', # Vacuum wavelength
'VRAD': 'spect.dopplerVeloc.radio', # Radio velocity
'VOPT': 'spect.dopplerVeloc.opt', # Optical velocity
'ZOPT': 'src.redshift', # Redshift
'AWAV': 'em.wl', # Air wavelength
'VELO': 'spect.dopplerVeloc', # Apparent radial velocity
'BETA': None, # Beta factor (v/c)
astrofrog marked this conversation as resolved.
Show resolved Hide resolved

# Time coordinates (https://www.aanda.org/articles/aa/pdf/2015/02/aa24653-14.pdf)
'TIME': 'time',
'TAI': 'time',
'TT': 'time',
'TDT': 'time',
'ET': 'time',
'IAT': 'time',
'UT1': 'time',
'UTC': 'time',
'GMT': 'time',
'GPS': 'time',
'TCG': 'time',
'TCB': 'time',
'TDB': 'time',
'LOCAL': 'time'

# UT() is handled separately in world_axis_physical_types

}


class FITSWCSAPIMixin(BaseLowLevelWCS, HighLevelWCSMixin):
"""
A mix-in class that is intended to be inherited by the
:class:`~astropy.wcs.WCS` class and provides the low- and high-level WCS API
"""

@property
def pixel_n_dim(self):
return self.naxis

@property
def world_n_dim(self):
return len(self.wcs.ctype)

@property
def world_axis_physical_types(self):
types = []
for axis_type in self.axis_type_names:
if axis_type.startswith('UT('):
types.append('time')
else:
types.append(CTYPE_TO_UCD1.get(axis_type, None))
return types

@property
def world_axis_units(self):
units = []
for unit in self.wcs.cunit:
if unit is None:
unit = ''
elif isinstance(unit, u.Unit):
unit = unit.to_string(format='vounit')
else:
try:
unit = u.Unit(unit).to_string(format='vounit')
except u.UnitsError:
unit = ''
units.append(unit)
return units

@property
def axis_correlation_matrix(self):

# If there are any distortions present, we assume that there may be
# correlations between all axes. Maybe if some distortions only apply
# to the image plane we can improve this?
if self.has_distortion:
return np.ones((self.world_n_dim, self.pixel_n_dim), dtype=bool)

# Assuming linear world coordinates along each axis, the correlation
# matrix would be given by whether or not the PC matrix is zero
matrix = self.wcs.get_pc() != 0

# We now need to check specifically for celestial coordinates since
# these can assume correlations because of spherical distortions. For
# each celestial coordinate we copy over the pixel dependencies from
# the other celestial coordinates.
celestial = (self.wcs.axis_types // 1000) % 10 == 2
celestial_indices = np.nonzero(celestial)[0]
for world1 in celestial_indices:
for world2 in celestial_indices:
if world1 != world2:
matrix[world1] |= matrix[world2]
matrix[world2] |= matrix[world1]

return matrix

def pixel_to_world_values(self, *pixel_arrays):
return self.all_pix2world(*pixel_arrays, 0)

def array_index_to_world_values(self, *indices):
return self.all_pix2world(*indices[::-1], 0)

def world_to_pixel_values(self, *world_arrays):
return self.all_world2pix(*world_arrays, 0)

def world_to_array_index_values(self, *world_arrays):
pixel_arrays = self.all_world2pix(*world_arrays, 0)[::-1]
array_indices = tuple(np.asarray(np.floor(pixel + 0.5), dtype=np.int) for pixel in pixel_arrays)
if len(array_indices) == 1:
return array_indices[0]
else:
return array_indices

@property
def world_axis_object_components(self):
return _get_components_and_classes(self)[0]

@property
def world_axis_object_classes(self):
return _get_components_and_classes(self)[1]

@property
def serialized_classes(self):
return False


def _get_components_and_classes(wcs):

# The aim of this function is to return whatever is needed for
# world_axis_object_components and world_axis_object_classes. It's easier
# to figure it out in one go and then return the values and let the
# properties return part of it.

# Avoid circular imports by importing here
from ..utils import wcs_to_celestial_frame
from ...coordinates import SkyCoord

components = [None] * wcs.naxis
classes = {}

# Let's start off by checking whether the WCS has a pair of celestial
# components

if wcs.has_celestial:

frame = wcs_to_celestial_frame(wcs)

kwargs = {}
kwargs['frame'] = frame
kwargs['unit'] = u.deg

classes['celestial'] = (SkyCoord, (), kwargs)

components[wcs.wcs.lng] = ('celestial', 0, 'spherical.lon.degree')
components[wcs.wcs.lat] = ('celestial', 1, 'spherical.lat.degree')

# Fallback: for any remaining components that haven't been identified, just
# return Quantity as the class to use

for i in range(wcs.naxis):
if components[i] is None:
name = wcs.axis_type_names[i].lower()
if name == '':
name = 'world'
while name in classes:
name += "_"
classes[name] = (u.Quantity, (), {'unit': wcs.wcs.cunit[i]})
components[i] = (name, 0, 'value')

return components, classes