gwcs-0.12.0/0000755000732200020070000000000013600427270014705 5ustar denchevaSTSCI\science00000000000000gwcs-0.12.0/.bandit.yaml0000644000732200020070000000006113600426757017116 0ustar denchevaSTSCI\science00000000000000exclude_dirs: - gwcs/tests - gwcs/tags/tests gwcs-0.12.0/.gitignore0000644000732200020070000000111713600426757016706 0ustar denchevaSTSCI\science00000000000000# Compiled files *.py[co] *.a *.o *.so __pycache__ # Ignore .c files by default to avoid including generated code. If you want to # add a non-generated .c extension, use `git add -f filename.c`. *.c # Other generated files */version.py */cython_version.py htmlcov .coverage MANIFEST .pytest_cache # Sphinx docs/api docs/_build docs/generated # Eclipse editor project files .project .pydevproject .settings # Packages/installer info *.egg *.eggs *.egg-info dist build eggs parts bin var sdist develop-eggs .installed.cfg distribute-*.tar.gz # Other .*.swp *~ # Mac OSX .DS_Store .idea gwcs-0.12.0/.gitmodules0000644000732200020070000000015413600426757017073 0ustar denchevaSTSCI\science00000000000000[submodule "astropy-helpers"] path = astropy_helpers url = https://github.com/astropy/astropy-helpers.git gwcs-0.12.0/.rtd-environment.yml0000644000732200020070000000026613600426757020656 0ustar denchevaSTSCI\science00000000000000 dependencies: - python>=3 - numpy - pip: - astropy - sphinx_astropy - sphinx-automodapi - stsci_rtd_theme - asdf - sphinx_rtd_theme - sphinx-asdf gwcs-0.12.0/.travis.yml0000644000732200020070000000647413600426757017042 0ustar denchevaSTSCI\science00000000000000language: python os: linux # Setting sudo to false opts in to Travis-CI container-based builds. sudo: false # The apt packages below are needed for sphinx builds, which can no longer # be installed with sudo apt-get. addons: apt: packages: - graphviz - texlive-latex-extra - dvipng python: - 3.7 - 3.8 env: global: # The following versions are the 'default' for tests, unless # overidden underneath. They are defined here in order to save having # to repeat them for all configurations. - NUMPY_VERSION=1.16 - ASDF_GIT='git+https://github.com/spacetelescope/asdf.git#egg=asdf' #- CONDA_DEPENDENCIES='scipy' #- ASTROPY_GIT='git+https://github.com/astropy/astropy.git@v3.1.x' - ASTROPY_GIT='git+https://github.com/astropy/astropy.git#egg=astropy - PIP_DEPENDENCIES=".[all]" matrix: # Don't wait for allowed failures fast_finish: true include: # Do a security check - env: - TEST_COMMAND="bandit -r gwcs -c .bandit.yaml" - PIP_DEPENDENCIES="bandit" # Do a coverage test. - python: 3.7 env: - TEST_COMMAND='pytest --cov-config=gwcs/tests/coveragerc --cov=gwcs' - PIP_DEPENDENCIES='.[test] coveralls pytest-cov' # Check for sphinx doc build warnings - we do this first because it # may run for a long time - python: 3.7 env: #PIP_DEPENDENCIES="$ASTROPY_GIT $ASDF_GIT sphinx sphinx-automodapi sphinx-rtd-theme stsci-rtd-theme sphinx-astropy -sphinx-asdf" PIP_DEPENDENCIES=".[docs]" TEST_COMMAND="make --directory=docs html" - python: 3.6 env: - NUMPY_VERSION=1.17 - PIP_DEPENDENCIES='.[test]' - TEST_COMMAND="pytest" - python: 3.7 env: - NUMPY_VERSION=1.17 - PIP_DEPENDENCIES='.[test]' - TEST_COMMAND="pytest" - python: 3.8 env: - NUMPY_VERSION=1.17 - PIP_DEPENDENCIES='.[test]' - TEST_COMMAND="pytest" # Do a PEP8 test with flake8 - python: 3.6 env: - TEST_COMMAND="flake8 gwcs --count --select=F, E101, E111, E112, E113, E401, E402, E711, E722, E30 --max-line-length=110" - PIP_DEPENDENCIES="flake8" - python: 3.6 env: - TEST_COMMAND='flake8 gwcs --count --max-line-length=110' - PIP_DEPENDENCIES="flake8" - python: 3.7 env: - PIP_DEPENDENCIES="$ASTROPY_GIT $ASDF_GIT .[test]" allow_failures: - python: 3.6 env: - TEST_COMMAND='flake8 gwcs --count --max-line-length=110' - PIP_DEPENDENCIES="flake8" - python: 3.7 env: - PIP_DEPENDENCIES="$ASTROPY_GIT $ASDF_GIT .[test]" install: - pip install numpy~=$NUMPY_VERSION - pip install $PIP_DEPENDENCIES script: - $TEST_COMMAND after_success: # If coveralls.io is set up for this package, uncomment the line # below and replace "packagename" with the name of your package. # The coveragerc file may be customized as needed for your package. - if [[ $TEST_COMMAND == *--cov* ]]; then coveralls --rcfile='gwcs/tests/coveragerc'; fi gwcs-0.12.0/CHANGES.rst0000644000732200020070000001253513600426764016524 0ustar denchevaSTSCI\science000000000000000.12.0 (2019-12-24) ------------------- New Features ^^^^^^^^^^^^ - ``gwcs.WCS`` now supports the ``world_axis_object_components`` and ``world_axis_object_classes`` methods of the low level WCS API as specified by APE 14. - Removed astropy-helpers from package. [#249] - Added a method ``fix_inputs`` which rturns an unique WCS from a compound WCS by fixing inputs. [#254] - Added two new transforms - ``ToDirectionCosines`` and ``FromDirectionCosines``. [#256] - Added new transforms ``WavelengthFromGratingEquation``, ``AnglesFromGratingEquation3D``. [#259] - ``gwcs.WCS`` now supports the new ``world_axis_names`` and ``pixel_axis_names`` properties on ``LowLevelWCS`` objects. [#260] - Update the ``StokesFrame`` to work for arrays of coordinates and integrate with APE 14. [#258] - Added ``Snell3D``, ``SellmeierGlass`` and ``SellmeierZemax`` transforms. [#270] API Changes ^^^^^^^^^^^ - Changed the initialization of ``TemporalFrame`` to be consistent with other coordinate frames. [#242] Bug Fixes ^^^^^^^^^ - Ensure that ``world_to_pixel_values`` and ``pixel_to_world_values`` always accept and return floats, even if the underlying transform uses units. [#248] 0.11.0 (2019/07/26) ------------------- New Features ^^^^^^^^^^^^ - Add a schema and tag for the Stokes frame. [#164] - Added ``WCS.pixel_shape`` property. [#233] Bug Fixes ^^^^^^^^^ - Update util.isnumerical(...) to recognize big-endian types as numeric. [#225] - Fixed issue in unified WCS API (APE14) for transforms that use ``Quantity``. [#222] - Fixed WCS API issues when ``output_frame`` is 1D, e.g. ``Spectral`` only. [#232] 0.10.0 (12/20/2018) ------------------- New Features ^^^^^^^^^^^^ - Initializing a ``WCS`` object with a ``pipeline`` list now keeps the complete ``CoordinateFrame`` objects in the ``WCS.pipeline``. The effect is that a ``WCS`` object can now be initialized with a ``pipeline`` from a different ``WCS`` object. [#174] - Implement support for astropy APE 14 (https://doi.org/10.5281/zenodo.1188875). [#146] - Added a ``wcs_from_[points`` function which creates a WCS object two matching sets of points ``(x,y)`` and ``(ra, dec)``. [#42] 0.9.0 (2018-05-23) ------------------ New Features ^^^^^^^^^^^^ - Added a ``TemporalFrame`` to represent relative or absolute time axes. [#125] - Removed deprecated ``grid_from_domain`` function and ``WCS.domain`` property. [#119] - Support for Python 2.x, 3.0, 3.1, 3.2, 3.3 and 3.4 was removed. [#119] - Add a ``coordinate_to_quantity`` method to ``CoordinateFrame`` which handles converting rich coordinate input to numerical values. It is an inverse of the ``coordinates`` method. [#133] - Add a ``StokesFrame`` which converts from 'I', 'Q', 'U', 'V' to 0-3. [#133] - Support serializing the base ``CoordinateFrame`` class to asdf, by making a specific tag and schema for ``Frame2D``. [#150] - Generalized the footrpint calculation to all output axes. [#167] API Changes ^^^^^^^^^^^ - The argument ``output="numerical_plus"`` was replaced by a bool argument ``with_units``. [#156] - Added a new flag ``axis_type`` to the footprint method. It controls what type of footprint to calculate. [#167] Bug Fixes ^^^^^^^^^ - Fixed a bug in ``bounding_box`` definition when the WCS has only one axis. [#117] - Fixed a bug in ``grid_from_bounding_box`` which caused the grid to be larger than the image in cases when the bounding box is on the edges of an image. [#121] 0.8.0 (2017-11-02) ------------------ - ``LabelMapperRange`` now returns ``LabelMapperRange._no_label`` when the key is not within any range. [#71] - ``LabelMapperDict`` now returns ``LabelMapperDict._no_label`` when the key does not match. [#72] - Replace ``domain`` with ``bounding_box``. [#74] - Added a ``LabelMapper`` model where ``mapper`` is an instance of `~astropy.modeling.core.Model`. [#78] - Evaluating a WCS with bounding box was moved to ``astropy.modeling``. [#86] - RegionsSelector now handles the case when a label does not have a corresponding transform and returns RegionsSelector.undefined_transform_value. [#86] - GWCS now deals with axes types which are neither celestial nor spectral as "unknown" and creates a transform equivalent to the FITS linear transform. [#92] 0.7 (2016-12-23) ---------------- New Features ^^^^^^^^^^^^ - Added ``wcs_from_fiducial`` function to wcstools. [#34] - Added ``domain`` to the WCS object. [#36] - Added ``grid_from_domain`` function. [#36] - The WCS object can return now an `~astropy.coordinates.SkyCoord` or `~astropy.units.Quantity` object. This is triggered by a new parameter to the ``__call__`` method, ``output`` which takes values of "numericals" (default) or "numericals_plus". [#64] API_Changes ^^^^^^^^^^^ - Added ``atol`` argument to ``LabelMapperDict``, representing the absolute tolerance [#29] - The ``CoordinateFrame.transform_to`` method was removed [#64] Bug Fixes ^^^^^^^^^ - Fixed a bug in ``LabelMapperDict`` where a wrong index was used.[#29] - Changed the order of the inputs when ``LabelMapperArray`` is evaluated as the inputs are supposed to be image coordinates. [#29] - Renamed variables in read_wcs_from_header to match loop variable [#63] 0.5.1 (2016-02-01) ------------------ Bug Fixes ^^^^^^^^^ - Added ASDF requirement to setup. [#30] - Import OrderedDict from collections, not from astropy. [#32] 0.5 (2015-12-28) ---------------- Initial release on PYPI. gwcs-0.12.0/CODE_OF_CONDUCT.md0000644000732200020070000000620713600426757017522 0ustar denchevaSTSCI\science00000000000000# Spacetelescope Open Source Code of Conduct We expect all "spacetelescope" organization projects to adopt a code of conduct that ensures a productive, respectful environment for all open source contributors and participants. We are committed to providing a strong and enforced code of conduct and expect everyone in our community to follow these guidelines when interacting with others in all forums. Our goal is to keep ours a positive, inclusive, successful, and growing community. The community of participants in open source Astronomy projects is made up of members from around the globe with a diverse set of skills, personalities, and experiences. It is through these differences that our community experiences success and continued growth. As members of the community, - We pledge to treat all people with respect and provide a harassment- and bullying-free environment, regardless of sex, sexual orientation and/or gender identity, disability, physical appearance, body size, race, nationality, ethnicity, and religion. In particular, sexual language and imagery, sexist, racist, or otherwise exclusionary jokes are not appropriate. - We pledge to respect the work of others by recognizing acknowledgment/citation requests of original authors. As authors, we pledge to be explicit about how we want our own work to be cited or acknowledged. - We pledge to welcome those interested in joining the community, and realize that including people with a variety of opinions and backgrounds will only serve to enrich our community. In particular, discussions relating to pros/cons of various technologies, programming languages, and so on are welcome, but these should be done with respect, taking proactive measure to ensure that all participants are heard and feel confident that they can freely express their opinions. - We pledge to welcome questions and answer them respectfully, paying particular attention to those new to the community. We pledge to provide respectful criticisms and feedback in forums, especially in discussion threads resulting from code contributions. - We pledge to be conscientious of the perceptions of the wider community and to respond to criticism respectfully. We will strive to model behaviors that encourage productive debate and disagreement, both within our community and where we are criticized. We will treat those outside our community with the same respect as people within our community. - We pledge to help the entire community follow the code of conduct, and to not remain silent when we see violations of the code of conduct. We will take action when members of our community violate this code such as such as contacting conduct@stsci.edu (all emails sent to this address will be treated with the strictest confidence) or talking privately with the person. This code of conduct applies to all community situations online and offline, including mailing lists, forums, social media, conferences, meetings, associated social events, and one-to-one interactions. Parts of this code of conduct have been adapted from the Astropy and Numfocus codes of conduct. http://www.astropy.org/code_of_conduct.html https://www.numfocus.org/about/code-of-conduct/ gwcs-0.12.0/CONTRIBUTING.md0000644000732200020070000000132113600426757017144 0ustar denchevaSTSCI\science00000000000000Please open a new issue or new pull request for bugs, feedback, or new features you would like to see. If there is an issue you would like to work on, please leave a comment and we will be happy to assist. New contributions and contributors are very welcome! New to github or open source projects? If you are unsure about where to start or haven't used github before, please feel free to contact the package maintainers. Feedback and feature requests? Is there something missing you would like to see? Please open an issue or send an email to the maintainers. This package follows the Spacetelescope [Code of Conduct](CODE_OF_CONDUCT.md) strives to provide a welcoming community to all of our users and contributors. gwcs-0.12.0/MANIFEST.in0000644000732200020070000000057613600426757016464 0ustar denchevaSTSCI\science00000000000000include README.rst include ez_setup.py include ah_bootstrap.py include setup.cfg recursive-include docs * exclude docs/generated recursive-include licenses * recursive-include cextern * recursive-include scripts * prune docs/_build prune build recursive-include astropy_helpers * exclude astropy_helpers/.git exclude astropy_helpers/.gitignore exclude *.pyc *.o prune docs/api gwcs-0.12.0/PKG-INFO0000644000732200020070000000055713600427270016011 0ustar denchevaSTSCI\science00000000000000Metadata-Version: 2.1 Name: gwcs Version: 0.12.0 Summary: Generalized World Coordinate System Home-page: https://gwcs.readthedocs.io/en/latest/ Author: gwcs developers Author-email: help@stsci.edu License: BSD Description: Tools for managing the WCS of astronomical observations in a general (non-FITS) way Platform: UNKNOWN Provides-Extra: test Provides-Extra: docs gwcs-0.12.0/README.rst0000644000732200020070000000673213600426757016415 0ustar denchevaSTSCI\science00000000000000.. GWCS - Generalized World Coordinate System ========================================== .. raw:: html

GWCS - Generalized World Coordinate System

Documentation Status Build Status Coverage Status license stsci astropy

Generalized World Coordinate System (GWCS) is an `Astropy`_ affiliated package providing tools for managing the World Coordinate System of astronomical data. GWCS takes a general approach to the problem of expressing transformations between pixel and world coordinates. It supports a data model which includes the entire transformation pipeline from input coordinates (detector by default) to world coordinates. It is tightly integrated with `Astropy`_. - Transforms are instances of ``astropy.Model``. They can be chained, joined or combined with arithmetic operators using the flexible framework of compound models in `astropy.modeling`_. - Celestial coordinates are instances of ``astropy.SkyCoord`` and are transformed to other standard celestial frames using `astropy.coordinates`_. - Time coordinates are represented by ``astropy.Time`` and can be further manipulated using the tools in `astropy.time`_ - Spectral coordinates are ``astropy.Quantity`` objects and can be converted to other units using the tools in `astropy.units`_. For complete features and usage examples see the `documentation`_ site. Note ---- Beginning with version 0.9 GWCS requires Python 3.5 and above. Installation ------------ To install:: pip install gwcs # Make sure pip >= 9.0.1 is used. To clone from github and install the master branch:: git clone https://github.com/spacetelescope/gwcs.git cd gwcs python setup.py install Contributing Code, Documentation, or Feedback --------------------------------------------- We welcome feedback and contributions to the project. Contributions of code, documentation, or general feedback are all appreciated. Please follow the `contributing guidelines `__ to submit an issue or a pull request. We strive to provide a welcoming community to all of our users by abiding to the `Code of Conduct `__. Citing GWCS ----------- .. image:: https://zenodo.org/badge/29208937.svg :target: https://zenodo.org/badge/latestdoi/29208937 If you use GWCS, please cite the package via its Zenodo record. .. _Astropy: http://www.astropy.org/ .. _astropy.time: http://docs.astropy.org/en/stable/time/ .. _astropy.modeling: http://docs.astropy.org/en/stable/modeling/ .. _astropy.units: http://docs.astropy.org/en/stable/units/ .. _astropy.coordinates: http://docs.astropy.org/en/stable/coordinates/ .. _documentation: http://gwcs.readthedocs.org/en/latest/ gwcs-0.12.0/conftest.py0000644000732200020070000000066613600426757017125 0ustar denchevaSTSCI\science00000000000000import os import pkg_resources from astropy.tests.helper import enable_deprecations_as_exceptions # Uncomment the following line to treat all DeprecationWarnings as # exceptions #enable_deprecations_as_exceptions() entry_points = [] for entry_point in pkg_resources.iter_entry_points('pytest11'): entry_points.append(entry_point.name) if "asdf_schema_tester" not in entry_points: pytest_plugins = ['asdf.tests.schema_tester'] gwcs-0.12.0/convert_schemas.py0000644000732200020070000002772313600426757020466 0ustar denchevaSTSCI\science00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst # -*- coding: utf-8 -*- from collections import OrderedDict import io import json import os import sys import textwrap import yaml def write_if_different(filename, data): """ Write ``data`` to ``filename``, if the content of the file is different. Parameters ---------- filename : str The file name to be written to. data : bytes The data to be written to `filename`. """ if not os.path.exists(os.path.dirname(filename)): os.makedirs(os.path.dirname(filename)) if os.path.exists(filename): with open(filename, 'rb') as fd: original_data = fd.read() else: original_data = None if original_data != data: print("Converting schema {0}".format( os.path.basename(filename))) with open(filename, 'wb') as fd: fd.write(data) def write_header(o, content, level): """ Write a reStructuredText header to the file. Parameters ---------- o : output stream content : str The content of the header level : int The level of the header """ levels = '=-~^.' if level >= len(levels): o.write('**{0}**\n\n'.format(content)) else: o.write(content) o.write('\n') o.write(levels[level] * len(content)) o.write('\n\n') def format_range(var_middle, var_end, minimum, maximum, exclusiveMinimum, exclusiveMaximum): """ Formats an mathematical description of a range, for example, ``0 ≤ x ≤ 2``. Parameters ---------- var_middle : str or None The string to put in the middle of an expression, such as the ``x`` in ``0 ≤ x ≤ 2``. var_end : str or None The string to put at one end of a single comparision, such as the ``x`` in ``x ≤ 0``. minimum : number The minimum value. maximum : number The maximum value. exclusiveMinimum : bool If `True`, the range excludes the minimum value. exclusiveMaximum : bool If `True`, the range excludes the maximum value Returns ------- expr : str The formatted range expression """ if minimum is not None and maximum is not None: part = '{0} '.format(minimum) if exclusiveMinimum: part += '<' else: part += '≤' part += ' {0} '.format(var_middle) if exclusiveMaximum: part += '<' else: part += '≤' part += ' {0}'.format(maximum) elif minimum is not None: if var_end is not None: part = '{0} '.format(var_end) else: part = '' if exclusiveMinimum: part += '> {0}'.format(minimum) else: part += '≥ {0}'.format(minimum) elif maximum is not None: if var_end is not None: part = '{0} '.format(var_end) else: part = '' if exclusiveMaximum: part += '< {0}'.format(maximum) else: part += '≤ {0}'.format(maximum) else: return None return part def format_type(schema, root): """ Creates an English/mathematical description of a schema fragment. Parameters ---------- schema : JSON schema fragment root : str The JSON path to the schema fragment. """ if 'anyOf' in schema: return ' :soft:`or` '.join( format_type(x, root) for x in schema['anyOf']) elif 'allOf' in schema: return ' :soft:`and` '.join( format_type(x, root) for x in schema['allOf']) elif '$ref' in schema: ref = schema['$ref'] if ref.startswith('#/'): return ':ref:`{0} <{1}/{2}>`'.format(ref[2:], root, ref[2:]) else: basename = os.path.basename(ref) if "tag:stsci.edu:asdf" in ref or "tag:astropy.org:astropy" in ref: return '`{0} <{1}>`'.format(basename, ref) else: return ':doc:`{0} <{1}>`'.format(basename, ref) else: type = schema.get('type') if isinstance(type, list): parts = [' or '.join(type)] elif type is None: parts = ['any'] else: parts = [type] if type == 'string': range = format_range('*len*', '*len*', schema.get('minLength'), schema.get('maxLength'), False, False) if range is not None or 'pattern' in schema or 'format' in schema: parts.append('(') if range is not None: parts.append(range) if 'pattern' in schema: pattern = schema['pattern'].encode('unicode_escape') pattern = pattern.decode('ascii') parts.append(':soft:`regex` :regexp:`{0}`'.format(pattern)) if 'format' in schema: parts.append(':soft:`format` {0}'.format(schema['format'])) parts.append(')') elif type in ('integer', 'number'): range = format_range('*x*', '', schema.get('minimum'), schema.get('maximum'), schema.get('exclusiveMinimum'), schema.get('exclusiveMaximum')) if range is not None: parts.append(range) # TODO: multipleOf elif type == 'object': range = format_range('*len*', '*len*', schema.get('minProperties'), schema.get('maxProperties'), False, False) if range is not None: parts.append(range) # TODO: Dependencies # TODO: Pattern properties elif type == 'array': items = schema.get('items') if schema.get('items') and isinstance(items, dict): if schema.get('uniqueItems'): parts.append(':soft:`of unique`') else: parts.append(':soft:`of`') parts.append('(') parts.append(format_type(items, root)) parts.append(')') range = format_range('*len*', '*len*', schema.get('minItems'), schema.get('maxItems'), False, False) if range is not None: parts.append(range) if 'enum' in schema: parts.append(':soft:`from`') parts.append(json.dumps(schema['enum'])) return ' '.join(parts) def reindent(content, indent): """ Reindent a string to the given number of spaces. """ content = textwrap.dedent(content) lines = [] for line in content.split('\n'): lines.append(indent + line) return '\n'.join(lines) def recurse(o, name, schema, path, level, required=False): """ Convert a schema fragment to reStructuredText. Parameters ---------- o : output stream name : str Name of the entry schema : schema fragment path : list of str Path to schema fragment level : int Indentation level required : bool If `True` the entry is required by the schema and will be documented as such. """ indent = ' ' * max(level, 0) o.write('\n\n') o.write(indent) o.write('.. _{0}:\n\n'.format(os.path.join(*path))) if level == 0: write_header(o, name, level) else: if name != 'items': o.write(indent) o.write(':entry:`{0}`\n\n'.format(name)) o.write(indent) if path[0].startswith("tag:stsci.edu:asdf"): o.write(format_type(schema, path[0])) else: o.write(":soft:`Type:` ") o.write(format_type(schema, path[0])) o.write('.') if required: o.write(' Required.') o.write('\n\n') o.write(reindent(schema.get('title', ''), indent)) o.write('\n\n') o.write(reindent(schema.get('description', ''), indent)) o.write('\n\n') if 'default' in schema: o.write(indent) o.write(':soft:`Default:` {0}'.format( json.dumps(schema['default']))) o.write('\n\n') if 'definitions' in schema: o.write(indent) o.write(":category:`Definitions:`\n\n") for key, val in schema['definitions'].items(): recurse(o, key, val, path + ['definitions', key], level + 1) if 'anyOf' in schema and len(schema['anyOf']) > 1: o.write(indent) o.write(':category:`Any of:`\n\n') for i, subschema in enumerate(schema['anyOf']): recurse(o, '—', subschema, path + ['anyOf', str(i)], level + 1) elif 'allOf' in schema and len(schema['allOf']) > 1: o.write(indent) o.write(':category:`All of:`\n\n') for i, subschema in enumerate(schema['allOf']): recurse(o, i, subschema, path + ['allOf', str(i)], level + 1) if schema.get('type') == 'object': o.write(indent) o.write(':category:`Properties:`\n\n') for key, val in schema.get('properties', {}).items(): recurse(o, key, val, path + ['properties', key], level + 1, key in schema.get('required', [])) elif schema.get('type') == 'array': o.write(indent) o.write(':category:`Items:`\n\n') items = schema.get('items') if isinstance(items, dict): recurse(o, 'items', items, path + ['items'], level + 1) elif isinstance(items, list): for i, val in enumerate(items): name = 'index[{0}]'.format(i) recurse(o, name, val, path + [str(i)], level + 1) if 'examples' in schema: o.write(indent) o.write(":category:`Examples:`\n\n") for description, example in schema['examples']: o.write(reindent(description + "::\n\n", indent)) o.write(reindent(example, indent + ' ')) o.write('\n\n') def convert_schema_to_rst(src, dst): """ Convert a YAML schema to reStructuredText. """ with open(src, 'rb') as fd: schema = yaml.safe_load(fd) with open(src, 'rb') as fd: yaml_content = fd.read() o = io.StringIO() id = schema.get('id', '#') name = os.path.basename(src[:-5]) if 'title' in schema: name += ': ' + schema['title'].strip() recurse(o, name, schema, [id], 0) #o.write(".. only:: html\n\n :download:`Original schema in YAML <{0}>`\n". #os.path.basename(src))) write_if_different(dst, yaml_content) write_if_different(dst[:-5] + ".rst", o.getvalue().encode('utf-8')) def construct_mapping(self, node, deep=False): """ Make sure the properties are written out in the same order as the original file. """ if not isinstance(node, yaml.MappingNode): raise yaml.constructor.ConstructorError(None, None, "expected a mapping node, but found %s" % node.id, node.start_mark) mapping = OrderedDict() for key_node, value_node in node.value: key = self.construct_object(key_node, deep=deep) try: hash(key) except TypeError as exc: raise yaml.constructor.ConstructorError( "while constructing a mapping", node.start_mark, "found unacceptable key (%s)" % exc, key_node.start_mark) value = self.construct_object(value_node, deep=deep) mapping[key] = value return mapping yaml.SafeLoader.add_constructor( 'tag:yaml.org,2002:map', construct_mapping) def main(src, dst): for root, dirs, files in os.walk(src): for fname in files: if not fname.endswith(".yaml"): continue src_path = os.path.join(root, fname) dst_path = os.path.join( dst, os.path.relpath(src_path, src)) convert_schema_to_rst(src_path, dst_path) def decode_filename(fname): return fname if __name__ == '__main__': src = decode_filename(sys.argv[-2]) dst = decode_filename(sys.argv[-1]) sys.exit(main(src, dst)) gwcs-0.12.0/docs/0000755000732200020070000000000013600427270015635 5ustar denchevaSTSCI\science00000000000000gwcs-0.12.0/docs/Makefile0000644000732200020070000001116413600426757017311 0ustar denchevaSTSCI\science00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest #This is needed with git because git doesn't create a dir if it's empty $(shell [ -d "_static" ] || mkdir -p _static) help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR) -rm -rf api html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Astropy.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Astropy.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/Astropy" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Astropy" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." make -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." gwcs-0.12.0/docs/_templates/0000755000732200020070000000000013600427270017772 5ustar denchevaSTSCI\science00000000000000gwcs-0.12.0/docs/_templates/autosummary/0000755000732200020070000000000013600427270022360 5ustar denchevaSTSCI\science00000000000000gwcs-0.12.0/docs/_templates/autosummary/base.rst0000644000732200020070000000037213600426757024037 0ustar denchevaSTSCI\science00000000000000{% extends "autosummary_core/base.rst" %} {# The template this is inherited from is in astropy/sphinx/ext/templates/autosummary_core. If you want to modify this template, it is strongly recommended that you still inherit from the astropy template. #}gwcs-0.12.0/docs/_templates/autosummary/class.rst0000644000732200020070000000037313600426757024233 0ustar denchevaSTSCI\science00000000000000{% extends "autosummary_core/class.rst" %} {# The template this is inherited from is in astropy/sphinx/ext/templates/autosummary_core. If you want to modify this template, it is strongly recommended that you still inherit from the astropy template. #}gwcs-0.12.0/docs/_templates/autosummary/module.rst0000644000732200020070000000037413600426757024414 0ustar denchevaSTSCI\science00000000000000{% extends "autosummary_core/module.rst" %} {# The template this is inherited from is in astropy/sphinx/ext/templates/autosummary_core. If you want to modify this template, it is strongly recommended that you still inherit from the astropy template. #}gwcs-0.12.0/docs/conf.py0000644000732200020070000001507213600426757017152 0ustar denchevaSTSCI\science00000000000000# -*- coding: utf-8 -*- # Licensed under a 3-clause BSD style license - see LICENSE.rst # # Astropy documentation build configuration file. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this file. # # All configuration values have a default. Some values are defined in # the global Astropy configuration which is loaded here before anything else. # See astropy.sphinx.conf for which values are set there. # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # sys.path.insert(0, os.path.abspath('..')) # IMPORTANT: the above commented section was generated by sphinx-quickstart, but # is *NOT* appropriate for astropy or Astropy affiliated packages. It is left # commented out with this explanation to make it clear why this should not be # done. If the sys.path entry above is added, when the astropy.sphinx.conf # import occurs, it will import the *source* version of astropy instead of the # version installed (if invoked as "make html" or directly with sphinx), or the # version in the build directory (if "python setup.py build_sphinx" is used). # Thus, any C-extensions that are needed to build the documentation will *not* # be accessible, and the documentation will not build correctly. import datetime import os import sys try: from sphinx_astropy.conf.v1 import * # noqa except ImportError: print('ERROR: the documentation requires the sphinx-astropy package to be installed') sys.exit(1) # Get configuration information from setup.cfg try: from ConfigParser import ConfigParser except ImportError: from configparser import ConfigParser conf = ConfigParser() conf.read([os.path.join(os.path.dirname(__file__), '..', 'setup.cfg')]) setup_cfg = dict(conf.items('metadata')) # -- General configuration ---------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.2' # To perform a Sphinx version check that needs to be more specific than # major.minor, call `check_sphinx_version("x.y.z")` here. # check_sphinx_version("1.2.1") # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns.append('_templates') # This is added to the end of RST files - a good place to put substitutions to # be used globally. rst_epilog += """ """ # Top-level directory containing ASDF schemas (relative to current directory) asdf_schema_path = '../gwcs/schemas' # This is the prefix common to all schema IDs in this repository asdf_schema_standard_prefix = 'stsci.edu/gwcs' asdf_schema_reference_mappings = [ ('tag:stsci.edu:asdf', 'http://asdf-standard.readthedocs.io/en/latest/generated/stsci.edu/asdf/'), ] # -- Project information ------------------------------------------------------ # This does not *have* to match the package name, but typically does project = setup_cfg['name'] author = setup_cfg['author'] copyright = '{0}, {1}'.format( datetime.datetime.now().year, setup_cfg['author']) # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. from pkg_resources import get_distribution release = get_distribution(project).version # for example take major/minor version = '.'.join(release.split('.')[:2]) # -- Options for HTML output --------------------------------------------------- # A NOTE ON HTML THEMES # The global astropy configuration uses a custom theme, 'bootstrap-astropy', # which is installed along with astropy. A different theme can be used or # the options for this theme can be modified by overriding some of the # variables set in the global configuration. The variables set in the # global configuration are listed below, commented out. # Add any paths that contain custom themes here, relative to this directory. # To use a different custom theme, add the directory containing the theme. #html_theme_path = [] # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. To override the custom theme, set this to the # name of a builtin theme or the name of a custom theme in html_theme_path. #html_theme = None # See sphinx-bootstrap-theme for documentation of these options # https://github.com/ryan-roemer/sphinx-bootstrap-theme html_theme_options = { 'logotext1': 'g', # white, semi-bold 'logotext2': 'wcs', # orange, light 'logotext3': ':docs' # white, light } # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = '' # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '' # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". html_title = '{0} v{1}'.format(project, release) # Output file base name for HTML help builder. htmlhelp_basename = project + 'doc' # -- Options for LaTeX output -------------------------------------------------- # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). #latex_documents = [('index', project + '.tex', project + u' Documentation', # author, 'manual')] # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [('index', project.lower(), project + u' Documentation', [author], 1)] ## -- Options for the edit_on_github extension ---------------------------------------- if eval(setup_cfg.get('edit_on_github')): extensions += ['astropy.sphinx.ext.edit_on_github'] versionmod = __import__(setup_cfg['name'] + '.version') edit_on_github_project = setup_cfg['github_project'] if versionmod.version.release: edit_on_github_branch = "v" + versionmod.version.version else: edit_on_github_branch = "master" edit_on_github_source_root = "" edit_on_github_doc_root = "docs" sys.path.insert(0, os.path.join(os.path.dirname('__file__'), 'sphinxext')) extensions += ['sphinx_asdf'] gwcs-0.12.0/docs/gwcs/0000755000732200020070000000000013600427270016600 5ustar denchevaSTSCI\science00000000000000gwcs-0.12.0/docs/gwcs/ifu-regions.png0000644000732200020070000003335713600426757021561 0ustar denchevaSTSCI\science00000000000000PNG  IHDR,d(sBIT|d pHYsaa?i IDATxuŒ uU<8f T`O&U:)N\\$݄tϕ- Kzr5c@ejq0Geݮky^tySST*(PjV P,K,@ P,K,@ P,K,@ P,K,@ P,K,@ P,K,@ P,K,@ P,K,@ P,K,@ P,K,@ P,K,@ P,K,@ P,K,@ P,@uYihhȾÇgO`um//O|"w~駟 /P!j*JCysQG墋. 7Pq\@ke֬Y%a=ƍ vX=I۶m/~y4hPUfxꩧ+t Ȱa:?C8p`~eĉY~}ry+_J e3pI@q뭷//=.r-'?SOe]:O>dh1bD{>ꪫ8>hyᇻ|.z>'?;iY^sIw2tsiٳ35^|iY&Y^Oy#V& ~?wwWɐbmLr.w߿r^`_'ko\{˓$zjYYޥw:HM 80O?tjH}m֭sgH^Դc׬YgɓaÆ{gĈy極q/R.p߿N>;vdÆ ?~~0}͛$͹w{ܹsSWW+VdKp 6lX-Z 1L6-OWx={v3r$evرc_GyKz[}nkkkVXɓ'J :4ƍwݶrl߾=SNmԩSSTr=}1bDN{ȣ>^xaOXm<3iii1ѣG駟Ύ;??cݺum:3hР_mmݺuogvKFsssc T*ټys /uuuo+I6m{ը`6ɺ7TckgBJnF^H %aocIv#f6mJMMMێݾ}{ZZ:DuӦMm{طs368ڵk3lذ7IIy/b3jԿѣr$a~MjP6铉'殻֭[֟{Z*w^ڄ R[[KcҥɤIA{nxgΜ9sАO?==XrgĈm~ӟŋsg…9o~3O=T{Pxߛ>}z}$;?~ٲeYlYjjjaÆ :4GuTV^Yfeʔ)ӧO>яfѢE5ꪫ2`,^8-A+2W_}u3s̼k9r}SN }oÆ :<ЩcgΜ3gqpy뮭^B,@ P,K,@ P,K^bveGG P,K,@ P,K,@ P,K,@ P,K,@ P,K,@ P,K,@ P,K,mĵ՞x P,ިk=`z'@X(`%XxKפ#>'Xb X(`%Xb X(`է}89k~|{~_=,@ P,@XzuzۯG}C./9ƍkvGWiz@R9)5_S1w`ذa?jA0TR``/D7cƌ>3a„==K_-o}>466fر8p`z|_رcs3ځ+:傃v~jo={m#H=܌=:f,tK6>?۫==`*jjj==`l޼9wq۷oǡp ]ⓟdO$|SOk_Z~_U! ]cɝwޙŋg֭ihh)[o5'pBǣ,tYfe֬Y=,@ P,K,@ P,K{Py# P,K,@ 5][ `褟'?N3r|K_ʶmf͚?> H}}}&O 6v믿>#FHmmm?̛7/q:#NXvm,|Ƚޛ_K͛ .'x"cǎMkkk-[n)O>dN9lܸݞ]w]3eʔ>}z,X3ft@T{;Ȏ;|~Ic^Ȓ%Ke˖;wnbŊ~IN8!Æ ˢEp$IsssϟiӦeISO=5zfϞƌ9:'  @'&Ivz}5+Vɓb%Iqn[[reoߞSoԩT*{l,0upKaÆ+YbE,Y3f.fݺuyWۭ?N޽{gĉ뮻uֶc{Z*w^ڄ R[[KoҥɤIdqI@'|ĉs|Gy$ .G}cI3&sNl۶-sseW__ٳggΜ9ihh駟{,MMM3bĈj*;,pYgeؘ';N._sC%\qOOR8r-yᇳxb;"XRկؘ 80}==Ֆ߼f̘}CKf0jN`ί]Yp[?gYbE~ӟvp ]b֭g>~2dH^~$Ɏ;$[lI>}ҿjI@ظqc^z,Z( m_wqG^} /;,t:(Vj+J% .̏~\2 .ѯ_vio;zjqIݪ'inu77C`(`%Xb X(`%Xb XnUs5D,@ĵ՞K,@ P,K,@ P,K,@ P,K,@ ;9묳А}7Çf͚?> H}}}&O 6v믿>#FHmmm?̛7/q*N!T{Iv[Ǝ|;}ݗYf;'رcښe˖妛nʓO>SN9%7nlwu]L2%O dƌyZP>'x3mڴ\r%O;v͝;7uuuYbEo$ 'aÆeѢEYpa9ϴiޡ9Sgillȑ#\ao1ZwTvښ+Vdm$C͸qrw\2۷oԩS1uT*s={$,Ce~c=6O K/4JgIKKK9=zt~ر#Inݺ]x4hP֯_g=K:ϫꪫ8>h暬[.?p$ АJ͛7gȐ!innN~RWWvoe7J2z7kmuoZk ,o%^{m.$;9۷oU&$9^ xgF^H %a0p$əgn}„ IΠA$6mM6&mm߾=--iӦ; {|&GqDwx|ڵ6lX$m_|15jz6 'ON|o~&IN<;'N]wݕ[seժU9&L,]~K.MMMM&MEg={X:a9s2o޼9$ĉ|$IԔ1csW\m۶eܹzO`e˖ :4's!d֭[sEjH XvZN;vkg}v6lؐ%K:%atO?7s@T*o͛7gٲe~o|B)n_Z˖}ޥ^%K$IzEK/:eGgG[{a͋L6-/R _BZZZ2k֬`K}?$Ʉ $sɧ?pMt1cƤ56l(njժ;GqDGpI]bڴi3f̘ 2$7n̲e~7_~yX ]O7ߜ?g^~~9cs-Z! ]⢋.E]T1K,@ P,K,@ P,K,@ P,K,@ P,K,@ P,K,@ P,K,@ P>wHj$7}wK,@ P,K,@ P,K,@ p7W^0`@֬YgɓaÆsgĈy極CC?|/NMMMǞx≌;6YlYn<9Sqv^wuill̔)SrgY`Af̘ѝESzK.$ƍ>,_csM]]]VX/Ir 'dذaYhQ.\$inn3mڴ̟??Irꩧ_ٳؘ#Gv@r-yᇳxT*vfŊ\r%}9I᱆T*l޼9C Isss뗺׷{+ԾimTѻ9^k{ZK5`˗gŊOZQLHrPvkt:$K0 \6nݚ|3g?!C_/vyז-[ꫯfIM6ucӦMI}}}dپ}{ZZ:uӦMm{`x7nK/E;ȫ\x9#SWWk׮Ͱaҷo$iǾinnΨQpI8蠃jժvRTpG?ʕ+3hР;'N]wݕ|+m)˪Ure=„ ҥKmK.MMMM&M}',o_~9nIDAT:|ݻwN=Զ3&sNl۶-sK}}}fϞ9s椡!~z{455/Έ#t. xjjj:|QGիWg}ɔ)S2u ><=PR|_sgfŹ+x< (wXޥo97|s?><@9sffΜGwXb *7U{K,@ P,K,@ P,K,@ P, #@|Sӿz衙4iR֬YSA ][V{|}ݗۿK/N:)Vx}={n!n6a„yY`AƍWI@xs$I3r򗿬DDne˖Y&G}tGpIݪrFSGޭ{oO{[3fȶmrW؛ : v~gkO̙n-7pC;.KrMMM`L>ЃTSSSW\Qqa ]K_R2gΜ̙3.}-\sM&L:+:I3Ϥ%sLcG~:;vH[m}Wx` w3$ ]1cFmۖ:˼ñ T*ټys /uuuokV&}ڨ$ws,н&Yj { .̙3'v[nwqͯ tt ITa;$ jjju] dmLlڴs6mڔ׷}tM;455}]q;#RWW֮]aÆo߾Iv˛}ܜQFu@7um'/)sɜ9s:<ާOL81wuWnڶseժU9&L,]K.MMMM&Me={X:k_ZL0!guVyvtIIv3f̘s9+m۶̝;7e]v|}}}fϞ9s椡!~z{455/Έ#TVX\2+WlXMMM~&I:ꨬ^:fʔ)SҧO|͢E:ܗrUWeYxq-Z:(W^yeo @Zy:u̙33sw;K%nݚ/d/[,˖-KMMM6lؐCVyBJ'X26lpa%Xb X(`巄bI``Owa%Xb X(`%Xb 5iX(`%Xb X(`%Xb X(`%Xb RSӈksxN,\(`%Xb X(`%Xb X(`%Xb X(`%Xb X(`%Xb M匦j@#Xb X(`%Xb K̯[8K,5_Sf(`%Xb `um'(`%Xb X(`%Xz=v ՞ཹjO^on@*ںuks!.w\ηy32[WjO`YW#sT{;/?Os뭷 .o .g^ *ߞO|IN;->lo&'ҫ7OfWvT{أ|cOO@}0`@?vSNͿۿ4{wXdݺu9rd.=:I~|n[oiiݟv$={˯'k~^vwyfzaq}񖼣y=̲jYYYޱIm۶U_{~s;JsGvXohhh{|W6lݟ^LSѹv]=rr#e=;O0fV;{]fE%u_G>OO0`@w}vGgyfnva8@'dÆ 93&MJ1"oƀ2lذ. @ 8p?]ܴiS4hP>Ovlu'W [U^~q=@s17xڵ;o5jT5c|cO,Urf֭Y|yKCɉ'X \P%&L駟K/4orGoߟ[o5555=}=;,Ut]w /ܹs},=Xu466CI]]];yUz<_~y8pիWv{5k2~ 0 #FHmmm?̛7/{|ԧ>Ç9C3iҤYW8˿g}s'[oٍ7ޘ^zL:}R~ʒ%K*W\|ŕmVy6lP>P;vl,MMM^0`@Nw}2jԨ!RWzU+?я*_W+L6mWƎ[7Yя~TY|ype}<Ͽz%\R[+WXrTjjj*/~]򗿬C92`v'X wVjjj*wqG38r!T~Vi6nrW\yW֞}J߾}+fjGmmmK.i TzU~Ggկ~am֭<2~+'tReСm'9T&MT袋*_z@sI@ 0 ~SUlJ֬X"'O~׶>tЌ7.w}wʕ+}L:SNMR=ܳGg$=cJ׿7?1~466f…9;<^@,knnNCCC߯CJjJ͛7ۯ_u8[qƌٶm[궙~?뛕2^}fȐ!>EK/Ϙ1#Їr%_k ޜ9srmnqWq:ꫯδiK/{^/%fͪh˳bŊ?(@ X 7pxӦMms~]mڴ)555o;viiiImmmcnjes655˂ 2}5?|L.WWοu|3g? 2$/r]޵e˖ӧ0s17xڵk$FXo#H]]]]6Æ K߾}>7/α+q٘1cښ-v7楗^ʢEuwW_M}}}.yEt?Ps=7[nۭ/]4rHN<*M''N]wݕ[?sYjU;V &6K.mҥKSSSI&/)sɜ9szjժ;GqDtAYjUV^jժy晩ի3ݻ*үS83* oە|YT*+˖-tM?ʲe*˖-kJRy'vzheƍ>oՕ~__-ZT|c<#~/ʝwYYzueO|vORO}#Xz[V>U:J~*{l;LvXRSSSիW??m?SeWyWo|rQGUW9*MMM=>رcͼW^[7|sSOp}٧R___7n\[oplE]+3?uj*/*s P,K,@ P,K,@ P,K,@ P,K,@ P,K,@ P,K,@ P,K,u,0z;"`KX-a`KX-a`KX-a`KX-a :MǏ |dIENDB`gwcs-0.12.0/docs/gwcs/ifu.rst0000644000732200020070000001134613600426757020133 0ustar denchevaSTSCI\science00000000000000An IFU Example - managing a discontiguous WCS ============================================= An IFU image represents the projection of several slices on a detector. Between the slices there are pixels which don't belong to any slice. In general each slice has a unique WCS transform. There are two ways to represent this kind of transforms in GWCS depending on the way the instrument is calibrated and the available information. Using a pixel to slice mapping ------------------------------ In this case a pixel map associating each pixel with a slice label (number or string) is available. The image below represents the projection of the slits of an IFU on a detector with a size (500, 1000). Slices are labeled from 1 to 6, while label 0 is reserved for pixels between the slices. .. image:: ifu-regions.png There are several models in GWCS which are useful in creating a WCS. Given (x, y) pixel indices, `~gwcs.selector.LabelMapperArray` returns labels (int or str) associated with these indices. `~gwcs.selector.RegionsSelector` maps labels with transforms. It uses the `~gwcs.selector.LabelMapperArray` to map these transforms to pixel indices. A step by step example of constructing the WCS for an IFU with 6 slits follows. First, import the usual packages. >>> import numpy as np >>> from astropy.modeling import models >>> from astropy import coordinates as coord >>> from astropy import units as u >>> from gwcs import wcs, selector >>> from gwcs import coordinate_frames as cf The output frame is common for all slits and is a composite frame with two subframes, `~gwcs.coordinate_frames.CelestialFrame` and `~gwcs.coordinate_frames.SpectralFrame`. >>> sky_frame = cf.CelestialFrame(name='icrs', reference_frame=coord.ICRS(), axes_order=(0, 2)) >>> spec_frame = cf.SpectralFrame(name='wave', unit=(u.micron,), axes_order=(1,), axes_names=('lambda',)) >>> cframe = cf.CompositeFrame([sky_frame, spec_frame], name='world') >>> det = cf.Frame2D(name='detector') All slices have the same input and output frames, however each slices has a different model transforming from pixels to world coordinates (RA, lambda, dec). For the sake of brevity this example uses a simple shift transform for each slice. Detailed examples of how to create more realistic transforms are available in :ref:`imaging_example`. >>> transforms = {} >>> for i in range(1, 7): ... transforms[i] = models.Mapping([0, 0, 1]) | models.Shift(i * 0.1) & models.Shift(i * 0.2) & models.Scale(i * 0.1) One way to initialize `~gwcs.selector.LabelMapperArray` is to pass it the shape of the array and the vertices of each slit on the detector {label: vertices} see :meth: `~gwcs.selector.LabelMapperArray.from_vertices`. In this example the mask is an array with the size of the detector where each item in the array corresponds to a pixel on the detector and its value is the slice number (label) this pixel belongs to. Assuming the array is stored in `ASDF `__ format, create the mask: .. doctest-skip-all >>> import asdf >>> f = asdf.open('mask.asdf') >>> data = f.tree['mask'] >>> mask = selector.LabelMapperArray(data) Create the pixel to world transform for the entire IFU: >>> regions_transform = selector.RegionsSelector(inputs=['x','y'], ... outputs=['ra', 'dec', 'lam'], ... selector=transforms, ... label_mapper=mask, ... undefined_transform_value=np.nan) The WCS object now can evaluate simultaneously the transforms of all slices. >>> wifu = wcs.WCS(forward_transform=regions_transform, output_frame=cframe, input_frame=det) >>> y, x = mask.mapper.shape >>> y, x = np.mgrid[:y, :x] >>> r, d, l = wifu(x, y) or of single slices. The :meth:`~gwcs.selector.RegionsSelector.set_input` method returns the forward_transform for a specific label. >>> wifu.forward_transform.set_input(4)(1, 2) (1.4, 1.8, 0.8) Custom model storing transforms in a dictionary ----------------------------------------------- In case a pixel to slice mapping is not available, one can write a custom mdoel storing transforms in a dictionary. The model would look like this: .. code:: from astropy.modeling.core import Model from astropy.modeling.parameters import Parameter class CustomModel(Model): inputs = ('label', 'x', 'y') outputs = ('xout', 'yout') def __init__(self, labels, transforms): super().__init__() self.labels = labels self.models = models def evaluate(self, label, x, y): index = self.labels.index(label) return self.models[index](x, y) gwcs-0.12.0/docs/gwcs/imaging_with_distortion.rst0000644000732200020070000000573413600426757024300 0ustar denchevaSTSCI\science00000000000000.. _imaging_example: Adding distortion to the imaging example ======================================== Let's expand the WCS created in :ref:`getting-started` by adding a polynomial distortion correction. Because the polynomial models in `~astropy.modeling` do not support units yet, this example will use transforms without units. At the end the units associated with the output frame are used to create a `~astropy.coordinates.SkyCoord` object. The imaging example without units: >>> import numpy as np >>> from astropy.modeling import models >>> from astropy import coordinates as coord >>> from astropy import units as u >>> from gwcs import wcs >>> from gwcs import coordinate_frames as cf >>> shift_by_crpix = models.Shift(-2048) & models.Shift(-1024) >>> matrix = np.array([[1.290551569736E-05, 5.9525007864732E-06], ... [5.0226382102765E-06 , -1.2644844123757E-05]]) >>> rotation = models.AffineTransformation2D(matrix) >>> rotation.inverse = models.AffineTransformation2D(np.linalg.inv(matrix)) >>> tan = models.Pix2Sky_TAN() >>> celestial_rotation = models.RotateNative2Celestial(5.63056810618, -72.05457184279, 180) >>> det2sky = shift_by_crpix | rotation | tan | celestial_rotation >>> det2sky.name = "linear_transform" >>> detector_frame = cf.Frame2D(name="detector", axes_names=("x", "y"), ... unit=(u.pix, u.pix)) >>> sky_frame = cf.CelestialFrame(reference_frame=coord.ICRS(), name='icrs', ... unit=(u.deg, u.deg)) >>> pipeline = [(detector_frame, det2sky), ... (sky_frame, None) ... ] >>> wcsobj = wcs.WCS(pipeline) >>> print(wcsobj) From Transform -------- ---------------- detector linear_transform icrs None First create distortion corrections represented by a polynomial model of fourth degree. The example uses the astropy `~astropy.modeling.polynomial.Polynomial2D` and `~astropy.modeling.mappings.Mapping` models. >>> poly_x = models.Polynomial2D(4) >>> poly_x.parameters = np.arange(15) * .1 >>> poly_y = models.Polynomial2D(4) >>> poly_y.parameters = np.arange(15) * .2 >>> distortion = models.Mapping((0, 1, 0, 1)) | poly_x & poly_y >>> distortion.name = "distortion" Create an intermediate frame for distortion free coordinates. >>> undistorted_frame = cf.Frame2D(name="undistorted_frame", unit=(u.pix, u.pix), ... axes_names=("undist_x", "undist_y")) Using the example in :ref:`getting-started`, add the distortion correction to the WCS pipeline and initialize the WCS. >>> pipeline = [(detector_frame, distortion), ... (undistorted_frame, det2sky), ... (sky_frame, None) ... ] >>> wcsobj = wcs.WCS(pipeline) >>> print(wcsobj) From Transform ----------------- ---------------- detector distortion undistorted_frame linear_transform icrs None gwcs-0.12.0/docs/gwcs/points_to_wcs.rst0000644000732200020070000001077713600426757022251 0ustar denchevaSTSCI\science00000000000000 .. _wcs_from_points_example: Fitting a WCS to input pixels & sky positions ============================================= Suppose we have an image where we have centroid positions for a number of sources, and we have matched these positions to an external catalog to obtain (RA, Dec). If this data is missing or has inaccurate WCS information, it is useful to fit or re-fit a GWCS object with this matched list of coordinate pairs to be able to transform between pixel and sky. This example shows how to use the `~gwcs.wcstools.wcs_from_points` tool to fit a WCS to a matched set of pixel and sky positions. Along with arrays of the (x,y) pixel position in the image and the matched sky coordinates, the fiducial point for the projection must be supplied as a `~astropy.coordinates.SkyCoord` object. Additionally, the projection type must be specified from the available projections in `~astropy.modeling.projections.projcode`. Geometric distortion can also be fit to the input coordinates - the distortion type (2D polynomial, chebyshev, legendre) and the degree can be supplied to fit this component of the model. The following example will show how to fit a WCS, including a 4th degree 2D polynomial, to a set of input pixel positions of sources in an image and their corresponding positions on the sky obtained from a catalog. Import the wcs_from_points function, >>> from gwcs.wcstools import wcs_from_points along with some useful general imports. >>> from astropy.coordinates import SkyCoord >>> from astropy.io import ascii >>> import astropy.units as u >>> import numpy as np A collection of 20 matched coordinate pairs in x, y, RA, and Dec stored in two arrays, will be used to fit the WCS information. The function requires tuples of arrays. >>> xy = (np.array([2810.156, 2810.156, 650.236, 1820.927, 3425.779, 2750.369, ... 212.422, 1146.91 , 27.055, 2100.888, 648.149, 22.212, ... 2003.314, 727.098, 248.91 , 409.998, 1986.931, 128.925, ... 1106.654, 1502.67 ]), ... np.array([1670.347, 1670.347, 360.325, 165.663, 900.922, 700.148, ... 1416.235, 1372.364, 398.823, 580.316, 317.952, 733.984, ... 339.024, 234.29 , 1241.608, 293.545, 1794.522, 1365.706, ... 583.135, 25.306])) >>> radec = (np.array([246.75001315, 246.75001315, 246.72033646, 246.72303144, ... 246.74164072, 246.73540614, 246.73379121, 246.73761455, ... 246.7179495 , 246.73051123, 246.71970072, 246.7228646 , ... 246.72647213, 246.7188386 , 246.7314031 , 246.71821002, ... 246.74785534, 246.73265223, 246.72579817, 246.71943263]), ... np.array([43.48690547, 43.48690547, 43.46792989, 43.48075238, ... 43.49560501, 43.48903538, 43.46045875, 43.47030776, ... 43.46132376, 43.48252763, 43.46802566, 43.46035331, ... 43.48218262, 43.46908299, 43.46131665, 43.46560591, ... 43.47791234, 43.45973025, 43.47208325, 43.47779988])) We can now choose the reference point on the sky for the projection. This is passed in as a `~astropy.coordinates.SkyCoord` object so that information about the celestial frame and units is given as well. The input world coordinates are passed in as unitless arrays, and so are assumed to be of the same unit and frame as the fiducial point. >>> proj_point = SkyCoord(246.7368408, 43.480712949, frame = 'icrs', unit = (u.deg,u.deg)) We can now call the function that returns a GWCS object corresponding to the best fit parameters that relate the input pixels and sky coordinates with a TAN projection centered at the reference point we specified, with a distortion model (degree 4 polynomial). This function will return a GWCS object that can be used to transform between coordinate frames. >>> gwcs_obj = wcs_from_points(xy, radec, proj_point) This GWCS object contains parameters for a TAN projection, rotation, scale, skew and a polynomial fit to x and y that represent the best-fit to the input coordinates. With WCS information associated with the data now, we can easily work in both pixel and sky space, and transform between frames. The GWCS object, which by default when called executes for forward transformation, can be used to convert coordinates from pixel to world. >>> gwcs_obj(36.235,642.215) # doctest: +FLOAT_CMP (246.72158004206716, 43.46075091731673) Or equivalently >>> gwcs_obj.forward_transform(36.235,642.215) # doctest: +FLOAT_CMP (246.72158004206716, 43.46075091731673) gwcs-0.12.0/docs/gwcs/pure_asdf.rst0000644000732200020070000001235613600426757021322 0ustar denchevaSTSCI\science00000000000000.. _pure_asdf: Listing of ``imaging_wcs.asdf`` =============================== Listing of ``imaging_wcs.asdf``:: #ASDF 1.0.0 #ASDF_STANDARD 1.2.0 %YAML 1.1 %TAG ! tag:stsci.edu:asdf/ --- !core/asdf-1.1.0 asdf_library: !core/software-1.0.0 {author: Space Telescope Science Institute, homepage: 'http://github.com/spacetelescope/asdf', name: asdf, version: 2.2.0.dev1526} history: extensions: - !core/extension_metadata-1.0.0 extension_class: asdf.extension.BuiltinExtension software: {name: asdf, version: 2.2.0.dev1526} - !core/extension_metadata-1.0.0 extension_class: astropy.io.misc.asdf.extension.AstropyExtension software: {name: astropy, version: 3.2.dev23222} - !core/extension_metadata-1.0.0 extension_class: astropy.io.misc.asdf.extension.AstropyAsdfExtension software: {name: astropy, version: 3.2.dev23222} - !core/extension_metadata-1.0.0 extension_class: gwcs.extension.GWCSExtension software: {name: gwcs, version: 0.10.dev417} wcs: ! name: '' steps: - ! frame: ! axes_names: [x, y] name: detector unit: [!unit/unit-1.0.0 pixel, !unit/unit-1.0.0 pixel] transform: !transform/compose-1.1.0 forward: - !transform/remap_axes-1.1.0 mapping: [0, 1, 0, 1] - !transform/concatenate-1.1.0 forward: - !transform/polynomial-1.1.0 coefficients: !core/ndarray-1.0.0 data: - [0.0, 0.5, 0.6000000000000001, 0.7000000000000001, 0.8] - [0.1, 0.9, 1.0, 1.1, 0.0] - [0.2, 1.2000000000000002, 1.3, 0.0, 0.0] - [0.30000000000000004, 1.4000000000000001, 0.0, 0.0, 0.0] - [0.4, 0.0, 0.0, 0.0, 0.0] datatype: float64 shape: [5, 5] - !transform/polynomial-1.1.0 coefficients: !core/ndarray-1.0.0 data: - [0.0, 1.0, 1.2000000000000002, 1.4000000000000001, 1.6] - [0.2, 1.8, 2.0, 2.2, 0.0] - [0.4, 2.4000000000000004, 2.6, 0.0, 0.0] - [0.6000000000000001, 2.8000000000000003, 0.0, 0.0, 0.0] - [0.8, 0.0, 0.0, 0.0, 0.0] datatype: float64 shape: [5, 5] - ! frame: ! axes_names: [undist_x, undist_y] name: undistorted_frame unit: [!unit/unit-1.0.0 pixel, !unit/unit-1.0.0 pixel] transform: !transform/compose-1.1.0 forward: - !transform/compose-1.1.0 forward: - !transform/compose-1.1.0 forward: - !transform/concatenate-1.1.0 forward: - !transform/shift-1.2.0 {offset: -2048.0} - !transform/shift-1.2.0 {offset: -1024.0} - !transform/affine-1.2.0 inverse: !transform/affine-1.2.0 matrix: !core/ndarray-1.0.0 data: - [65488.318039522, 30828.31712434267] - [26012.509548778366, -66838.34993781192] datatype: float64 shape: [2, 2] translation: !core/ndarray-1.0.0 data: [0.0, 0.0] datatype: float64 shape: [2] matrix: !core/ndarray-1.0.0 data: - [1.290551569736e-05, 5.9525007864732e-06] - [5.0226382102765e-06, -1.2644844123757e-05] datatype: float64 shape: [2, 2] translation: !core/ndarray-1.0.0 data: [0.0, 0.0] datatype: float64 shape: [2] - !transform/gnomonic-1.1.0 {direction: pix2sky} - !transform/rotate3d-1.2.0 {phi: 5.63056810618, psi: 180.0, theta: -72.05457184279} inverse: !transform/compose-1.1.0 forward: - !transform/rotate3d-1.2.0 {direction: celestial2native, phi: 5.63056810618, psi: 180.0, theta: -72.05457184279} - !transform/compose-1.1.0 forward: - !transform/gnomonic-1.1.0 {direction: sky2pix} - !transform/compose-1.1.0 forward: - !transform/affine-1.2.0 matrix: !core/ndarray-1.0.0 data: - [65488.318039522, 30828.31712434267] - [26012.509548778366, -66838.34993781192] datatype: float64 shape: [2, 2] translation: !core/ndarray-1.0.0 data: [0.0, 0.0] datatype: float64 shape: [2] - !transform/concatenate-1.1.0 forward: - !transform/shift-1.2.0 {offset: 2048.0} - !transform/shift-1.2.0 {offset: 1024.0} name: linear_transform - ! frame: ! axes_names: [lon, lat] name: icrs reference_frame: ! frame_attributes: {} unit: [!unit/unit-1.0.0 deg, !unit/unit-1.0.0 deg] ... gwcs-0.12.0/docs/gwcs/schemas/0000755000732200020070000000000013600427270020223 5ustar denchevaSTSCI\science00000000000000gwcs-0.12.0/docs/gwcs/schemas/index.rst0000644000732200020070000000066713600426757022106 0ustar denchevaSTSCI\science00000000000000.. _gwcs-schemas: GWCS Schema Definitions ======================= WCS object ---------- .. asdf-autoschemas:: wcs-1.0.0 step-1.0.0 Coordinate Frames ----------------- .. asdf-autoschemas:: celestial_frame-1.0.0 frame-1.0.0 spectral_frame-1.0.0 frame2d-1.0.0 temporal_frame-1.0.0 composite_frame-1.0.0 Transforms ---------- .. asdf-autoschemas:: label_mapper-1.0.0 regions_selector-1.0.0 gwcs-0.12.0/docs/gwcs/using_wcs.rst0000644000732200020070000000614413600426757021351 0ustar denchevaSTSCI\science00000000000000.. _user_api: Using the WCS object ==================== This section uses the ``imaging_wcs.asdf`` created in :ref:`imaging_example` to read in a WCS object and demo its methods. .. doctest-skip:: >>> import asdf >>> asdf_file = asdf.open("imaging_wcs.asdf") >>> wcsobj = asdf_file.tree["wcs"] >>> print(wcsobj) # doctest: +SKIP From Transform ----------------- ---------------- detector distortion undistorted_frame linear_transform icrs None To see what frames are defined: .. doctest-skip:: >>> print(wcsobj.available_frames) ['detector', 'undistorted_frame', 'icrs'] >>> wcsobj.input_frame >>> wcsobj.output_frame )> Because the ``output_frame`` is a `~gwcs.coordinate_frames.CoordinateFrame` object we can get the result of the WCS transform as an `~astropy.coordinates.SkyCoord` object and transform them to other standard coordinate frames supported by `astropy.coordinates`. .. doctest-skip:: >>> skycoord = wcsobj(1, 2, with_units=True) >>> print(skycoord) >>> print(skycoord.transform_to("galactic")) The WCS object has an attribute :attr:`~gwcs.WCS.bounding_box` (default value of ``None``) which describes the range of acceptable values for each input axis. .. doctest-skip:: >>> wcsobj.bounding_box = ((0, 2048), (0, 1000)) >>> wcsobj((2,3), (1020, 980)) array([nan, 133.48248429]), array([nan, -11.24021056]) The WCS object accepts a boolean flag called ``with_bounding_box`` with default value of ``True``. Output values which are outside the ``bounding_box`` are set to ``NaN``. There are cases when this is not desirable and ``with_bounding_box=False`` should be passes. Calling the :meth:`~gwcs.WCS.footprint` returns the footprint on the sky. .. doctest-skip:: >>> wcsobj.footprint() Some methods allow managing the transforms in a more detailed manner. Transforms between frames can be retrieved and evaluated separately. .. doctest-skip:: >>> dist = wcsobj.get_transform('detector', 'undistorted_frame') >>> dist(1, 2) # doctest: +FLOAT_CMP (47.8, 95.60) Transforms in the pipeline can be replaced by new transforms. .. doctest-skip:: >>> new_transform = models.Shift(1) & models.Shift(1.5) | distortion >>> wcsobj.set_transform('detector', 'focal_frame', new_transform) >>> wcsobj(1, 2) # doctest: +FLOAT_CMP (5.5583005430002785, -72.06028278184611) A transform can be inserted before or after a frame in the pipeline. .. doctest-skip:: >>> scale = models.Scale(2) & models.Scale(1) >>> wcsobj.insert_transform('icrs', scale, after=False) >>> wcsobj(1, 2) # doctest: +FLOAT_CMP (11.116601086000557, -72.06028278184611) gwcs-0.12.0/docs/gwcs/wcs_ape.rst0000644000732200020070000000161513600426757020767 0ustar denchevaSTSCI\science00000000000000.. _ape14: Common Interface for World Coordinate System - APE 14 ===================================================== To improve interoperability between packages, the Astropy Project and other interested parties have collaboratively defined a standardized application programming interface (API) for world coordinate system objects to be used in Python. This API is described in the Astropy Proposal for Enhancements (APE) 14: `A shared Python interface for World Coordinate Systems `_. The base classes that define the low- (`~astropy.wcs.wcsapi.BaseLowLevelWCS`) and high- (:class:`~astropy.wcs.wcsapi.BaseHighLevelWCS`) level APIs are in astropy. GWCS implements both APIs. Once a gWCS object is created the API methods will be available. It is recommended that applications use the ``Common API`` to ensure transparent use of ``GWCS`` and ``FITSWCS`` objects. gwcs-0.12.0/docs/gwcs/wcs_validation.rst0000644000732200020070000000201613600426757022350 0ustar denchevaSTSCI\science00000000000000.. _wcs_validation: WCS validation ============== The WCS is validated when an object is read in or written to a file. However, this happens transparently to the end user and knowing the details of the validation machinery is not necessary to use or construct a WCS object. GWCS uses the `Advanced Scientific Data Format `__ (ASDF) to validate the transforms, coordinate frames and the overall WCS object structure. ASDF makes use of abstract data type definitions called ``schemas``. The serialization and deserialization happens in classes, referred to as ``tags``. Most of the transform schemas live in the ``asdf-standard`` package while most of the transform tags live in ``astropy``. :ref:`gwcs-schemas` are available for the WCS object, coordinate frames and some WCS specific transforms. Packages using GWCS may create their own transforms and schemas and register them as an ``Asdf Extension``. If those are of general use, it is recommended they be included in astropy. gwcs-0.12.0/docs/gwcs/wcstools.rst0000644000732200020070000000236713600426757021230 0ustar denchevaSTSCI\science00000000000000WCS User Tools ============== `~gwcs.wcstools.grid_from_bounding_box` is a function which returns a grid of input points based on the bounding_box of the WCS. >>> from gwcs.wcstools import grid_from_bounding_box >>> bounding_box = ((0, 4096), (0, 2048)) >>> x, y = grid_from_bounding_box(bounding_box) >>> ra, dec = w(x, y) # doctest: +SKIP The `~gwcs.wcstools` module contains functions of general usability. `~gwcs.wcstools.wcs_from_fiducial` is a function which given a fiducial in some coordinate system, returns a WCS object. >>> from gwcs.wcstools import wcs_from_fiducial >>> from astropy import coordinates as coord >>> from astropy import units as u >>> from astropy.modeling import models To create a WCS from a pointing on the sky, as a minimum pass a sky coordinate and a projection to the function. >>> fiducial = coord.SkyCoord(5.46 * u.deg, -72.2 * u.deg, frame='icrs') >>> tan = models.Pix2Sky_TAN() Any additional transforms are prepended to the projection and sky rotation. >>> trans = models.Shift(-2048) & models.Shift(-1024) | models.Scale(1.38*10**-5) & models.Scale(1.38*10**-5) >>> w = wcs_from_fiducial(fiducial, projection=tan, transform=trans) >>> w(2048, 1024) # doctest: +FLOAT_CMP (5.46, -72.2) gwcs-0.12.0/docs/index.rst0000644000732200020070000003511213600426757017511 0ustar denchevaSTSCI\science00000000000000GWCS Documentation ================== `GWCS `__ is a package for managing the World Coordinate System (WCS) of astronomical data. Introduction & Motivation for GWCS ---------------------------------- The mapping from ‘pixel’ coordinates to corresponding ‘real-world’ coordinates (e.g. celestial coordinates, spectral wavelength) is crucial to relating astronomical data to the phenomena they describe. Images and other types of data often come encoded with information that describes this mapping – this is referred to as the ‘World Coordinate System’ or WCS. The term WCS is often used to refer specifically to the most widely used 'FITS implementation of WCS', but here unless specified WCS refers to the broader concept of relating pixel ⟷ world. (See the discussion in `APE14 `__ for more on this topic). The FITS WCS standard, currently the most widely used method of encoding WCS in data, describes a set of required FITS header keywords and allowed values that describe how pixel ⟷ world transformations should be done. This current paradigm of encoding data with only instructions on how to relate pixel to world, separate from the transformation machinery itself, has several limitations: * Limited flexibility. WCS keywords and their values are rigidly defined so that the instructions are unambiguous. This places limitations on, for example, describing geometric distortion in images since only a handful of distortion models are defined in the FITS standard (and therefore can be encoded in FITS headers as WCS information). * Separation of data from transformation pipelines. The machinery that transforms pixel ⟷ world does not exist along side the data – there is merely a roadmap for how one *would* do the transformation. External packages and libraries (e.g wcslib, or its Python interface astropy.wcs) must be written to interpret the instructions and execute the transformation. These libraries don’t allow easy access to coordinate frames along the course of the full pixel to world transformation pipeline. Additionally, since these libraries can only interpret FITS WCS information, any custom ‘WCS’ definitions outside of FITS require the user to write their own transformation pipelines. * Incompatibility with varying file formats. New file formats that are becoming more widely used in place of FITS to store astronomical data, like the ASDF format, also require a method of encoding WCS information. FITS WCS and the accompanying libraries are adapted for FITS only. A more flexible interface would be agnostic to file type, as long as the necessary information is present. The `GWCS `__ package and GWCS object is a generalized WCS implementation that mitigates these limitations. The goal of the GWCS package is to provide a flexible toolkit for expressing and evaluating transformations between pixel and world coordinates, as well as intermediate frames along the course of this transformation.The GWCS object supports a data model which includes the entire transformation pipeline from input pixel coordinates to world coordinates (and vice versa). The basis of the GWCS object is astropy `modeling `__. Models that describe the pixel ⟷ world transformations can be chained, joined or combined with arithmetic operators using the flexible framework of compound models in modeling. This approach allows for easy access to intermediate frames. In the case of a celestial output frame `coordinates `__ provides further transformations between standard celestial coordinate frames. Spectral output coordinates are instances of `~astropy.units.Quantity` and can be transformed to other units with the tools in that package. `~astropy.time.Time` coordinates are instances of `~astropy.time.Time`. GWCS supports transforms initialized with `~astropy.units.Quantity` objects ensuring automatic unit conversion. Pixel Conventions and Definitions --------------------------------- This API assumes that integer pixel values fall at the center of pixels (as assumed in the FITS-WCS standard, see Section 2.1.4 of `Greisen et al., 2002, A&A 446, 747 `_), while at the same time matching the Python 0-index philosophy. That is, the first pixel is considered pixel ``0``, but pixel coordinates ``(0, 0)`` are the *center* of that pixel. Hence the first pixel spans pixel values ``-0.5`` to ``0.5``. There are two main conventions for ordering pixel coordinates. In the context of 2-dimensional imaging data/arrays, one can either think of the pixel coordinates as traditional Cartesian coordinates (which we call ``x`` and ``y`` here), which are usually given with the horizontal coordinate (``x``) first, and the vertical coordinate (``y``) second, meaning that pixel coordinates would be given as ``(x, y)``. Alternatively, one can give the coordinates by first giving the row in the data, then the column, i.e. ``(row, column)``. While the former is a more common convention when e.g. plotting (think for example of the Matplotlib ``scatter(x, y)`` method), the latter is the convention used when accessing values from e.g. Numpy arrays that represent images (``image[row, column]``). The GWCS object assumes Cartesian order ``(x, y)``, however the :ref:`ape14` accepts both conventions. The order of the pixel coordinates (``(x, y)`` vs ``(row, column)``) in the ``Common API`` depends on the method or property used, and this can normally be determined from the property or method name. Properties and methods containing ``pixel`` assume ``(x, y)`` ordering, while properties and methods containing ``array`` assume ``(row, column)`` ordering. Installation ------------ `gwcs `__ requires: - `numpy `__ - `astropy `__ - `asdf `__ To install from source:: git clone https://github.com/spacetelescope/gwcs.git cd gwcs python setup.py install To install the latest release:: pip install gwcs The latest release of GWCS is also available as part of `astroconda `__. .. _getting-started: Getting Started --------------- The WCS data model represents a pipeline of transformations between two coordinate frames, the final one usually a physical coordinate system. It is represented as a list of steps executed in order. Each step defines a starting coordinate frame and the transform to the next frame in the pipeline. The last step has no transform, only a frame which is the output frame of the total transform. As a minimum a WCS object has an ``input_frame`` (defaults to "detector"), an ``output_frame`` and the transform between them. The WCS is validated using the `ASDF Standard `__ and serialized to file using the `asdf `__ package. There are two ways to save the WCS to a file: - `Save a WCS object as a pure ASDF file`_ - `Save a WCS object as an ASDF extension in a FITS file`_ A step by step example of constructing an imaging GWCS object. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The following example shows how to construct a GWCS object equivalent to a FITS imaging WCS without distortion, defined in this FITS imaging header:: WCSAXES = 2 / Number of coordinate axes WCSNAME = '47 Tuc ' / Coordinate system title CRPIX1 = 2048.0 / Pixel coordinate of reference point CRPIX2 = 1024.0 / Pixel coordinate of reference point PC1_1 = 1.290551569736E-05 / Coordinate transformation matrix element PC1_2 = 5.9525007864732E-06 / Coordinate transformation matrix element PC2_1 = 5.0226382102765E-06 / Coordinate transformation matrix element PC2_2 = -1.2644844123757E-05 / Coordinate transformation matrix element CDELT1 = 1.0 / [deg] Coordinate increment at reference point CDELT2 = 1.0 / [deg] Coordinate increment at reference point CUNIT1 = 'deg' / Units of coordinate increment and value CUNIT2 = 'deg' / Units of coordinate increment and value CTYPE1 = 'RA---TAN' / TAN (gnomonic) projection + SIP distortions CTYPE2 = 'DEC--TAN' / TAN (gnomonic) projection + SIP distortions CRVAL1 = 5.63056810618 / [deg] Coordinate value at reference point CRVAL2 = -72.05457184279 / [deg] Coordinate value at reference point LONPOLE = 180.0 / [deg] Native longitude of celestial pole LATPOLE = -72.05457184279 / [deg] Native latitude of celestial pole RADESYS = 'ICRS' / Equatorial coordinate system The following imports are generally useful: >>> import numpy as np >>> from astropy.modeling import models >>> from astropy import coordinates as coord >>> from astropy import units as u >>> from gwcs import wcs >>> from gwcs import coordinate_frames as cf The ``forward_transform`` is constructed as a combined model using `astropy.modeling`. The ``frames`` are subclasses of `~gwcs.coordinate_frames.CoordinateFrame`. Although strings are acceptable as ``coordinate_frames`` it is recommended this is used only in testing/debugging. Using the `~astropy.modeling` package create a combined model to transform detector coordinates to ICRS following the FITS WCS standard convention. First, create a transform which shifts the input ``x`` and ``y`` coordinates by ``CRPIX``. We subtract 1 from the CRPIX values because the first pixel is considered pixel ``1`` in FITS WCS: >>> shift_by_crpix = models.Shift(-(2048 - 1)*u.pix) & models.Shift(-(1024 - 1)*u.pix) Create a transform which rotates the inputs using the ``PC matrix``. >>> matrix = np.array([[1.290551569736E-05, 5.9525007864732E-06], ... [5.0226382102765E-06 , -1.2644844123757E-05]]) >>> rotation = models.AffineTransformation2D(matrix * u.deg, ... translation=[0, 0] * u.deg) >>> rotation.input_units_equivalencies = {"x": u.pixel_scale(1*u.deg/u.pix), ... "y": u.pixel_scale(1*u.deg/u.pix)} >>> rotation.inverse = models.AffineTransformation2D(np.linalg.inv(matrix) * u.pix, ... translation=[0, 0] * u.pix) >>> rotation.inverse.input_units_equivalencies = {"x": u.pixel_scale(1*u.pix/u.deg), ... "y": u.pixel_scale(1*u.pix/u.deg)} Create a tangent projection and a rotation on the sky using ``CRVAL``. >>> tan = models.Pix2Sky_TAN() >>> celestial_rotation = models.RotateNative2Celestial(5.63056810618*u.deg, -72.05457184279*u.deg, 180*u.deg) >>> det2sky = shift_by_crpix | rotation | tan | celestial_rotation >>> det2sky.name = "linear_transform" Create a ``detector`` coordinate frame and a ``celestial`` ICRS frame. >>> detector_frame = cf.Frame2D(name="detector", axes_names=("x", "y"), ... unit=(u.pix, u.pix)) >>> sky_frame = cf.CelestialFrame(reference_frame=coord.ICRS(), name='icrs', ... unit=(u.deg, u.deg)) This WCS pipeline has only one step - from ``detector`` to ``sky``: >>> pipeline = [(detector_frame, det2sky), ... (sky_frame, None) ... ] >>> wcsobj = wcs.WCS(pipeline) >>> print(wcsobj) From Transform -------- ---------------- detector linear_transform icrs None To convert a pixel (x, y) = (1, 2) to sky coordinates, call the WCS object as a function: >>> sky = wcsobj(1*u.pix, 2*u.pix, with_units=True) >>> print(sky) The :meth:`~gwcs.wcs.WCS.invert` method evaluates the :meth:`~gwcs.wcs.WCS.backward_transform` if available, otherwise applies an iterative method to calculate the reverse coordinates. >>> wcsobj.invert(sky) (, ) .. _save_as_asdf: Save a WCS object as a pure ASDF file +++++++++++++++++++++++++++++++++++++ .. doctest-skip:: >>> from asdf import AsdfFile >>> tree = {"wcs": wcsobj} >>> wcs_file = AsdfFile(tree) >>> wcs_file.write_to("imaging_wcs.asdf") :ref:`pure_asdf` Save a WCS object as an ASDF extension in a FITS file +++++++++++++++++++++++++++++++++++++++++++++++++++++ .. doctest-skip:: >>> from astropy.io import fits >>> from asdf import fits_embed >>> hdul = fits.open("example_imaging.fits") >>> hdul.info() Filename: example_imaging.fits No. Name Ver Type Cards Dimensions Format 0 PRIMARY 1 PrimaryHDU 775 () 1 SCI 1 ImageHDU 71 (600, 550) float32 >>> tree = {"sci", hdul.data, ... "wcs": wcsobj} >>> fa = fits.embed.AsdfInFits(hdul, tree) >>> fa.write_to("imaging_with_wcs_in_asdf.fits") >>> fits.info("imaging_with_wcs_in_asdf.fits") Filename: example_with_wcs.asdf No. Name Ver Type Cards Dimensions Format 0 PRIMARY 1 PrimaryHDU 775 () 1 SCI 1 ImageHDU 71 (600, 550) float32 2 ASDF 1 BinTableHDU 11 1R x 1C [5086B] Reading a WCS object from a file ++++++++++++++++++++++++++++++++ `ASDF `__ is used to read a WCS object from a pure ASDF file or from an ASDF extension in a FITS file. .. doctest-skip:: >>> import asdf >>> asdf_file = asdf.open("imaging_wcs.asdf") >>> wcsobj = asdf_file.tree['wcs'] .. doctest-skip:: >>> import asdf >>> fa = asdf.open("imaging_with_wcs_in_asdf.fits") >>> wcsobj = fa.tree["wcs"] Other Examples -------------- .. toctree:: :maxdepth: 2 gwcs/imaging_with_distortion.rst gwcs/ifu.rst Using `gwcs` ------------ .. toctree:: :maxdepth: 2 gwcs/wcs_ape.rst gwcs/using_wcs.rst gwcs/wcstools.rst gwcs/pure_asdf.rst gwcs/wcs_validation.rst gwcs/schemas/index.rst gwcs/points_to_wcs.rst See also -------- - `The modeling package in astropy `__ - `The coordinates package in astropy `__ - `The Advanced Scientific Data Format (ASDF) standard `__ and its `Python implementation `__ Reference/API ------------- .. automodapi:: gwcs.wcs .. automodapi:: gwcs.coordinate_frames .. automodapi:: gwcs.wcstools .. automodapi:: gwcs.selector .. automodapi:: gwcs.spectroscopy gwcs-0.12.0/docs/make.bat0000644000732200020070000001064113600426757017255 0ustar denchevaSTSCI\science00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. changes to make an overview over all changed/added/deprecated items echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Astropy.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Astropy.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) :end gwcs-0.12.0/gwcs/0000755000732200020070000000000013600427270015650 5ustar denchevaSTSCI\science00000000000000gwcs-0.12.0/gwcs/__init__.py0000644000732200020070000000453613600426757020002 0ustar denchevaSTSCI\science00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ GWCS - Generalized World Coordinate System ========================================== Generalized World Coordinate System (GWCS) is an Astropy affiliated package providing tools for managing the World Coordinate System of astronomical data. GWCS takes a general approach to the problem of expressing transformations between pixel and world coordinates. It supports a data model which includes the entire transformation pipeline from input coordinates (detector by default) to world coordinates. It is tightly integrated with Astropy. - Transforms are instances of ``astropy.Model``. They can be chained, joined or combined with arithmetic operators using the flexible framework of compound models in ``astropy.modeling``. - Celestial coordinates are instances of ``astropy.SkyCoord`` and are transformed to other standard celestial frames using ``astropy.coordinates``. - Time coordinates are represented by ``astropy.Time`` and can be further manipulated using the tools in ``astropy.time`` - Spectral coordinates are ``astropy.Quantity`` objects and can be converted to other units using the tools in ``astropy.units``. For complete features and usage examples see the documentation site: http://gwcs.readthedocs.org Note ---- GWCS supports only Python 3. Installation ------------ To install:: pip install gwcs To clone from github and install the master branch:: git clone https://github.com/spacetelescope/gwcs.git cd gwcs python setup.py install Contributing Code, Documentation, or Feedback --------------------------------------------- GWCS is developed on github. We welcome feedback and contributions to the project. Contributions of code, documentation, or general feedback are all appreciated. More information about contributing is in the github repository. """ import sys if sys.version_info < (3, 6): raise ImportError("GWCS supports Python versions 3.6 and above.") # pragma: no cover from pkg_resources import get_distribution, DistributionNotFound try: __version__ = get_distribution(__name__).version except DistributionNotFound: # pragma: no cover # package is not installed pass # pragma: no cover from .wcs import * # noqa from .wcstools import * # noqa from .coordinate_frames import * # noqa from .selector import * # noqa gwcs-0.12.0/gwcs/api.py0000644000732200020070000003246113600426757017012 0ustar denchevaSTSCI\science00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This module contains a mixin class which exposes the WCS API defined in astropy APE 14 (https://doi.org/10.5281/zenodo.1188875). """ from astropy.wcs.wcsapi import BaseHighLevelWCS, BaseLowLevelWCS from astropy.modeling import separable import astropy.units as u from . import utils from . import coordinate_frames as cf __all__ = ["GWCSAPIMixin"] class GWCSAPIMixin(BaseHighLevelWCS, BaseLowLevelWCS): """ A mix-in class that is intended to be inherited by the :class:`~gwcs.wcs.WCS` class and provides the low- and high-level WCS API described in the astropy APE 14 (https://doi.org/10.5281/zenodo.1188875). """ # Low Level APE 14 API @property def pixel_n_dim(self): """ The number of axes in the pixel coordinate system. """ if self.input_frame is None: return self.forward_transform.n_inputs return self.input_frame.naxes @property def world_n_dim(self): """ The number of axes in the world coordinate system. """ if self.output_frame is None: return self.forward_transform.n_outputs return self.output_frame.naxes @property def world_axis_physical_types(self): """ An iterable of strings describing the physical type for each world axis. These should be names from the VO UCD1+ controlled Vocabulary (http://www.ivoa.net/documents/latest/UCDlist.html). If no matching UCD type exists, this can instead be ``"custom:xxx"``, where ``xxx`` is an arbitrary string. Alternatively, if the physical type is unknown/undefined, an element can be `None`. """ # A CompositeFrame orders the output correctly based on axes_order. if isinstance(self.output_frame, cf.CompositeFrame): return self.output_frame.axis_physical_types # If we don't have a CompositeFrame, where this is taken care of for us, # we need to make sure we re-order the output to match the transform. # The underlying frames don't reorder themselves because axes_order is global. return tuple(self.output_frame.axis_physical_types[i] for i in self.output_frame.axes_order) @property def world_axis_units(self): """ An iterable of strings given the units of the world coordinates for each axis. The strings should follow the `IVOA VOUnit standard `_ (though as noted in the VOUnit specification document, units that do not follow this standard are still allowed, but just not recommended). """ return tuple(unit.to_string(format='vounit') for unit in self.output_frame.unit) def _remove_quantity_output(self, result, frame): if self.forward_transform.uses_quantity: if self.output_frame.naxes == 1: result = [result] result = tuple(r.to_value(unit) for r, unit in zip(result, frame.unit)) # If we only have one output axes, we shouldn't return a tuple. if self.output_frame.naxes == 1 and isinstance(result, tuple): return result[0] return result def _add_units_input(self, arrays, transform, frame): if transform.uses_quantity: return tuple(u.Quantity(array, unit) for array, unit in zip(arrays, frame.unit)) return arrays def pixel_to_world_values(self, *pixel_arrays): """ Convert pixel coordinates to world coordinates. This method takes ``pixel_n_dim`` scalars or arrays as input, and pixel coordinates should be zero-based. Returns ``world_n_dim`` scalars or arrays in units given by ``world_axis_units``. Note that pixel coordinates are assumed to be 0 at the center of the first pixel in each dimension. If a pixel is in a region where the WCS is not defined, NaN can be returned. The coordinates should be specified in the ``(x, y)`` order, where for an image, ``x`` is the horizontal coordinate and ``y`` is the vertical coordinate. """ pixel_arrays = self._add_units_input(pixel_arrays, self.forward_transform, self.input_frame) result = self(*pixel_arrays, with_units=False) return self._remove_quantity_output(result, self.output_frame) def array_index_to_world_values(self, *index_arrays): """ Convert array indices to world coordinates. This is the same as `~BaseLowLevelWCS.pixel_to_world_values` except that the indices should be given in ``(i, j)`` order, where for an image ``i`` is the row and ``j`` is the column (i.e. the opposite order to `~BaseLowLevelWCS.pixel_to_world_values`). """ index_arrays = self._add_units_input(index_arrays[::-1], self.forward_transform, self.input_frame) result = self(*index_arrays, with_units=False) return self._remove_quantity_output(result, self.output_frame) def world_to_pixel_values(self, *world_arrays): """ Convert world coordinates to pixel coordinates. This method takes ``world_n_dim`` scalars or arrays as input in units given by ``world_axis_units``. Returns ``pixel_n_dim`` scalars or arrays. Note that pixel coordinates are assumed to be 0 at the center of the first pixel in each dimension. If a world coordinate does not have a matching pixel coordinate, NaN can be returned. The coordinates should be returned in the ``(x, y)`` order, where for an image, ``x`` is the horizontal coordinate and ``y`` is the vertical coordinate. """ world_arrays = self._add_units_input(world_arrays, self.backward_transform, self.output_frame) result = self.invert(*world_arrays, with_units=False) return self._remove_quantity_output(result, self.input_frame) def world_to_array_index_values(self, *world_arrays): """ Convert world coordinates to array indices. This is the same as `~BaseLowLevelWCS.world_to_pixel_values` except that the indices should be returned in ``(i, j)`` order, where for an image ``i`` is the row and ``j`` is the column (i.e. the opposite order to `~BaseLowLevelWCS.pixel_to_world_values`). The indices should be returned as rounded integers. """ world_arrays = self._add_units_input(world_arrays, self.backward_transform, self.output_frame) result = self.invert(*world_arrays, with_units=False) if self.pixel_n_dim != 1: result = result[::-1] return self._remove_quantity_output(result, self.input_frame) @property def array_shape(self): """ The shape of the data that the WCS applies to as a tuple of length `~BaseLowLevelWCS.pixel_n_dim`. If the WCS is valid in the context of a dataset with a particular shape, then this property can be used to store the shape of the data. This can be used for example if implementing slicing of WCS objects. This is an optional property, and it should return `None` if a shape is not known or relevant. The shape should be given in ``(row, column)`` order (the convention for arrays in Python). """ return self._array_shape @array_shape.setter def array_shape(self, value): self._array_shape = value @property def pixel_bounds(self): """ The bounds (in pixel coordinates) inside which the WCS is defined, as a list with `~BaseLowLevelWCS.pixel_n_dim` ``(min, max)`` tuples. The bounds should be given in ``[(xmin, xmax), (ymin, ymax)]`` order. WCS solutions are sometimes only guaranteed to be accurate within a certain range of pixel values, for example when defining a WCS that includes fitted distortions. This is an optional property, and it should return `None` if a shape is not known or relevant. """ bounding_box = self.bounding_box if bounding_box is None: return bounding_box if self.pixel_n_dim == 1 and len(bounding_box) == 2: bounding_box = (bounding_box,) # Iterate over the bounding box and convert from quantity if required. bounding_box = list(bounding_box) for i, bb_axes in enumerate(bounding_box): bb = [] for lim in bb_axes: if isinstance(lim, u.Quantity): lim = lim.value bb.append(lim) bounding_box[i] = tuple(bb) return tuple(bounding_box) @property def pixel_shape(self): """ The shape of the data that the WCS applies to as a tuple of length ``pixel_n_dim`` in ``(x, y)`` order (where for an image, ``x`` is the horizontal coordinate and ``y`` is the vertical coordinate) (optional). If the WCS is valid in the context of a dataset with a particular shape, then this property can be used to store the shape of the data. This can be used for example if implementing slicing of WCS objects. This is an optional property, and it should return `None` if a shape is neither known nor relevant. """ return self._pixel_shape @pixel_shape.setter def pixel_shape(self, value): if value is None: self._pixel_shape = None return wcs_naxes = self.input_frame.naxes if len(value) != wcs_naxes: raise ValueError("The number of data axes, " "{}, does not equal the " "shape {}.".format(wcs_naxes, len(value))) self._pixel_shape = tuple(value) @property def axis_correlation_matrix(self): """ Returns an (`~BaseLowLevelWCS.world_n_dim`, `~BaseLowLevelWCS.pixel_n_dim`) matrix that indicates using booleans whether a given world coordinate depends on a given pixel coordinate. This defaults to a matrix where all elements are `True` in the absence of any further information. For completely independent axes, the diagonal would be `True` and all other entries `False`. """ return separable.separability_matrix(self.forward_transform) @property def serialized_classes(self): """ Indicates whether Python objects are given in serialized form or as actual Python objects. """ return False @property def world_axis_object_classes(self): return self.output_frame._world_axis_object_classes @property def world_axis_object_components(self): return self.output_frame._world_axis_object_components # High level APE 14 API @property def low_level_wcs(self): """ Returns a reference to the underlying low-level WCS object. """ return self def _sanitize_pixel_inputs(self, *pixel_arrays): pixels = [] if self.forward_transform.uses_quantity: for i, pixel in enumerate(pixel_arrays): if not isinstance(pixel, u.Quantity): pixel = u.Quantity(value=pixel, unit=self.input_frame.unit[i]) pixels.append(pixel) else: for i, pixel in enumerate(pixel_arrays): if isinstance(pixel, u.Quantity): if pixel.unit != self.input_frame.unit[i]: raise ValueError('Quantity input does not match the ' 'input_frame unit.') pixel = pixel.value pixels.append(pixel) return pixels def pixel_to_world(self, *pixel_arrays): """ Convert pixel values to world coordinates. """ pixels = self._sanitize_pixel_inputs(*pixel_arrays) return self(*pixels, with_units=True) def array_index_to_world(self, *index_arrays): """ Convert array indices to world coordinates (represented by Astropy objects). """ pixel_arrays = index_arrays[::-1] pixels = self._sanitize_pixel_inputs(*pixel_arrays) return self(*pixels, with_units=True) def world_to_pixel(self, *world_objects): """ Convert world coordinates to pixel values. """ result = self.invert(*world_objects, with_units=True) if not utils.isnumerical(result[0]): result = [i.value for i in result] if self.input_frame.naxes == 1: return result[0] return result def world_to_array_index(self, *world_objects): """ Convert world coordinates (represented by Astropy objects) to array indices. """ result = self.invert(*world_objects, with_units=True)[::-1] return tuple([utils._toindex(r) for r in result]) @property def pixel_axis_names(self): """ An iterable of strings describing the name for each pixel axis. """ if self.input_frame is not None: return self.input_frame.axes_names return tuple([''] * self.pixel_n_dim) @property def world_axis_names(self): """ An iterable of strings describing the name for each world axis. """ if self.output_frame is not None: return self.output_frame.axes_names return tuple([''] * self.world_n_dim) gwcs-0.12.0/gwcs/coordinate_frames.py0000644000732200020070000006173513600426757021733 0ustar denchevaSTSCI\science00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Defines coordinate frames and ties them to data axes. """ import logging import numpy as np from astropy.utils.misc import isiterable from astropy import time from astropy import units as u from astropy import utils as astutil from astropy import coordinates as coord from astropy.wcs.wcsapi.low_level_api import (validate_physical_types, VALID_UCDS) __all__ = ['Frame2D', 'CelestialFrame', 'SpectralFrame', 'CompositeFrame', 'CoordinateFrame', 'TemporalFrame'] STANDARD_REFERENCE_FRAMES = [frame.upper() for frame in coord.builtin_frames.__all__] STANDARD_REFERENCE_POSITION = ["GEOCENTER", "BARYCENTER", "HELIOCENTER", "TOPOCENTER", "LSR", "LSRK", "LSRD", "GALACTIC_CENTER", "LOCAL_GROUP_CENTER"] class CoordinateFrame: """ Base class for Coordinate Frames. Parameters ---------- naxes : int Number of axes. axes_type : str One of ["SPATIAL", "SPECTRAL", "TIME"] axes_order : tuple of int A dimension in the input data that corresponds to this axis. reference_frame : astropy.coordinates.builtin_frames Reference frame (usually used with output_frame to convert to world coordinate objects). reference_position : str Reference position - one of `STANDARD_REFERENCE_POSITION` unit : list of astropy.units.Unit Unit for each axis. axes_names : list Names of the axes in this frame. name : str Name of this frame. """ def __init__(self, naxes, axes_type, axes_order, reference_frame=None, reference_position=None, unit=None, axes_names=None, name=None, axis_physical_types=None): self._naxes = naxes self._axes_order = tuple(axes_order) if isinstance(axes_type, str): self._axes_type = (axes_type,) else: self._axes_type = tuple(axes_type) self._reference_frame = reference_frame if unit is not None: if astutil.isiterable(unit): unit = tuple(unit) else: unit = (unit,) if len(unit) != naxes: raise ValueError("Number of units does not match number of axes.") else: self._unit = tuple([u.Unit(au) for au in unit]) else: self._unit = tuple(u.Unit("") for na in range(naxes)) if axes_names is not None: if isinstance(axes_names, str): axes_names = (axes_names,) else: axes_names = tuple(axes_names) if len(axes_names) != naxes: raise ValueError("Number of axes names does not match number of axes.") else: axes_names = tuple([""] * naxes) self._axes_names = axes_names if name is None: self._name = self.__class__.__name__ else: self._name = name self._reference_position = reference_position if len(self._axes_type) != naxes: raise ValueError("Length of axes_type does not match number of axes.") if len(self._axes_order) != naxes: raise ValueError("Length of axes_order does not match number of axes.") super(CoordinateFrame, self).__init__() self._axis_physical_types = self._set_axis_physical_types(axis_physical_types) def _set_axis_physical_types(self, pht=None): """ Set the physical type of the coordinate axes using VO UCD1+ v1.23 definitions. """ if pht is not None: if isinstance(pht, str): pht = (pht,) elif not isiterable(pht): raise TypeError("axis_physical_types must be of type string or iterable of strings") if len(pht) != self.naxes: raise ValueError('"axis_physical_types" must be of length {}'.format(self.naxes)) ph_type = [] for axt in pht: if axt not in VALID_UCDS and not axt.startswith("custom:"): ph_type.append("custom:{}".format(axt)) else: ph_type.append(axt) elif isinstance(self, CelestialFrame): if isinstance(self.reference_frame, coord.Galactic): ph_type = "pos.galactic.lon", "pos.galactic.lat" elif isinstance(self.reference_frame, (coord.GeocentricTrueEcliptic, coord.GCRS, coord.PrecessedGeocentric)): ph_type = "pos.bodyrc.lon", "pos.bodyrc.lat" elif isinstance(self.reference_frame, coord.builtin_frames.BaseRADecFrame): ph_type = "pos.eq.ra", "pos.eq.dec" elif isinstance(self.reference_frame, coord.builtin_frames.BaseEclipticFrame): ph_type = "pos.ecliptic.lon", "pos.ecliptic.lat" else: ph_type = tuple("custom:{}".format(t) for t in self.axes_names) elif isinstance(self, SpectralFrame): if self.unit[0].physical_type == "frequency": ph_type = ("em.freq",) elif self.unit[0].physical_type == "length": ph_type = ("em.wl",) elif self.unit[0].physical_type == "energy": ph_type = ("em.energy",) elif self.unit[0].physical_type == "speed": ph_type = ("spect.dopplerVeloc",) logging.warning("Physical type may be ambiguous. Consider " "setting the physical type explicitly as " "either 'spect.dopplerVeloc.optical' or " "'spect.dopplerVeloc.radio'.") else: ph_type = ("custom:{}".format(self.unit[0].physical_type),) elif isinstance(self, TemporalFrame): ph_type = ("time",) elif isinstance(self, Frame2D): if all(self.axes_names): ph_type = self.axes_names else: ph_type = self.axes_type ph_type = tuple("custom:{}".format(t) for t in ph_type) else: ph_type = tuple("custom:{}".format(t) for t in self.axes_type) validate_physical_types(ph_type) return tuple(ph_type) def __repr__(self): fmt = '<{0}(name="{1}", unit={2}, axes_names={3}, axes_order={4}'.format( self.__class__.__name__, self.name, self.unit, self.axes_names, self.axes_order) if self.reference_position is not None: fmt += ', reference_position="{0}"'.format(self.reference_position) if self.reference_frame is not None: fmt += ", reference_frame={0}".format(self.reference_frame) fmt += ")>" return fmt def __str__(self): if self._name is not None: return self._name return self.__class__.__name__ @property def name(self): """ A custom name of this frame.""" return self._name @name.setter def name(self, val): """ A custom name of this frame.""" self._name = val @property def naxes(self): """ The number of axes in this frame.""" return self._naxes @property def unit(self): """The unit of this frame.""" return self._unit @property def axes_names(self): """ Names of axes in the frame.""" return self._axes_names @property def axes_order(self): """ A tuple of indices which map inputs to axes.""" return self._axes_order @property def reference_frame(self): """ Reference frame, used to convert to world coordinate objects. """ return self._reference_frame @property def reference_position(self): """ Reference Position. """ return getattr(self, "_reference_position", None) @property def axes_type(self): """ Type of this frame : 'SPATIAL', 'SPECTRAL', 'TIME'. """ return self._axes_type def coordinates(self, *args): """ Create world coordinates object""" args = [args[i] for i in self.axes_order] coo = tuple([arg * un if not hasattr(arg, "to") else arg.to(un) for arg, un in zip(args, self.unit)]) return coo def coordinate_to_quantity(self, *coords): """ Given a rich coordinate object return an astropy quantity object. """ # NoOp leaves it to the model to handle return coords @property def axis_physical_types(self): return self._axis_physical_types @property def _world_axis_object_components(self): raise NotImplementedError(f"This method is not implemented for {type(self)}") @property def _world_axis_object_classes(self): raise NotImplementedError(f"This method is not implemented for {type(self)}") class CelestialFrame(CoordinateFrame): """ Celestial Frame Representation Parameters ---------- axes_order : tuple of int A dimension in the input data that corresponds to this axis. reference_frame : astropy.coordinates.builtin_frames A reference frame. unit : str or units.Unit instance or iterable of those Units on axes. axes_names : list Names of the axes in this frame. name : str Name of this frame. """ def __init__(self, axes_order=None, reference_frame=None, unit=None, axes_names=None, name=None, axis_physical_types=None): naxes = 2 if reference_frame is not None: if not isinstance(reference_frame, str): if reference_frame.name.upper() in STANDARD_REFERENCE_FRAMES: _axes_names = list(reference_frame.representation_component_names.values()) if 'distance' in _axes_names: _axes_names.remove('distance') if axes_names is None: axes_names = _axes_names naxes = len(_axes_names) _unit = list(reference_frame.representation_component_units.values()) if unit is None and _unit: unit = _unit if axes_order is None: axes_order = tuple(range(naxes)) if unit is None: unit = tuple([u.degree] * naxes) axes_type = ['SPATIAL'] * naxes super(CelestialFrame, self).__init__(naxes=naxes, axes_type=axes_type, axes_order=axes_order, reference_frame=reference_frame, unit=unit, axes_names=axes_names, name=name, axis_physical_types=axis_physical_types) @property def _world_axis_object_classes(self): return {'celestial': ( coord.SkyCoord, (), {'frame': self.reference_frame, 'unit': self.unit})} @property def _world_axis_object_components(self): return [('celestial', 0, 'spherical.lon'), ('celestial', 1, 'spherical.lat')] def coordinates(self, *args): """ Create a SkyCoord object. Parameters ---------- args : float inputs to wcs.input_frame """ if isinstance(args[0], coord.SkyCoord): return args[0].transform_to(self.reference_frame) return coord.SkyCoord(*args, unit=self.unit, frame=self.reference_frame) def coordinate_to_quantity(self, *coords): """ Convert a ``SkyCoord`` object to quantities.""" if len(coords) == 2: arg = coords elif len(coords) == 1: arg = coords[0] else: raise ValueError("Unexpected number of coordinates in " "input to frame {} : " "expected 2, got {}".format(self.name, len(coords))) if isinstance(arg, coord.SkyCoord): arg = arg.transform_to(self._reference_frame) try: lon = arg.data.lon lat = arg.data.lat except AttributeError: lon = arg.spherical.lon lat = arg.spherical.lat return lon, lat elif all(isinstance(a, u.Quantity) for a in arg): return tuple(arg) else: raise ValueError("Could not convert input {} to lon and lat quantities.".format(arg)) class SpectralFrame(CoordinateFrame): """ Represents Spectral Frame Parameters ---------- axes_order : tuple or int A dimension in the input data that corresponds to this axis. reference_frame : astropy.coordinates.builtin_frames Reference frame (usually used with output_frame to convert to world coordinate objects). unit : str or units.Unit instance Spectral unit. axes_names : str Spectral axis name. name : str Name for this frame. reference_position : str Reference position - one of `STANDARD_REFERENCE_POSITION` """ def __init__(self, axes_order=(0,), reference_frame=None, unit=None, axes_names=None, name=None, axis_physical_types=None, reference_position=None): super(SpectralFrame, self).__init__(naxes=1, axes_type="SPECTRAL", axes_order=axes_order, axes_names=axes_names, reference_frame=reference_frame, unit=unit, name=name, reference_position=reference_position, axis_physical_types=axis_physical_types) @property def _world_axis_object_classes(self): return {'spectral': ( u.Quantity, (), {'unit': self.unit[0]})} @property def _world_axis_object_components(self): return [('spectral', 0, 'value')] def coordinates(self, *args, equivalencies=[]): if hasattr(args[0], 'unit'): return args[0].to(self.unit[0], equivalencies=equivalencies) if np.isscalar(args): return args * self.unit[0] else: return args[0] * self.unit[0] def coordinate_to_quantity(self, *coords): if hasattr(coords[0], 'unit'): return coords[0] return coords[0] * self.unit[0] class TemporalFrame(CoordinateFrame): """ A coordinate frame for time axes. Parameters ---------- reference_frame : `~astropy.time.Time` A Time object which holds the time scale and format. If data is provided, it is the time zero point. To not set a zero point for the frame initialize `reference_frame` with an empty list. unit : str or `~astropy.units.Unit` Time unit. axes_names : str Time axis name. axes_order : tuple or int A dimension in the data that corresponds to this axis. name : str Name for this frame. """ def __init__(self, reference_frame, unit=None, axes_order=(0,), axes_names=None, name=None, axis_physical_types=None): axes_names = axes_names or "{}({}; {}".format(reference_frame.format, reference_frame.scale, reference_frame.location) super().__init__(naxes=1, axes_type="TIME", axes_order=axes_order, axes_names=axes_names, reference_frame=reference_frame, unit=unit, name=name, axis_physical_types=axis_physical_types) self._attrs = {} for a in self.reference_frame.info._represent_as_dict_extra_attrs: try: self._attrs[a] = getattr(self.reference_frame, a) except AttributeError: pass @property def _world_axis_object_classes(self): comp = ( time.Time, (), {'unit': self.unit[0], **self._attrs}, self._convert_to_time) return {'temporal': comp} @property def _world_axis_object_components(self): return [('temporal', 0, 'value')] def coordinates(self, *args): if np.isscalar(args): dt = args else: dt = args[0] return self._convert_to_time(dt, unit=self.unit[0], **self._attrs) def _convert_to_time(self, dt, *, unit, **kwargs): if not isinstance(self.reference_frame.value, np.ndarray): if not hasattr(dt, 'unit'): dt = dt * unit return self.reference_frame + dt else: return time.Time(dt, **kwargs) def coordinate_to_quantity(self, *coords): if isinstance(coords[0], time.Time): ref_value = self.reference_frame.value if not isinstance(ref_value, np.ndarray): return (coords[0] - self.reference_frame).to(self.unit[0]) else: # If we can't convert to a quantity just drop the object out # and hope the transform can cope. return coords[0] # Is already a quantity elif hasattr(coords[0], 'unit'): return coords[0] else: raise ValueError("Can not convert {} to Quantity".format(coords[0])) class CompositeFrame(CoordinateFrame): """ Represents one or more frames. Parameters ---------- frames : list List of frames (TemporalFrame, CelestialFrame, SpectralFrame, CoordinateFrame). name : str Name for this frame. """ def __init__(self, frames, name=None): self._frames = frames[:] naxes = sum([frame._naxes for frame in self._frames]) axes_type = list(range(naxes)) unit = list(range(naxes)) axes_names = list(range(naxes)) axes_order = [] ph_type = list(range(naxes)) for frame in frames: axes_order.extend(frame.axes_order) for frame in frames: for ind, axtype, un, n, pht in zip(frame.axes_order, frame.axes_type, frame.unit, frame.axes_names, frame.axis_physical_types): axes_type[ind] = axtype axes_names[ind] = n unit[ind] = un ph_type[ind] = pht if len(np.unique(axes_order)) != len(axes_order): raise ValueError("Incorrect numbering of axes, " "axes_order should contain unique numbers, " "got {}.".format(axes_order)) super(CompositeFrame, self).__init__(naxes, axes_type=axes_type, axes_order=axes_order, unit=unit, axes_names=axes_names, name=name) self._axis_physical_types = tuple(ph_type) @property def frames(self): return self._frames def __repr__(self): return repr(self.frames) def coordinates(self, *args): coo = [] if len(args) == len(self.frames): for frame, arg in zip(self.frames, args): coo.append(frame.coordinates(arg)) else: for frame in self.frames: fargs = [args[i] for i in frame.axes_order] coo.append(frame.coordinates(*fargs)) return coo def coordinate_to_quantity(self, *coords): if len(coords) == len(self.frames): args = coords elif len(coords) == self.naxes: args = [] for _frame in self.frames: if _frame.naxes > 1: # Collect the arguments for this frame based on axes_order args.append([coords[i] for i in _frame.axes_order]) else: args.append(coords[_frame.axes_order[0]]) else: raise ValueError("Incorrect number of arguments") qs = [] for _frame, arg in zip(self.frames, args): ret = _frame.coordinate_to_quantity(arg) if isinstance(ret, tuple): qs += list(ret) else: qs.append(ret) return qs @property def _world_axis_object_components(self): """ We need to generate the components respecting the axes_order. """ out = [None] * self.naxes for frame in self.frames: for i, ao in enumerate(frame.axes_order): out[ao] = frame._world_axis_object_components[i] if any([o is None for o in out]): raise ValueError("axes_order leads to incomplete world_axis_object_components") return out @property def _world_axis_object_classes(self): out = {} for frame in self.frames: out.update(frame._world_axis_object_classes) return out class StokesProfile(str): # This list of profiles in Table 7 in Greisen & Calabretta (2002) # modified to be 0 indexed profiles = { 'I': 0, 'Q': 1, 'U': 2, 'V': 3, 'RR': -1, 'LL': -2, 'RL': -3, 'LR': -4, 'XX': -5, 'YY': -6, 'XY': -7, 'YX': -8, } @classmethod def from_index(cls, indexes): """ Construct a StokesProfile object from a numerical index. Parameters ---------- indexes : `int`, `numpy.ndarray` An index or array of indices to construct StokesProfile objects from. """ nans = np.isnan(indexes) indexes = np.asanyarray(indexes, dtype=int) out = np.empty_like(indexes, dtype=object) out[nans] = np.nan for profile, index in cls.profiles.items(): out[indexes == index] = profile if out.size == 1 and not nans: return StokesProfile(out.item()) elif nans.all(): return np.array(out, dtype=float) return out def __new__(cls, content): content = str(content) if content not in cls.profiles.keys(): raise ValueError(f"The profile name must be one of {cls.profiles.keys()} not {content}") return str.__new__(cls, content) def value(self): return self.profiles[self] class StokesFrame(CoordinateFrame): """ A coordinate frame for representing stokes polarisation states Parameters ---------- name : str Name of this frame. """ def __init__(self, axes_order=(0,), name=None): super(StokesFrame, self).__init__(1, ["STOKES"], axes_order, name=name, axes_names=("stokes",), unit=u.one, axis_physical_types="phys.polarization.stokes") @property def _world_axis_object_classes(self): return {'stokes': ( StokesProfile, (), {}, StokesProfile.from_index)} @property def _world_axis_object_components(self): return [('stokes', 0, 'value')] def coordinates(self, *args): if isinstance(args[0], u.Quantity): arg = args[0].value else: arg = args[0] return StokesProfile.from_index(arg) def coordinate_to_quantity(self, *coords): if isinstance(coords[0], str): if coords[0] in StokesProfile.profiles.keys(): return StokesProfile.profiles[coords[0]] * u.one else: return coords[0] class Frame2D(CoordinateFrame): """ A 2D coordinate frame. Parameters ---------- axes_order : tuple of int A dimension in the input data that corresponds to this axis. unit : list of astropy.units.Unit Unit for each axis. axes_names : list Names of the axes in this frame. name : str Name of this frame. """ def __init__(self, axes_order=(0, 1), unit=(u.pix, u.pix), axes_names=('x', 'y'), name=None, axis_physical_types=None): super(Frame2D, self).__init__(naxes=2, axes_type=["SPATIAL", "SPATIAL"], axes_order=axes_order, name=name, axes_names=axes_names, unit=unit, axis_physical_types=axis_physical_types) def coordinates(self, *args): args = [args[i] for i in self.axes_order] coo = tuple([arg * un for arg, un in zip(args, self.unit)]) return coo def coordinate_to_quantity(self, *coords): # list or tuple if len(coords) == 1 and astutil.isiterable(coords[0]): coords = list(coords[0]) elif len(coords) == 2: coords = list(coords) else: raise ValueError("Unexpected number of coordinates in " "input to frame {} : " "expected 2, got {}".format(self.name, len(coords))) for i in range(2): if not hasattr(coords[i], 'unit'): coords[i] = coords[i] * self.unit[i] return tuple(coords) gwcs-0.12.0/gwcs/extension.py0000644000732200020070000000204613600426757020251 0ustar denchevaSTSCI\science00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst import os from asdf import util from asdf.extension import BuiltinExtension from .tags.wcs import * # noqa from .tags.selectortags import * # noqa from .tags.spectroscopy_models import * # noqa from .tags.geometry_models import * # noqa # Make sure that all tag implementations are imported by the time we create # the extension class so that _gwcs_types is populated correctly. from .tags import * # noqa from .gwcs_types import _gwcs_types SCHEMA_PATH = os.path.abspath( os.path.join(os.path.dirname(__file__), 'schemas')) class GWCSExtension(BuiltinExtension): @property def types(self): return _gwcs_types @property def tag_mapping(self): return [('tag:stsci.edu:gwcs', 'http://stsci.edu/schemas/gwcs{tag_suffix}')] @property def url_mapping(self): return [('http://stsci.edu/schemas/gwcs', util.filepath_to_url(os.path.join(SCHEMA_PATH, "stsci.edu")) + '/gwcs{url_suffix}.yaml')] gwcs-0.12.0/gwcs/geometry.py0000644000732200020070000000240313600426757020065 0ustar denchevaSTSCI\science00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Spectroscopy related models. """ import numpy as np from astropy.modeling.core import Model __all__ = ['ToDirectionCosines', 'FromDirectionCosines'] class ToDirectionCosines(Model): """ Transform a vector to direction cosines. """ _separable = False n_inputs = 3 n_outputs = 4 def __init__(self, **kwargs): super().__init__(**kwargs) self.inputs = ('x', 'y', 'z') self.outputs = ('cosa', 'cosb', 'cosc', 'length') def evaluate(self, x, y, z): vabs = np.sqrt(1. + x**2 + y**2) cosa = x / vabs cosb = y / vabs cosc = 1. / vabs return cosa, cosb, cosc, vabs def inverse(self): return FromDirectionCosines() class FromDirectionCosines(Model): """ Transform directional cosines to vector. """ _separable = False n_inputs = 4 n_outputs = 3 def __init__(self, **kwargs): super().__init__(**kwargs) self.inputs = ('cosa', 'cosb', 'cosc', 'length') self.outputs = ('x', 'y', 'z') def evaluate(self, cosa, cosb, cosc, length): return cosa * length, cosb * length, cosc * length def inverse(self): return ToDirectionCosines() gwcs-0.12.0/gwcs/gwcs_types.py0000644000732200020070000000373113600426757020426 0ustar denchevaSTSCI\science00000000000000# -*- coding: utf-8 -*- """ Defines a ``GWCSType`` used by GWCS. All types are added automatically to ``_gwcs_types`` and the GWCSExtension. """ import six from astropy.io.misc.asdf.tags.transform.basic import TransformType from asdf.types import ExtensionTypeMeta, CustomType from astropy.io.misc.asdf.types import AstropyTypeMeta __all__ = ['GWCSType', 'GWCSTransformType'] _gwcs_types = set() class GWCSTransformTypeMeta(AstropyTypeMeta): """ Keeps track of `GWCSType` subclasses that are created so that they can be stored automatically by astropy extensions for ASDF. """ def __new__(mcls, name, bases, attrs): cls = super(GWCSTransformTypeMeta, mcls).__new__(mcls, name, bases, attrs) # Classes using this metaclass are automatically added to the list of # jwst types and JWSTExtensions.types. if cls.organization == 'stsci.edu' and cls.standard == 'gwcs': _gwcs_types.add(cls) return cls class GWCSTypeMeta(ExtensionTypeMeta): """ Keeps track of `GWCSType` subclasses that are created so that they can be stored automatically by astropy extensions for ASDF. """ def __new__(mcls, name, bases, attrs): cls = super(GWCSTypeMeta, mcls).__new__(mcls, name, bases, attrs) # Classes using this metaclass are automatically added to the list of # jwst types and JWSTExtensions.types. if cls.organization == 'stsci.edu' and cls.standard == 'gwcs': _gwcs_types.add(cls) return cls @six.add_metaclass(GWCSTypeMeta) class GWCSType(CustomType): """ This class represents types that have schemas and tags implemented within GWCS. """ organization = 'stsci.edu' standard = 'gwcs' @six.add_metaclass(GWCSTransformTypeMeta) class GWCSTransformType(TransformType): """ This class represents transform types that have schemas and tags implemented within GWCS. """ organization = 'stsci.edu' standard = 'gwcs' gwcs-0.12.0/gwcs/region.py0000644000732200020070000002366513600426757017532 0ustar denchevaSTSCI\science00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst import abc from collections import OrderedDict import numpy as np class Region(metaclass=abc.ABCMeta): """ Base class for regions. Parameters ---------- rid : int or str region ID coordinate_frame : `~gwcs.coordinate_frames.CoordinateFrame` Coordinate frame in which the region is defined. """ def __init__(self, rid, coordinate_frame): self._coordinate_system = coordinate_frame self._rid = rid @abc.abstractmethod def __contains__(self, x, y): """ Determines if a pixel is within a region. Parameters ---------- x, y : float x , y values of a pixel Returns ------- True or False Subclasses must define this method. """ def scan(self, mask): """ Sets mask values to region id for all pixels within the region. Subclasses must define this method. Parameters ---------- mask : ndarray An array with the shape of the mask to be uised in `~gwcs.selector.RegionsSelector`. Returns ------- mask : ndarray An array where the value of the elements is the region ID. Pixels which are not included in any region are marked with 0 or "". """ class Polygon(Region): """ Represents a 2D polygon region with multiple vertices. Parameters ---------- rid : str polygon id vertices : list of (x,y) tuples or lists The list is ordered in such a way that when traversed in a counterclockwise direction, the enclosed area is the polygon. The last vertex must coincide with the first vertex, minimum 4 vertices are needed to define a triangle coord_frame : str or `~gwcs.coordinate_frames.CoordinateFrame` Coordinate frame in which the polygon is defined. """ def __init__(self, rid, vertices, coord_frame="detector"): if len(vertices) < 4: raise ValueError("Expected vertices to be " "a list of minimum 4 tuples (x,y)") super(Polygon, self).__init__(rid, coord_frame) self._vertices = np.asarray(vertices) self._bbox = self._get_bounding_box() self._scan_line_range = list(range(self._bbox[1], self._bbox[3] + self._bbox[1] + 1)) # constructs a Global Edge Table (GET) in bbox coordinates self._GET = self._construct_ordered_GET() def _get_bounding_box(self): x = self._vertices[:, 0].min() y = self._vertices[:, 1].min() w = self._vertices[:, 0].max() - x h = self._vertices[:, 1].max() - y return (x, y, w, h) def _construct_ordered_GET(self): """ Construct a Global Edge Table (GET) The GET is an OrderedDict. Keys are scan line numbers, ordered from bbox.ymin to bbox.ymax, where bbox is the bounding box of the polygon. Values are lists of edges for which edge.ymin==scan_line_number. Returns ------- GET: OrderedDict {scan_line: [edge1, edge2]} """ # edges is a list of Edge objects which define a polygon # with these vertices edges = self.get_edges() GET = OrderedDict.fromkeys(self._scan_line_range) ymin = np.asarray([e._ymin for e in edges]) for i in self._scan_line_range: ymin_ind = (ymin == i).nonzero()[0] if ymin_ind.any(): GET[i] = [edges[ymin_ind[0]]] for j in ymin_ind[1:]: GET[i].append(edges[j]) return GET def get_edges(self): """ Create a list of Edge objects from vertices """ return [Edge(name='E{}'.format(i - 1), start=self._vertices[i - 1], stop=self._vertices[i]) for i in range(1, len(self._vertices)) ] def scan(self, data): """ This is the main function which scans the polygon and creates the mask Parameters ---------- data : array the mask array it has all zeros initially, elements within a region are set to the region's ID Algorithm: - Set the Global Edge Table (GET) - Set y to be the smallest y coordinate that has an entry in GET - Initialize the Active Edge Table (AET) to be empty - For each scan line: 1. Add edges from GET to AET for which ymin==y 2. Remove edges from AET fro which ymax==y 3. Compute the intersection of the current scan line with all edges in the AET 4. Sort on X of intersection point 5. Set elements between pairs of X in the AET to the Edge's ID """ # TODO: 1.This algorithm does not mark pixels in the top row and left most column. # Pad the initial pixel description on top and left with 1 px to prevent this. # 2. Currently it uses intersection of the scan line with edges. If this is # too slow it should use the 1/m increment (replace 3 above) (or the increment # should be removed from the GET entry). y = np.min(list(self._GET.keys())) AET = [] scline = self._scan_line_range[-1] while y <= scline: AET = self.update_AET(y, AET) scan_line = Edge('scan_line', start=[self._bbox[0], y], stop=[self._bbox[0] + self._bbox[2], y]) x = [np.ceil(e.compute_AET_entry(scan_line)[1]) for e in AET if e is not None] xnew = np.asarray(np.sort(x), dtype=np.int) for i, j in zip(xnew[::2], xnew[1::2]): data[y][i:j + 1] = self._rid y = y + 1 return data def update_AET(self, y, AET): """ Update the Active Edge Table (AET) Add edges from GET to AET for which ymin of the edge is equal to the y of the scan line. Remove edges from AET for which ymax of the edge is equal to y of the scan line. """ edge_cont = self._GET[y] if edge_cont is not None: for edge in edge_cont: if edge._start[1] != edge._stop[1] and edge._ymin == y: AET.append(edge) for edge in AET[::-1]: if edge is not None: if edge._ymax == y: AET.remove(edge) return AET def __contains__(self, px): """even-odd algorithm or smth else better sould be used""" return px[0] >= self._bbox[0] and px[0] <= self._bbox[0] + self._bbox[2] and \ px[1] >= self._bbox[1] and px[1] <= self._bbox[1] + self._bbox[3] class Edge: """ Edge representation. An edge has a "start" and "stop" (x,y) vertices and an entry in the GET table of a polygon. The GET entry is a list of these values: [ymax, x_at_ymin, delta_x/delta_y] """ def __init__(self, name=None, start=None, stop=None, next=None): self._start = None if start is not None: self._start = np.asarray(start) self._name = name self._stop = stop if stop is not None: self._stop = np.asarray(stop) self._next = next if self._stop is not None and self._start is not None: if self._start[1] < self._stop[1]: self._ymin = self._start[1] self._yminx = self._start[0] else: self._ymin = self._stop[1] self._yminx = self._stop[0] self._ymax = max(self._start[1], self._stop[1]) self._xmin = min(self._start[0], self._stop[0]) self._xmax = max(self._start[0], self._stop[1]) else: self._ymin = None self._yminx = None self._ymax = None self._xmin = None self._xmax = None self.GET_entry = self.compute_GET_entry() @property def ymin(self): return self._ymin @property def start(self): return self._start @property def stop(self): return self._stop @property def ymax(self): return self._ymax def compute_GET_entry(self): """ Compute the entry in the Global Edge Table [ymax, x@ymin, 1/m] """ if self._start is None: entry = None else: earr = np.asarray([self._start, self._stop]) if np.diff(earr[:, 1]).item() == 0: return None else: entry = [self._ymax, self._yminx, (np.diff(earr[:, 0]) / np.diff(earr[:, 1])).item(), None] return entry def compute_AET_entry(self, edge): """ Compute the entry for an edge in the current Active Edge Table [ymax, x_intersect, 1/m] note: currently 1/m is not used """ x = self.intersection(edge)[0] return [self._ymax, x, self.GET_entry[2]] def __repr__(self): fmt = "" if self._name is not None: fmt += self._name next = self.next while next is not None: fmt += "-->" fmt += next._name next = next.next return fmt @property def next(self): return self._next @next.setter def next(self, edge): if self._name is None: self._name = edge._name self._stop = edge._stop self._start = edge._start self._next = edge.next else: self._next = edge def intersection(self, edge): u = self._stop - self._start v = edge._stop - edge._start w = self._start - edge._start D = np.cross(u, v) return np.cross(v, w) / D * u + self._start def is_parallel(self, edge): u = self._stop - self._start v = edge._stop - edge._start if np.cross(u, v): return False else: return True gwcs-0.12.0/gwcs/schemas/0000755000732200020070000000000013600427270017273 5ustar denchevaSTSCI\science00000000000000gwcs-0.12.0/gwcs/schemas/__init__.py0000644000732200020070000000000013600426757021403 0ustar denchevaSTSCI\science00000000000000gwcs-0.12.0/gwcs/schemas/stsci.edu/0000755000732200020070000000000013600427270021174 5ustar denchevaSTSCI\science00000000000000gwcs-0.12.0/gwcs/schemas/stsci.edu/gwcs/0000755000732200020070000000000013600427270022137 5ustar denchevaSTSCI\science00000000000000gwcs-0.12.0/gwcs/schemas/stsci.edu/gwcs/celestial_frame-1.0.0.yaml0000644000732200020070000000070513600426757026607 0ustar denchevaSTSCI\science00000000000000%YAML 1.1 --- $schema: "http://stsci.edu/schemas/yaml-schema/draft-01" id: "http://stsci.edu/schemas/gwcs/celestial_frame-1.0.0" tag: "tag:stsci.edu:gwcs/celestial_frame-1.0.0" title: > Represents a celestial frame. allOf: - type: object properties: axes_names: minItems: 2 maxItems: 3 axes_order: minItems: 2 maxItems: 3 unit: minItems: 2 maxItems: 3 - $ref: frame-1.0.0 gwcs-0.12.0/gwcs/schemas/stsci.edu/gwcs/composite_frame-1.0.0.yaml0000644000732200020070000000070313600426757026642 0ustar denchevaSTSCI\science00000000000000%YAML 1.1 --- $schema: "http://stsci.edu/schemas/yaml-schema/draft-01" id: "http://stsci.edu/schemas/gwcs/composite_frame-1.0.0" tag: "tag:stsci.edu:gwcs/composite_frame-1.0.0" title: > Represents a set of frames. allOf: - type: object properties: name: description: Name of composite frame. type: string frames: description: List of frames in the composite frame. type: array gwcs-0.12.0/gwcs/schemas/stsci.edu/gwcs/direction_cosines-1.0.0.yaml0000644000732200020070000000173213600426757027174 0ustar denchevaSTSCI\science00000000000000%YAML 1.1 --- $schema: "http://stsci.edu/schemas/yaml-schema/draft-01" id: "http://stsci.edu/schemas/gwcs/direction_cosines-1.0.0" tag: "tag:stsci.edu:gwcs/direction_cosines-1.0.0" title: > Convert coordinates between vector and direction cosine form. description: | This schema is for transforms which convert to and from direction cosines. examples: - - Convert direction cosines to vectors. - | ! transform_type: from_direction_cosines - - Convert vectors to directional cosines. - | ! transform_type: to_direction_cosines allOf: - $ref: "tag:stsci.edu:asdf/transform/transform-1.1.0" - object: properties: transform_type: description: | The type of transform/class to initialize. type: string enum: [to_direction_cosines, from_direction_cosines] required: [transform_type] gwcs-0.12.0/gwcs/schemas/stsci.edu/gwcs/frame-1.0.0.yaml0000644000732200020070000000405413600426757024563 0ustar denchevaSTSCI\science00000000000000%YAML 1.1 --- $schema: "http://stsci.edu/schemas/yaml-schema/draft-01" id: "http://stsci.edu/schemas/gwcs/frame-1.0.0" title: | The base class of all coordinate frames. description: | These objects are designed to be nested in arbitrary ways to build up transformation pipelines out of a number of low-level pieces. examples: - - | A celestial frame in the ICRS reference frame. - | ! axes_names: [lon, lat] name: CelestialFrame reference_frame: ! frame_attributes: {} unit: [!unit/unit-1.0.0 deg, !unit/unit-1.0.0 deg] - - | A pixel frame in three dimensions - | ! axes_names: [raster position, slit position, wavelength] axes_order: [0, 1, 2] axes_type: [SPATIAL, SPATIAL, SPECTRAL] name: pixel naxes: 3 unit: [!unit/unit-1.0.0 pixel, !unit/unit-1.0.0 pixel, !unit/unit-1.0.0 pixel] type: object properties: name: description: | A user-friendly name for the frame. type: string axes_order: description: | The order of the axes. type: array items: type: integer axes_names: description: | The name of each axis in this frame. type: array items: anyOf: - type: string - type: 'null' reference_frame: description: | The reference frame. $ref: "tag:astropy.org:astropy/coordinates/frames/baseframe-1.0.0" unit: description: | Units for each axis. type: array items: $ref: "tag:stsci.edu:asdf/unit/unit-1.0.0" axis_physical_types: description: | An iterable of strings describing the physical type for each world axis. These should be names from the VO UCD1+ controlled Vocabulary (http://www.ivoa.net/documents/latest/UCDlist.html). type: array items: type: string required: [name] additionalProperties: true gwcs-0.12.0/gwcs/schemas/stsci.edu/gwcs/frame2d-1.0.0.yaml0000644000732200020070000000123413600426757025006 0ustar denchevaSTSCI\science00000000000000%YAML 1.1 --- $schema: "http://stsci.edu/schemas/yaml-schema/draft-01" id: "http://stsci.edu/schemas/gwcs/frame2d-1.0.0" tag: "tag:stsci.edu:gwcs/frame2d-1.0.0" title: > Represents a 2D frame. examples: - - | A two dimensional spatial frame - | ! axes_names: [lon, lat] name: Frame2D unit: [!unit/unit-1.0.0 pixel, !unit/unit-1.0.0 pixel] allOf: - type: object properties: axes_names: minItems: 2 maxItems: 2 axes_order: minItems: 2 maxItems: 2 unit: minItems: 2 maxItems: 2 - $ref: frame-1.0.0 gwcs-0.12.0/gwcs/schemas/stsci.edu/gwcs/grating_equation-1.0.0.yaml0000644000732200020070000000254313600426757027032 0ustar denchevaSTSCI\science00000000000000%YAML 1.1 --- $schema: "http://stsci.edu/schemas/yaml-schema/draft-01" id: "http://stsci.edu/schemas/gwcs/grating_equation-1.0.0" tag: "tag:stsci.edu:gwcs/grating_equation-1.0.0" title: > A grating equation model. description: | Supports two models: - Given incident angle and wavelength compute the refraction/difraction angle. - Given an incident angle and a refraction angle compute the wavelength. examples: - - AnglesFromGratingEquation3D model. - | ! groove_density: 2700.0 order: 2.0 output: angle - - WavelengthFromGratingEquation model. - | ! groove_density: 2700.0 order: 2.0 output: wavelength allOf: - $ref: "tag:stsci.edu:asdf/transform/transform-1.1.0" - type: object properties: groove_density: description: | The groove density of the grating anyOf: - type: number - $ref: "tag:stsci.edu:asdf/unit/quantity-1.1.0" order: description: | Spectral order type: number output: type: string description: | indicates which quantity the grating equation is solved for. enum: [wavelength, angle] required: [groove_density, order, output] gwcs-0.12.0/gwcs/schemas/stsci.edu/gwcs/label_mapper-1.0.0.yaml0000644000732200020070000000772013600426757026117 0ustar denchevaSTSCI\science00000000000000%YAML 1.1 --- $schema: "http://stsci.edu/schemas/yaml-schema/draft-01" id: "http://stsci.edu/schemas/gwcs/label_mapper-1.0.0" tag: "tag:stsci.edu:gwcs/label_mapper-1.0.0" title: > Represents a mapping from a coordinate value to a label. description: | A label mapper instance maps inputs to a label. It is used together with [regions_selector](ref:regions_selector-1.0.0). The [label_mapper](ref:label_mapper-1.0.0) returns the label corresponding to given inputs. The [regions_selector](ref:regions_selector-1.0.0) returns the transform corresponding to this label. This maps inputs (e.g. pixels on a detector) to transforms uniquely. examples: - - Map array indices are to labels. - | ! mapper: !core/ndarray-1.0.0 data: - [1, 0, 2] - [1, 0, 2] - [1, 0, 2] datatype: int64 shape: [3, 3] no_label: 0 - - Map numbers dictionary to transforms which return labels. - | ! atol: 1.0e-08 inputs: [x, y] inputs_mapping: !transform/remap_axes-1.1.0 mapping: [0] n_inputs: 2 mapper: !!omap - !!omap labels: [-1.67833272, -1.9580548, -1.118888] - !!omap models: - !transform/shift-1.1.0 {offset: 6.0} - !transform/shift-1.1.0 {offset: 2.0} - !transform/shift-1.1.0 {offset: 4.0} no_label: 0 - - Map a number within a range of numbers to transforms which return labels. - | ! mapper: !!omap - !!omap labels: - [3.2, 4.1] - [2.67, 2.98] - [1.95, 2.3] - !!omap models: - !transform/shift-1.1.0 {offset: 6.0} - !transform/shift-1.1.0 {offset: 2.0} - !transform/shift-1.1.0 {offset: 4.0} inputs: [x, y] inputs_mapping: !transform/remap_axes-1.1.0 mapping: [0] n_inputs: 2 allOf: - $ref: "tag:stsci.edu:asdf/transform/transform-1.1.0" - type: object properties: mapper: description: | A mapping of inputs to labels. In the general case this is a `astropy.modeling.core.Model`. It could be a numpy array with the shape of the detector/observation. Pixel values are of type integer or string and represent region labels. Pixels which are not within any region have value ``no_label``. It could be a dictionary which maps tuples to labels or floating point numbers to labels. anyOf: - $ref: "tag:stsci.edu:asdf/core/ndarray-1.0.0" - $ref: "tag:stsci.edu:asdf/transform/transform-1.1.0" - type: object properties: labels: type: array items: anyOf: - type: number - type: array items: type: number minLength: 2 maxLength: 2 models: type: array items: $ref: "tag:stsci.edu:asdf/transform/transform-1.1.0" inputs: type: array items: type: string description: | Names of inputs. inputs_mapping: $ref: "tag:stsci.edu:asdf/transform/transform-1.1.0" description: | [mapping](https://asdf-standard.readthedocs.io/en/latest/generated/stsci.edu/asdf/transform/remap_axes-1.1.0.html) atol: type: number description: | absolute tolerance to compare keys in mapper. no_label: description: | Fill in value for missing output. anyOf: - type: number - type: string required: [mapper] gwcs-0.12.0/gwcs/schemas/stsci.edu/gwcs/regions_selector-1.0.0.yaml0000644000732200020070000000636213600426757027043 0ustar denchevaSTSCI\science00000000000000%YAML 1.1 --- $schema: "http://stsci.edu/schemas/yaml-schema/draft-01" id: "http://stsci.edu/schemas/gwcs/regions_selector-1.0.0" tag: "tag:stsci.edu:gwcs/regions_selector-1.0.0" title: > Represents a discontinuous transform. description: | Maps regions to transgorms and evaluates the transforms with the corresponding inputs. examples: - - Create a regions_selector schema for 2 regions, labeled "1" and "2". - | ! inputs: [x, y] label_mapper: ! mapper: !core/ndarray-1.0.0 datatype: int8 data: - [0, 1, 1, 0, 2, 0] - [0, 1, 1, 0, 2, 0] - [0, 1, 1, 0, 2, 0] - [0, 1, 1, 0, 2, 0] - [0, 1, 1, 0, 2, 0] datatype: int64 shape: [5, 6] no_label: 0 outputs: [ra, dec, lam] selector: !!omap - !!omap labels: [1, 2] - !!omap transforms: - !transform/compose-1.1.0 forward: - !transform/remap_axes-1.1.0 mapping: [0, 1, 1] - !transform/concatenate-1.1.0 forward: - !transform/concatenate-1.1.0 forward: - !transform/shift-1.1.0 {offset: 1.0} - !transform/shift-1.1.0 {offset: 2.0} - !transform/shift-1.1.0 {offset: 3.0} - !transform/compose-1.1.0 forward: - !transform/remap_axes-1.1.0 mapping: [0, 1, 1] - !transform/concatenate-1.1.0 forward: - !transform/concatenate-1.1.0 forward: - !transform/scale-1.1.0 {factor: 2.0} - !transform/scale-1.1.0 {factor: 3.0} - !transform/scale-1.1.0 {factor: 3.0} undefined_transform_value: .nan allOf: - $ref: "tag:stsci.edu:asdf/transform/transform-1.1.0" - type: object properties: label_mapper: description: | An instance of [label_mapper-1.1.0](ref:label_mapper-1.0.0) $ref: "./label_mapper-1.0.0" inputs: description: | Names of inputs. type: array items: type: string outputs: description: | Names of outputs. type: array items: type: string selector: description: | A mapping of regions to trransforms. type: object properties: labels: description: | An array of unique region labels. type: array items: type: - integer - string transforms: description: | A transform for each region. The order should match the order of labels. type: array items: $ref: "tag:stsci.edu:asdf/transform/transform-1.1.0" undefined_transform_value: description: | Value to be returned if there's no transform defined for the inputs. type: number required: [label_mapper, inputs, outputs, selector] gwcs-0.12.0/gwcs/schemas/stsci.edu/gwcs/sellmeier_glass-1.0.0.yaml0000644000732200020070000000204413600426757026640 0ustar denchevaSTSCI\science00000000000000%YAML 1.1 --- $schema: "http://stsci.edu/schemas/yaml-schema/draft-01" id: "http://stsci.edu/schemas/gwcs/sellmeier_glass-1.0.0" tag: "tag:stsci.edu:gwcs/sellmeier_glass-1.0.0" title: Sellmeier equation for glass description: | Sellmeier equation for glass. $$ n(\\lambda)^2 = 1 + \\frac{(B1 * \\lambda^2 )}{(\\lambda^2 - C1)} + \\frac{(B2 * \\lambda^2 )}{(\\lambda^2 - C2)} + \\frac{(B3 * \\lambda^2 )}{(\\lambda^2 - C3)} $$ allOf: - $ref: "tag:stsci.edu:asdf/transform/transform-1.1.0" - type: object properties: B_coef: description: | B coefficients in Sellmeier equation. anyOf: - type: array - $ref: "tag:stsci.edu:asdf/core/ndarray-1.0.0" items: type: number minItems: 3 maxItems: 3 C_coef: description: | C coefficients in Sellmeier equation. anyOf: - type: array - $ref: "tag:stsci.edu:asdf/core/ndarray-1.0.0" items: type: number minItems: 3 maxItems: 3 gwcs-0.12.0/gwcs/schemas/stsci.edu/gwcs/sellmeier_zemax-1.0.0.yaml0000644000732200020070000000366613600426757026666 0ustar denchevaSTSCI\science00000000000000%YAML 1.1 --- $schema: "http://stsci.edu/schemas/yaml-schema/draft-01" id: "http://stsci.edu/schemas/gwcs/sellmeier_zemax-1.0.0" tag: "tag:stsci.edu:gwcs/sellmeier_zemax-1.0.0" title: Sellmeier equation for glass used by Zemax description: | Sellmeier equation for glass used by Zemax allOf: - $ref: "tag:stsci.edu:asdf/transform/transform-1.1.0" - type: object properties: B_coef: description: | B coefficients in Sellmeier equation. anyOf: - type: array - $ref: "tag:stsci.edu:asdf/core/ndarray-1.0.0" items: type: number minItems: 3 maxItems: 3 C_coef: description: | C coefficients in Sellmeier equation. anyOf: - type: array - $ref: "tag:stsci.edu:asdf/core/ndarray-1.0.0" items: type: number minItems: 3 maxItems: 3 D_coef: description: | Thermal D coefficients of the glass. anyOf: - type: array - $ref: "tag:stsci.edu:asdf/core/ndarray-1.0.0" items: type: number minItems: 3 maxItems: 3 E_coef: description: | Thermal E coefficients of the glass. anyOf: - type: array - $ref: "tag:stsci.edu:asdf/core/ndarray-1.0.0" items: type: number minItems: 3 maxItems: 3 ref_temperature: description: | Reference temperature of the glass in Kelvin. type: number ref_pressure: description: | Reference pressure of the glass in ATM. type: number temperature: description: | System temperature in Kelvin. type: number pressure: description: | System pressure in ATM. type: number required: [B_coef, C_coef, D_coef, E_coef, ref_temperature, ref_pressure, temperature, pressure] gwcs-0.12.0/gwcs/schemas/stsci.edu/gwcs/snell3d-1.0.0.yaml0000644000732200020070000000056013600426757025033 0ustar denchevaSTSCI\science00000000000000%YAML 1.1 --- $schema: "http://stsci.edu/schemas/yaml-schema/draft-01" id: "http://stsci.edu/schemas/gwcs/snell3d-1.0.0" tag: "tag:stsci.edu:gwcs/snell3d-1.0.0" title: Snell Law in 3D space description: | Snell Law in 3D. Inputs are index of refraction and direction cosines. Outputs are direction cosines. $ref: "tag:stsci.edu:asdf/transform/transform-1.1.0" gwcs-0.12.0/gwcs/schemas/stsci.edu/gwcs/spectral_frame-1.0.0.yaml0000644000732200020070000000116013600426757026453 0ustar denchevaSTSCI\science00000000000000%YAML 1.1 --- $schema: "http://stsci.edu/schemas/yaml-schema/draft-01" id: "http://stsci.edu/schemas/gwcs/spectral_frame-1.0.0" tag: "tag:stsci.edu:gwcs/spectral_frame-1.0.0" title: > Represents a spectral frame. allOf: - type: object properties: reference_position: description: | The position of the reference frame. enum: [geocenter, barycenter, heliocenter] default: geocenter axes_names: minItems: 1 maxItems: 1 axes_order: minItems: 1 maxItems: 1 unit: minItems: 1 maxItems: 1 - $ref: frame-1.0.0 gwcs-0.12.0/gwcs/schemas/stsci.edu/gwcs/step-1.0.0.yaml0000644000732200020070000000140113600426757024435 0ustar denchevaSTSCI\science00000000000000%YAML 1.1 --- $schema: "http://stsci.edu/schemas/yaml-schema/draft-01" id: "http://stsci.edu/schemas/gwcs/step-1.0.0" tag: "tag:stsci.edu:gwcs/step-1.0.0" title: > Describes a single step of a WCS transform pipeline. description: > examples: [] type: object properties: frame: description: | The frame of the inputs to the transform. anyOf: - type: string - $ref: frame-1.0.0 transform: description: | The transform from this step to the next one. The last step in a WCS should not have a transform, but exists only to describe the frames and units of the final output axes. anyOf: - $ref: "tag:stsci.edu:asdf/transform/transform-1.1.0" - type: 'null' default: null required: [frame] gwcs-0.12.0/gwcs/schemas/stsci.edu/gwcs/stokes_frame-1.0.0.yaml0000644000732200020070000000065713600426757026160 0ustar denchevaSTSCI\science00000000000000%YAML 1.1 --- $schema: "http://stsci.edu/schemas/yaml-schema/draft-01" id: "http://stsci.edu/schemas/gwcs/stokes_frame-1.0.0" tag: "tag:stsci.edu:gwcs/stokes_frame-1.0.0" title: > Represents a stokes frame type: object properties: name: description: | A user-friendly name for the frame. type: string axes_order: description: | The order of the axes. type: array items: type: integer gwcs-0.12.0/gwcs/schemas/stsci.edu/gwcs/temporal_frame-1.0.0.yaml0000644000732200020070000000221713600426757026465 0ustar denchevaSTSCI\science00000000000000%YAML 1.1 --- $schema: "http://stsci.edu/schemas/yaml-schema/draft-01" id: "http://stsci.edu/schemas/gwcs/temporal_frame-1.0.0" tag: "tag:stsci.edu:gwcs/temporal_frame-1.0.0" title: > Represents a temporal frame. type: object properties: name: description: | A user-friendly name for the frame. type: string axes_order: description: | The order of the axes. type: array items: type: integer axes_names: description: | The name of each axis in this frame. type: array items: anyOf: - type: string - type: 'null' reference_frame: description: | The reference frame. $ref: "tag:stsci.edu:asdf/time/time-1.1.0" unit: description: | Units for each axis. type: array items: $ref: "tag:stsci.edu:asdf/unit/unit-1.0.0" axis_physical_types: description: | An iterable of strings describing the physical type for each world axis. These should be names from the VO UCD1+ controlled Vocabulary (http://www.ivoa.net/documents/latest/UCDlist.html). type: array items: type: string required: [name] gwcs-0.12.0/gwcs/schemas/stsci.edu/gwcs/wcs-1.0.0.yaml0000644000732200020070000000206313600426757024263 0ustar denchevaSTSCI\science00000000000000%YAML 1.1 --- $schema: "http://stsci.edu/schemas/yaml-schema/draft-01" id: "http://stsci.edu/schemas/gwcs/wcs-1.0.0" tag: "tag:stsci.edu:gwcs/wcs-1.0.0" title: > A system for describing generalized world coordinate transformations. description: > ASDF WCS is a way of specifying transformations (usually from detector space to world coordinate space and back) by using the transformations in the `transform-schema` module. type: object properties: name: description: | A descriptive name for this WCS. type: string steps: description: | A list of steps in the forward transformation from detector to world coordinates. The inverse transformation is determined automatically by reversing this list, and inverting each of the individual transforms according to the rules described in [inverse](https://asdf-standard.readthedocs.io/en/latest/generated/stsci.edu/asdf/transform/transform-1.1.0.html#inverse). type: array items: $ref: step-1.0.0 required: [name, steps] additionalProperties: true gwcs-0.12.0/gwcs/selector.py0000644000732200020070000006024713600426757020064 0ustar denchevaSTSCI\science00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ The classes in this module create discontinuous transforms. The main class is `RegionsSelector`. It maps inputs to transforms and evaluates the transforms on the corresponding inputs. Regions are well defined spaces in the same frame as the inputs. Regions are assigned unique labels (int or str). The region labels are used as a proxy between inputs and transforms. An example is the location of IFU slices in the detector frame. `RegionsSelector` uses two structures: - A mapping of inputs to labels - "label_mapper" - A mapping of labels to transforms - "transform_selector" A "label_mapper" is also a transform, a subclass of `astropy.modeling.core.Model`, which returns the labels corresponding to the inputs. An instance of a ``LabelMapper`` class is passed to `RegionsSelector`. The labels are used by `RegionsSelector` to match inputs to transforms. Finally, `RegionsSelector` evaluates the transforms on the corresponding inputs. Label mappers and transforms take the same inputs as `RegionsSelector`. The inputs should be filtered appropriately using the ``inputs_mapping`` argument which is ian instance of `~astropy.modeling.mappings.Mapping`. The transforms in "transform_selector" should have the same number of inputs and outputs. This is illustrated below using two regions, labeled 1 and 2 :: +-----------+ | +-+ | | | | +-+ | | |1| |2| | | | | +-+ | | +-+ | +-----------+ :: +--------------+ | label mapper | +--------------+ ^ | | V ----------| +-------+ | | label | +--------+ +-------+ ---> | inputs | | +--------+ V | +--------------------+ | | transform_selector | | +--------------------+ V | +-----------+ | | transform |<----------- +------------+ | V +---------+ | outputs | +---------+ The base class _LabelMapper can be subclassed to create other label mappers. """ import warnings import numpy as np from astropy.modeling.core import Model from astropy.modeling import models as astmodels from . import region from .utils import RegionError, _toindex __all__ = ['LabelMapperArray', 'LabelMapperDict', 'LabelMapperRange', 'RegionsSelector', 'LabelMapper'] def get_unique_regions(regions): regions = np.asarray(regions) if isinstance(regions, np.ndarray): unique_regions = np.unique(regions).tolist() try: unique_regions.remove(0) unique_regions.remove('') except ValueError: pass try: unique_regions.remove("") except ValueError: pass elif isinstance(regions, dict): unique_regions = [] for key in regions.keys(): unique_regions.append(regions[key](key)) else: raise TypeError("Unable to get unique regions.") return unique_regions class LabelMapperArrayIndexingError(Exception): def __init__(self, message): super(LabelMapperArrayIndexingError, self).__init__(message) class _LabelMapper(Model): """ Maps inputs to regions. Returns the region labels corresponding to the inputs. Labels are strings or numbers which uniquely identify a location. For example, labels may represent slices of an IFU or names of spherical polygons. Parameters ---------- mapper : object A python structure which represents the labels. Look at subclasses for examples. no_label : str or int "" or 0 A return value for a location which has no corresponding label. inputs_mapping : `~astropy.modeling.mappings.Mapping` An optional Mapping model to be prepended to the LabelMapper with the purpose to filter the inputs or change their order. name : str The name of this transform. """ def __init__(self, mapper, no_label, inputs_mapping=None, name=None, **kwargs): self._no_label = no_label self._inputs_mapping = inputs_mapping self._mapper = mapper super(_LabelMapper, self).__init__(name=name, **kwargs) @property def mapper(self): return self._mapper @property def inputs_mapping(self): return self._inputs_mapping @property def no_label(self): return self._no_label def evaluate(self, *args): raise NotImplementedError("Subclasses should implement this method.") class LabelMapperArray(_LabelMapper): """ Maps array locations to labels. Parameters ---------- mapper : ndarray An array of integers or strings where the values correspond to a label in `~gwcs.selector.RegionsSelector` model. For pixels for which the transform is not defined the value should be set to 0 or " ". inputs_mapping : `~astropy.modeling.mappings.Mapping` An optional Mapping model to be prepended to the LabelMapper with the purpose to filter the inputs or change their order so that the output of it is (x, y) values to index the array. name : str The name of this transform. Use case: For an IFU observation, the array represents the detector and its values correspond to the IFU slice label. """ n_inputs = 2 n_outputs = 1 linear = False fittable = False def __init__(self, mapper, inputs_mapping=None, name=None, **kwargs): if mapper.dtype.type is not np.unicode_: mapper = np.asanyarray(mapper, dtype=np.int) _no_label = 0 else: _no_label = "" super(LabelMapperArray, self).__init__(mapper, _no_label, name=name, **kwargs) self.inputs = ('x', 'y') self.outputs = ('label',) def evaluate(self, *args): args = tuple([_toindex(a) for a in args]) try: result = self._mapper[args[::-1]] except IndexError as e: raise LabelMapperArrayIndexingError(e) return result @classmethod def from_vertices(cls, shape, regions): """ Create a `~gwcs.selector.LabelMapperArray` from polygon vertices stores in a dict. Parameters ---------- shape : tuple shape of mapper array regions: dict {region_label : list_of_polygon_vertices} The keys in this dictionary should match the region labels in `~gwcs.selector.RegionsSelector`. The list of vertices is ordered in such a way that when traversed in a counterclockwise direction, the enclosed area is the polygon. The last vertex must coincide with the first vertex, minimum 4 vertices are needed to define a triangle. Returns ------- mapper : `~gwcs.selector.LabelMapperArray` This models is used with `~gwcs.selector.RegionsSelector`. A model which takes the same inputs as `~gwcs.selector.RegionsSelector` and returns a label. Examples -------- >>> regions = {1: [[795, 970], [2047, 970], [2047, 999], [795, 999], [795, 970]], ... 2: [[844, 1067], [2047, 1067], [2047, 1113], [844, 1113], [844, 1067]], ... 3: [[654, 1029], [2047, 1029], [2047, 1078], [654, 1078], [654, 1029]], ... 4: [[772, 990], [2047, 990], [2047, 1042], [772, 1042], [772, 990]] ... } >>> mapper = LabelMapperArray.from_vertices((2400, 2400), regions) """ labels = np.array(list(regions.keys())) mask = np.zeros(shape, dtype=labels.dtype) for rid, vert in regions.items(): pol = region.Polygon(rid, vert) mask = pol.scan(mask) return cls(mask) class LabelMapperDict(_LabelMapper): """ Maps a number to a transform, which when evaluated returns a label. Use case: inverse transforms of an IFU. For an IFU observation, the keys are constant angles (corresponding to a slice) and values are transforms which return a slice number. Parameters ---------- inputs : tuple of str Names for the inputs, e.g. ('alpha', 'beta', lam') mapper : dict Maps key values to transforms. inputs_mapping : `~astropy.modeling.mappings.Mapping` An optional Mapping model to be prepended to the LabelMapper with the purpose to filter the inputs or change their order. It returns a number which is one of the keys of ``mapper``. atol : float Absolute tolerance when comparing inputs to ``mapper.keys``. It is passed to np.isclose. name : str The name of this transform. """ standard_broadcasting = False linear = False fittable = False n_outputs = 1 def __init__(self, inputs, mapper, inputs_mapping=None, atol=10**-8, name=None, **kwargs): self._atol = atol _no_label = 0 self._inputs = inputs self._n_inputs = len(inputs) if not all([m.n_outputs == 1 for m in mapper.values()]): raise TypeError("All transforms in mapper must have one output.") self._input_units_strict = {key: False for key in self._inputs} self._input_units_allow_dimensionless = {key: False for key in self._inputs} super(LabelMapperDict, self).__init__(mapper, _no_label, inputs_mapping, name=name, **kwargs) self.outputs = ('labels',) @property def n_inputs(self): return self._n_inputs @property def inputs(self): """ The name(s) of the input variable(s) on which a model is evaluated. """ return self._inputs @inputs.setter def inputs(self, val): """ The name(s) of the input variable(s) on which a model is evaluated. """ self._inputs = val @property def atol(self): return self._atol @atol.setter def atol(self, val): self._atol = val def evaluate(self, *args): shape = args[0].shape args = [a.flatten() for a in args] # if n_inputs > 1, determine which one is to be used as keys if self.inputs_mapping is not None: keys = self._inputs_mapping.evaluate(*args) else: keys = args keys = keys.flatten() # create an empty array for the results res = np.zeros(keys.shape) + self._no_label # If this is part of a combined transform, some of the inputs # may be NaNs. # Set NaNs to the ``_no_label`` value mapper_keys = list(self.mapper.keys()) # Loop over the keys in mapper and compare to inputs. # Find the indices where they are within ``atol`` # and evaluate the transform to get the corresponding label. for key in mapper_keys: ind = np.isclose(key, keys, atol=self._atol) inputs = [a[ind] for a in args] res[ind] = self.mapper[key](*inputs) res.shape = shape return res class LabelMapperRange(_LabelMapper): """ The structure this class uses maps a range of values to a transform. Given an input value it finds the range the value falls in and returns the corresponding transform. When evaluated the transform returns a label. Example: Pick a transform based on wavelength range. For an IFU observation, the keys are (lambda_min, lambda_max) tuples and values are transforms which return a label corresponding to a slice. Parameters ---------- inputs : tuple of str Names for the inputs, e.g. ('alpha', 'beta', 'lambda') mapper : dict Maps tuples of length 2 to transforms. inputs_mapping : `~astropy.modeling.mappings.Mapping` An optional Mapping model to be prepended to the LabelMapper with the purpose to filter the inputs or change their order. atol : float Absolute tolerance when comparing inputs to ``mapper.keys``. It is passed to np.isclose. name : str The name of this transform. """ standard_broadcasting = False n_outputs = 1 linear = False fittable = False def __init__(self, inputs, mapper, inputs_mapping=None, name=None, **kwargs): if self._has_overlapping(np.array(list(mapper.keys()))): raise ValueError("Overlapping ranges of values are not supported.") self._inputs = inputs self._n_inputs = len(inputs) _no_label = 0 if not all([m.n_outputs == 1 for m in mapper.values()]): raise TypeError("All transforms in mapper must have one output.") self._input_units_strict = {key: False for key in self._inputs} self._input_units_allow_dimensionless = {key: False for key in self._inputs} super(LabelMapperRange, self).__init__(mapper, _no_label, inputs_mapping, name=name, **kwargs) self.outputs = ('labels',) @property def n_inputs(self): return self._n_inputs @property def inputs(self): """ The name(s) of the input variable(s) on which a model is evaluated. """ return self._inputs @inputs.setter def inputs(self, val): """ The name(s) of the input variable(s) on which a model is evaluated. """ self._inputs = val @staticmethod def _has_overlapping(ranges): """ Test a list of tuple representing ranges of values has no overlapping ranges. """ d = dict(ranges) start = ranges[:, 0] end = ranges[:, 1] start.sort() l = [] for v in start: l.append([v, d[v]]) l = np.array(l) start = np.roll(l[:, 0], -1) end = l[:, 1] if any((end - start)[:-1] > 0) or any(start[-1] > end): return True else: return False # move this to utils? def _find_range(self, value_range, value): """ Returns the index of the tuple which holds value. Parameters ---------- value_range : np.ndarray an (2, 2) array of non-overlapping (min, max) values value : float The value Returns ------- ind : int Index of the tuple which defines a range holding the input value. None, if the input value is not within any available range. """ a, b = value_range[:, 0], value_range[:, 1] ind = np.logical_and(value >= a, value <= b).nonzero()[0] if ind.size > 1: raise ValueError("There are overlapping ranges.") elif ind.size == 0: return None else: return ind.item() def evaluate(self, *args): shape = args[0].shape args = [a.flatten() for a in args] if self.inputs_mapping is not None: keys = self._inputs_mapping.evaluate(*args) else: keys = args keys = keys.flatten() # Define an array for the results. res = np.zeros(keys.shape) + self._no_label nan_ind = np.isnan(keys) res[nan_ind] = self._no_label value_ranges = list(self.mapper.keys()) # For each tuple in mapper, find the indices of the inputs # which fall within the range it defines. for val_range in value_ranges: temp = keys.copy() temp[nan_ind] = np.nan temp = np.where(np.logical_or(temp <= val_range[0], temp >= val_range[1]), np.nan, temp) ind = ~np.isnan(temp) if ind.any(): inputs = [a[ind] for a in args] res[ind] = self.mapper[tuple(val_range)](*inputs) else: continue res.shape = shape if len(np.nonzero(res)[0]) == 0: warnings.warn("All data is outside the valid range - {0}.".format(self.name)) return res class RegionsSelector(Model): """ This model defines discontinuous transforms. It maps inputs to their corresponding transforms. It uses an instance of `_LabelMapper` as a proxy to map inputs to the correct region. Parameters ---------- inputs : list of str Names of the inputs. outputs : list of str Names of the outputs. selector : dict Mapping of region labels to transforms. Labels can be of type int or str, transforms are of type `~astropy.modeling.core.Model`. label_mapper : a subclass of `~gwcs.selector._LabelMapper` A model which maps locations to region labels. undefined_transform_value : float, np.nan (default) Value to be returned if there's no transform defined for the inputs. name : str The name of this transform. """ standard_broadcasting = False linear = False fittable = False def __init__(self, inputs, outputs, selector, label_mapper, undefined_transform_value=np.nan, name=None, **kwargs): self._inputs = inputs self._outputs = outputs self._n_inputs = len(inputs) self._n_outputs = len(outputs) self.label_mapper = label_mapper self._undefined_transform_value = undefined_transform_value self._selector = selector # copy.deepcopy(selector) if " " in selector.keys() or 0 in selector.keys(): raise ValueError('"0" and " " are not allowed as keys.') self._input_units_strict = {key: False for key in self._inputs} self._input_units_allow_dimensionless = {key: False for key in self._inputs} super(RegionsSelector, self).__init__(n_models=1, name=name, **kwargs) def set_input(self, rid): """ Sets one of the inputs and returns a transform associated with it. """ if rid in self._selector: return self._selector[rid] else: raise RegionError("Region {0} not found".format(rid)) def inverse(self): if self.label_mapper.inverse is not None: try: transforms_inv = {} for rid in self._selector: transforms_inv[rid] = self._selector[rid].inverse except AttributeError: raise NotImplementedError("The inverse of all regions must be defined" "for RegionsSelector to have an inverse.") return self.__class__(self.outputs, self.inputs, transforms_inv, self.label_mapper.inverse) else: raise NotImplementedError("The label mapper must have an inverse " "for RegionsSelector to have an inverse.") def evaluate(self, *args): """ Parameters ---------- args : float or ndarray Input pixel coordinate, one input for each dimension. """ # Get the region labels corresponding to these inputs rids = self.label_mapper(*args).flatten() # Raise an error if all pixels are outside regions if (rids == self.label_mapper.no_label).all(): warnings.warn("The input positions are not inside any region.") # Create output arrays and set any pixels not within regions to # "undefined_transform_value" no_trans_ind = (rids == self.label_mapper.no_label).nonzero() outputs = [np.empty(rids.shape) for n in range(self.n_outputs)] for out in outputs: out[no_trans_ind] = self.undefined_transform_value # Compute the transformations args = [a.flatten() for a in args] uniq = get_unique_regions(rids) for rid in uniq: ind = (rids == rid) inputs = [a[ind] for a in args] if rid in self._selector: result = self._selector[rid](*inputs) else: # If there's no transform for a label, return np.nan result = [np.empty(inputs[0].shape) + self._undefined_transform_value for i in range(self.n_outputs)] for j in range(self.n_outputs): outputs[j][ind] = result[j] return outputs @property def undefined_transform_value(self): return self._undefined_transform_value @undefined_transform_value.setter def undefined_transform_value(self, value): self._undefined_transform_value = value @property def outputs(self): """The name(s) of the output(s) of the model.""" return self._outputs @property def selector(self): return self._selector @property def inputs(self): """ The name(s) of the input variable(s) on which a model is evaluated. """ return self._inputs @inputs.setter def inputs(self, val): """ The name(s) of the input variable(s) on which a model is evaluated. """ self._inputs = val @outputs.setter def outputs(self, val): """ The name(s) of the output variable(s). """ self._outputs = val @property def n_inputs(self): return self._n_inputs @property def n_outputs(self): return self._n_outputs class LabelMapper(_LabelMapper): """ Maps inputs to regions. Returns the region labels corresponding to the inputs. Labels are strings or numbers which uniquely identify a location. For example, labels may represent slices of an IFU or names of spherical polygons. Parameters ---------- mapper : `~astropy.modeling.core.Model` A function which returns a region. no_label : str or int "" or 0 A return value for a location which has no corresponding label. inputs_mapping : `~astropy.modeling.mappings.Mapping` or tuple An optional Mapping model to be prepended to the LabelMapper with the purpose to filter the inputs or change their order. If tuple, a `~astropy.modeling.mappings.Mapping` model will be created from it. name : str The name of this transform. """ n_outputs = 1 def __init__(self, inputs, mapper, no_label=np.nan, inputs_mapping=None, name=None, **kwargs): self._no_label = no_label self._inputs = inputs self._n_inputs = len(inputs) self._outputs = tuple(['x{0}'.format(ind) for ind in list(range(mapper.n_outputs))]) if isinstance(inputs_mapping, tuple): inputs_mapping = astmodels.Mapping(inputs_mapping) elif inputs_mapping is not None and not isinstance(inputs_mapping, astmodels.Mapping): raise TypeError("inputs_mapping must be an instance of astropy.modeling.Mapping.") self._inputs_mapping = inputs_mapping self._mapper = mapper self._input_units_strict = {key: False for key in self._inputs} self._input_units_allow_dimensionless = {key: False for key in self._inputs} super(_LabelMapper, self).__init__(name=name, **kwargs) self.outputs = ('label',) @property def inputs(self): """ The name(s) of the input variable(s) on which a model is evaluated. """ return self._inputs @inputs.setter def inputs(self, val): """ The name(s) of the input variable(s) on which a model is evaluated. """ self._inputs = val @property def n_inputs(self): return self._n_inputs @property def mapper(self): return self._mapper @property def inputs_mapping(self): return self._inputs_mapping @property def no_label(self): return self._no_label def evaluate(self, *args): if self.inputs_mapping is not None: args = self.inputs_mapping(*args) if self.n_outputs == 1: args = [args] res = self.mapper(*args) if np.isscalar(res): res = np.array([res]) return np.array(res) gwcs-0.12.0/gwcs/spectroscopy.py0000644000732200020070000002565113600426757021001 0ustar denchevaSTSCI\science00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Spectroscopy related models. """ import numpy as np from astropy.modeling.core import Model from astropy.modeling.parameters import Parameter import astropy.units as u __all__ = ['WavelengthFromGratingEquation', 'AnglesFromGratingEquation3D', 'Snell3D', 'SellmeierGlass', 'SellmeierZemax'] class WavelengthFromGratingEquation(Model): r""" Solve the Grating Dispersion Law for the wavelength. .. Note:: This form of the equation can be used for paraxial (small angle approximation) as well as oblique incident angles. With paraxial systems the inputs are ``sin`` of the angles and it transforms to :math:`(\sin(alpha_in) + \sin(alpha_out)) / (groove_density * spectral_order)`. With oblique angles the inputs are the direction cosines of the angles. Parameters ---------- groove_density : int Grating ruling density in units of 1/length. spectral_order : int Spectral order. Examples -------- >>> from astropy.modeling.models import math >>> model = WavelengthFromGratingEquation(groove_density=20000*1/u.m, spectral_order=-1) >>> alpha_in = (math.Deg2radUfunc() | math.SinUfunc())(.0001 * u.deg) >>> alpha_out = (math.Deg2radUfunc() | math.SinUfunc())(.0001 * u.deg) >>> lam = model(alpha_in, alpha_out) >>> print(lam) -1.7453292519934437e-10 m """ _separable = False linear = False n_inputs = 2 n_outputs = 1 groove_density = Parameter(default=1) """ Grating ruling density in units of 1/m.""" spectral_order = Parameter(default=1) """ Spectral order.""" def __init__(self, groove_density, spectral_order, **kwargs): super().__init__(groove_density=groove_density, spectral_order=spectral_order, **kwargs) self.inputs = ("alpha_in", "alpha_out") """ Sine function of the angles or the direction cosines.""" self.outputs = ("wavelength",) """ Wavelength.""" def evaluate(self, alpha_in, alpha_out, groove_density, spectral_order): return (alpha_in + alpha_out) / (groove_density * spectral_order) @property def return_units(self): if self.groove_density.unit is None: return None return {'wavelength': u.Unit(1 / self.groove_density.unit)} class AnglesFromGratingEquation3D(Model): """ Solve the 3D Grating Dispersion Law in Direction Cosine space for the refracted angle. Parameters ---------- groove_density : int Grating ruling density in units of 1/m. order : int Spectral order. Examples -------- >>> from astropy.modeling.models import math >>> model = AnglesFromGratingEquation3D(groove_density=20000*1/u.m, spectral_order=-1) >>> alpha_in = (math.Deg2radUfunc() | math.SinUfunc())(.0001 * u.deg) >>> beta_in = (math.Deg2radUfunc() | math.SinUfunc())(.0001 * u.deg) >>> lam = 2e-6 * u.m >>> alpha_out, beta_out, gamma_out = model(lam, alpha_in, beta_in) >>> print(alpha_out, beta_out, gamma_out) 0.04000174532925199 -1.7453292519934436e-06 0.9991996098716049 """ _separable = False linear = False n_inputs = 3 n_outputs = 3 groove_density = Parameter(default=1) """ Grating ruling density in units 1/ length.""" spectral_order = Parameter(default=1) """ Spectral order.""" def __init__(self, groove_density, spectral_order, **kwargs): super().__init__(groove_density=groove_density, spectral_order=spectral_order, **kwargs) self.inputs = ("wavelength", "alpha_in", "beta_in") """ Wavelength and 2 angle coordinates going into the grating.""" self.outputs = ("alpha_out", "beta_out", "gamma_out") """ Two angles coming out of the grating. """ def evaluate(self, wavelength, alpha_in, beta_in, groove_density, spectral_order): if alpha_in.shape != beta_in.shape: raise ValueError("Expected input arrays to have the same shape.") if isinstance(groove_density, u.Quantity): alpha_in = u.Quantity(alpha_in) beta_in = u.Quantity(beta_in) alpha_out = -groove_density * spectral_order * wavelength + alpha_in beta_out = - beta_in gamma_out = np.sqrt(1 - alpha_out ** 2 - beta_out ** 2) return alpha_out, beta_out, gamma_out @property def input_units(self): if self.groove_density.unit is None: return None return {'wavelength': 1 / self.groove_density.unit, 'alpha_in': u.Unit(1), 'beta_in': u.Unit(1)} class Snell3D(Model): """ Snell model in 3D form. Inputs are index of refraction and direction cosines. Returns ------- alpha_out, beta_out, gamma_out : float Direction cosines. """ _separable = False linear = False n_inputs = 4 n_outputs = 3 def __init__(self, **kwargs): super().__init__(**kwargs) self.inputs = ('n', 'alpha_in', 'beta_in', 'gamma_in') self.outputs = ('alpha_out', 'beta_out', 'gamma_out') @staticmethod def evaluate(n, alpha_in, beta_in, gamma_in): # Apply Snell's law through front surface, # eq 5.3.3 II in Nirspec docs alpha_out = alpha_in / n beta_out = beta_in / n gamma_out = np.sqrt(1.0 - alpha_out**2 - beta_out**2) return alpha_out, beta_out, gamma_out class SellmeierGlass(Model): """ Sellmeier equation for glass. Parameters ---------- B_coef : ndarray Iterable of size 3 containing B coefficients. C_coef : ndarray Iterable of size 3 containing c coefficients in units of `u.um**2`. Returns ------- n : float Refractive index. Examples -------- >>> import astropy.units as u >>> b_coef = [0.58339748, 0.46085267, 3.8915394] >>> c_coef = [0.00252643, 0.010078333, 1200.556] * u.um**2 >>> model = SellmeierGlass(b_coef, c_coef) >>> model(2 * u.m) References ---------- .. [1] https://en.wikipedia.org/wiki/Sellmeier_equation Notes ----- Model formula: .. math:: n(\\lambda)^2 = 1 + \\frac{(B1 * \\lambda^2 )}{(\\lambda^2 - C1)} + \\frac{(B2 * \\lambda^2 )}{(\\lambda^2 - C2)} + \\frac{(B3 * \\lambda^2 )}{(\\lambda^2 - C3)} """ _separable = False standard_broadcasting = False linear = False n_inputs = 1 n_outputs = 1 B_coef = Parameter(default=np.array([1, 1, 1])) """ B1, B2, B3 coefficients. """ C_coef = Parameter(default=np.array([0, 0, 0])) """ C1, C2, C3 coefficients in units of um ** 2. """ def __init__(self, B_coef, C_coef, **kwargs): super().__init__(B_coef, C_coef) self.inputs = ('wavelength',) self.outputs = ('n',) @staticmethod def evaluate(wavelength, B_coef, C_coef): B1, B2, B3 = B_coef[0] C1, C2, C3 = C_coef[0] n = np.sqrt(1. + B1 * wavelength ** 2 / (wavelength ** 2 - C1) + B2 * wavelength ** 2 / (wavelength ** 2 - C2) + B3 * wavelength ** 2 / (wavelength ** 2 - C3) ) return n @property def input_units(self): if self.C_coef.unit is None: return None return {'wavelength': u.um} class SellmeierZemax(Model): """ Sellmeier equation used by Zemax. Parameters ---------- temperature : float Temperature of the material in `u.Kelvin`. ref_temperature : float Reference emperature of the glass in `u.Kelvin`. ref_pressure : float Reference pressure in ATM. pressure : float Measured pressure in ATM. B_coef : ndarray Iterable of size 3 containing B coefficients. C_coef : ndarray Iterable of size 3 containing C coefficients in units of `u.um**2`. D_coef : ndarray Iterable of size 3 containing constants to describe the behavior of the material. E_coef : ndarray Iterable of size 3 containing constants to describe the behavior of the material. Returns ------- n : float Refractive index. """ _separable = False standard_broadcasting = False linear = False n_inputs = 1 n_outputs = 1 temperature = Parameter(default=0) ref_temperature = Parameter(default=0) ref_pressure = Parameter(default=0) pressure = Parameter(default=0) B_coef = Parameter(default=[1, 1, 1]) C_coef = Parameter(default=[0, 0, 0]) D_coef = Parameter(default=[0, 0, 0]) E_coef = Parameter(default=[1, 1, 1]) def __init__(self, temperature=temperature, ref_temperature=ref_temperature, ref_pressure=ref_pressure, pressure=pressure, B_coef=B_coef, C_coef=C_coef, D_coef=D_coef, E_coef=E_coef, **kwargs): super().__init__(temperature=temperature, ref_temperature=ref_temperature, ref_pressure=ref_pressure, pressure=pressure, B_coef=B_coef, C_coef=C_coef, D_coef=D_coef, E_coef=E_coef, **kwargs) self.inputs = ('wavelength',) self.outputs = ('n',) def evaluate(self, wavelength, temp, ref_temp, ref_pressure, pressure, B_coef, C_coef, D_coef, E_coef): """ Input ``wavelength`` is in units of microns. """ if isinstance(temp, u.Quantity): temp = temp.to(u.Celsius) ref_temp = ref_temp.to(u.Celsius) else: KtoC = 273.15 # kelvin to celcius conversion temp -= KtoC ref_temp -= KtoC delt = temp - ref_temp D0, D1, D2 = D_coef[0] E0, E1, lam_tk = E_coef[0] nref = 1. + (6432.8 + 2949810. * wavelength ** 2 / (146.0 * wavelength ** 2 - 1.) + (5540.0 * wavelength ** 2) / (41.0 * wavelength ** 2 - 1.)) * 1e-8 # T should be in C, P should be in ATM nair_obs = 1.0 + ((nref - 1.0) * pressure) / (1.0 + (temp - 15.) * 3.4785e-3) nair_ref = 1.0 + ((nref - 1.0) * ref_pressure) / (1.0 + (ref_temp - 15) * 3.4785e-3) # Compute the relative index of the glass at Tref and Pref using Sellmeier equation I. lamrel = wavelength * nair_obs / nair_ref nrel = SellmeierGlass.evaluate(lamrel[0], B_coef, C_coef) # Convert the relative index of refraction at the reference temperature and pressure # to absolute. nabs_ref = nrel * nair_ref # Compute the absolute index of the glass delnabs = (0.5 * (nrel ** 2 - 1.) / nrel) * \ (D0 * delt + D1 * delt ** 2 + D2 * delt ** 3 + \ (E0 * delt + E1 * delt ** 2) / (lamrel ** 2 - lam_tk ** 2)) nabs_obs = nabs_ref + delnabs # Define the relative index at the system's operating T and P. n = nabs_obs / nair_obs return n gwcs-0.12.0/gwcs/tags/0000755000732200020070000000000013600427270016606 5ustar denchevaSTSCI\science00000000000000gwcs-0.12.0/gwcs/tags/__init__.py0000644000732200020070000000036013600426757020727 0ustar denchevaSTSCI\science00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst # -*- coding: utf-8 -*- from astropy import units as u def _parameter_to_value(param): if param.unit is not None: return u.Quantity(param) return param.value gwcs-0.12.0/gwcs/tags/geometry_models.py0000644000732200020070000000233013600426757022365 0ustar denchevaSTSCI\science00000000000000""" ASDF tags for geometry related models. """ from asdf import yamlutil from ..gwcs_types import GWCSTransformType from .. geometry import ToDirectionCosines, FromDirectionCosines __all__ = ['DirectionCosinesType'] class DirectionCosinesType(GWCSTransformType): name = "direction_cosines" types = [ToDirectionCosines, FromDirectionCosines] version = "1.0.0" @classmethod def from_tree_transform(cls, node, ctx): transform_type = node['transform_type'] if transform_type == 'to_direction_cosines': return ToDirectionCosines() elif transform_type == 'from_direction_cosines': return FromDirectionCosines() else: raise TypeError(f"Unknown model_type {transform_type}") @classmethod def to_tree_transform(cls, model, ctx): if isinstance(model, FromDirectionCosines): transform_type = 'from_direction_cosines' elif isinstance(model, ToDirectionCosines): transform_type = 'to_direction_cosines' else: raise TypeError(f"Model of type {model.__class__} is not supported.") node = {'transform_type': transform_type} return yamlutil.custom_tree_to_tagged_tree(node, ctx) gwcs-0.12.0/gwcs/tags/selectortags.py0000644000732200020070000001351713600426757021677 0ustar denchevaSTSCI\science00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst # -*- coding: utf-8 -*- from collections import OrderedDict import numpy as np from numpy.testing import assert_array_equal from astropy.modeling import models from astropy.modeling.core import Model from astropy.utils.misc import isiterable from asdf import yamlutil from asdf.tags.core.ndarray import NDArrayType from ..gwcs_types import GWCSTransformType from ..selector import * __all__ = ['LabelMapperType', 'RegionsSelectorType'] class LabelMapperType(GWCSTransformType): name = "label_mapper" types = [LabelMapperArray, LabelMapperDict, LabelMapperRange, LabelMapper] version = "1.0.0" @classmethod def from_tree_transform(cls, node, ctx): inputs_mapping = node.get('inputs_mapping', None) if inputs_mapping is not None and not isinstance(inputs_mapping, models.Mapping): raise TypeError("inputs_mapping must be an instance" "of astropy.modeling.models.Mapping.") mapper = node['mapper'] atol = node.get('atol', 10**-8) no_label = node.get('no_label', np.nan) if isinstance(mapper, NDArrayType): if mapper.ndim != 2: raise NotImplementedError( "GWCS currently only supports 2x2 masks ") return LabelMapperArray(mapper, inputs_mapping) elif isinstance(mapper, Model): inputs = node.get('inputs') return LabelMapper(inputs, mapper, inputs_mapping=inputs_mapping, no_label=no_label) else: inputs = node.get('inputs', None) if inputs is not None: inputs = tuple(inputs) labels = mapper.get('labels') transforms = mapper.get('models') if isiterable(labels[0]): labels = [tuple(l) for l in labels] dict_mapper = dict(zip(labels, transforms)) return LabelMapperRange(inputs, dict_mapper, inputs_mapping) else: dict_mapper = dict(zip(labels, transforms)) return LabelMapperDict(inputs, dict_mapper, inputs_mapping, atol=atol) @classmethod def to_tree_transform(cls, model, ctx): node = OrderedDict() node['no_label'] = model.no_label if model.inputs_mapping is not None: node['inputs_mapping'] = model.inputs_mapping if isinstance(model, LabelMapperArray): node['mapper'] = model.mapper elif isinstance(model, LabelMapper): node['mapper'] = model.mapper node['inputs'] = list(model.inputs) elif isinstance(model, (LabelMapperDict, LabelMapperRange)): if hasattr(model, 'atol'): node['atol'] = model.atol mapper = OrderedDict() labels = list(model.mapper) transforms = [] for k in labels: transforms.append(model.mapper[k]) if isiterable(labels[0]): labels = [list(l) for l in labels] mapper['labels'] = labels mapper['models'] = transforms node['mapper'] = mapper node['inputs'] = list(model.inputs) else: raise TypeError("Unrecognized type of LabelMapper - {0}".format(model)) return yamlutil.custom_tree_to_tagged_tree(node, ctx) @classmethod def assert_equal(cls, a, b): # TODO: If models become comparable themselves, remove this. assert (a.__class__ == b.__class__) # nosec if isinstance(a.mapper, dict): assert(a.mapper.__class__ == b.mapper.__class__) # nosec assert(all(np.in1d(list(a.mapper), list(b.mapper)))) # nosec for k in a.mapper: assert (a.mapper[k].__class__ == b.mapper[k].__class__) # nosec assert(all(a.mapper[k].parameters == b.mapper[k].parameters)) # nosec assert (a.inputs == b.inputs) # nosec assert (a.inputs_mapping.mapping == b.inputs_mapping.mapping) # nosec else: assert_array_equal(a.mapper, b.mapper) class RegionsSelectorType(GWCSTransformType): name = "regions_selector" types = [RegionsSelector] version = "1.0.0" @classmethod def from_tree_transform(cls, node, ctx): inputs = node['inputs'] outputs = node['outputs'] label_mapper = node['label_mapper'] undefined_transform_value = node['undefined_transform_value'] sel = node['selector'] sel = dict(zip(sel['labels'], sel['transforms'])) return RegionsSelector(inputs, outputs, sel, label_mapper, undefined_transform_value) @classmethod def to_tree_transform(cls, model, ctx): selector = OrderedDict() node = OrderedDict() labels = list(model.selector) values = [] for l in labels: values.append(model.selector[l]) selector['labels'] = labels selector['transforms'] = values node['inputs'] = list(model.inputs) node['outputs'] = list(model.outputs) node['selector'] = selector node['label_mapper'] = model.label_mapper node['undefined_transform_value'] = model.undefined_transform_value return yamlutil.custom_tree_to_tagged_tree(node, ctx) @classmethod def assert_equal(cls, a, b): # TODO: If models become comparable themselves, remove this. assert (a.__class__ == b.__class__) # nosec LabelMapperType.assert_equal(a.label_mapper, b.label_mapper) assert_array_equal(a.inputs, b.inputs) assert_array_equal(a.outputs, b.outputs) assert_array_equal(a.selector.keys(), b.selector.keys()) for key in a.selector: assert_array_equal(a.selector[key].parameters, b.selector[key].parameters) assert_array_equal(a.undefined_transform_value, b.undefined_transform_value) gwcs-0.12.0/gwcs/tags/spectroscopy_models.py0000644000732200020070000001073113600426757023273 0ustar denchevaSTSCI\science00000000000000""" ASDF tags for spectroscopy related models. """ import numpy as np from numpy.testing import assert_array_equal from astropy import units as u from astropy.tests.helper import assert_quantity_allclose from asdf import yamlutil from ..gwcs_types import GWCSTransformType from .. spectroscopy import * from . import _parameter_to_value __all__ = ['GratingEquationType', 'SellmeierGlassType', 'SellmeierZemaxType', 'Snell3D'] class SellmeierGlassType(GWCSTransformType): name = "sellmeier_glass" types = [SellmeierGlass] version = "1.0.0" @classmethod def from_tree_transform(cls, node, ctx): return SellmeierGlass(node['B_coef'], node['C_coef']) @classmethod def to_tree_transform(cls, model, ctx): node = {'B_coef': _parameter_to_value(model.B_coef), 'C_coef': _parameter_to_value(model.C_coef)} return yamlutil.custom_tree_to_tagged_tree(node, ctx) class SellmeierZemaxType(GWCSTransformType): name = "sellmeier_zemax" types = [SellmeierZemax] version = "1.0.0" @classmethod def from_tree_transform(cls, node, ctx): return SellmeierZemax(node['temperature'], node['ref_temperature'], node['ref_pressure'], node['pressure'], node['B_coef'], node['C_coef'], node['D_coef'], node['E_coef']) @classmethod def to_tree_transform(cls, model, ctx): node = {'B_coef': _parameter_to_value(model.B_coef), 'C_coef': _parameter_to_value(model.C_coef), 'D_coef': _parameter_to_value(model.D_coef), 'E_coef': _parameter_to_value(model.E_coef), 'temperature': _parameter_to_value(model.temperature), 'ref_temperature': _parameter_to_value(model.ref_temperature), 'pressure': _parameter_to_value(model.pressure), 'ref_pressure': _parameter_to_value(model.ref_pressure)} return yamlutil.custom_tree_to_tagged_tree(node, ctx) class Snell3DType(GWCSTransformType): name = "snell3d" types = [Snell3D] version = "1.0.0" @classmethod def from_tree_transform(cls, node, ctx): return Snell3D() @classmethod def to_tree_transform(cls, model, ctx): return yamlutil.custom_tree_to_tagged_tree({}, ctx) class GratingEquationType(GWCSTransformType): name = "grating_equation" version = '1.0.0' types = [AnglesFromGratingEquation3D, WavelengthFromGratingEquation] @classmethod def from_tree_transform(cls, node, ctx): groove_density = node['groove_density'] order = node['order'] output = node['output'] if output == "wavelength": model = WavelengthFromGratingEquation(groove_density=groove_density, spectral_order=order) elif output == "angle": model = AnglesFromGratingEquation3D(groove_density=groove_density, spectral_order=order) else: raise ValueError("Can't create a GratingEquation model with " "output {0}".format(output)) return model @classmethod def to_tree_transform(cls, model, ctx): if model.groove_density.unit is not None: groove_density = u.Quantity(model.groove_density.value, unit=model.groove_density.unit) else: groove_density = model.groove_density.value node = {'order': model.spectral_order.value, 'groove_density': groove_density } if isinstance(model, AnglesFromGratingEquation3D): node['output'] = 'angle' elif isinstance(model, WavelengthFromGratingEquation): node['output'] = 'wavelength' else: raise TypeError("Can't serialize an instance of {0}" .format(model.__class__.__name__)) return yamlutil.custom_tree_to_tagged_tree(node, ctx) @classmethod def assert_equal(cls, a, b): if isinstance(a, AnglesFromGratingEquation3D): assert isinstance(b, AnglesFromGratingEquation3D) # nosec elif isinstance(a, WavelengthFromGratingEquation): assert isinstance(b, WavelengthFromGratingEquation) # nosec assert_quantity_allclose(a.groove_density, b.groove_density) # nosec assert a.spectral_order.value == b.spectral_order.value # nosec gwcs-0.12.0/gwcs/tags/tests/0000755000732200020070000000000013600427270017750 5ustar denchevaSTSCI\science00000000000000gwcs-0.12.0/gwcs/tags/tests/__init__.py0000644000732200020070000000000013600426757022060 0ustar denchevaSTSCI\science00000000000000gwcs-0.12.0/gwcs/tags/tests/test_selector.py0000644000732200020070000000515113600426757023214 0ustar denchevaSTSCI\science00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst # -*- coding: utf-8 -*- import numpy as np from astropy.modeling.models import Mapping, Shift, Scale, Polynomial2D from ... import selector from asdf.tests import helpers from ...tests.test_region import create_scalar_mapper from ...extension import GWCSExtension def test_regions_selector(tmpdir): m1 = Mapping([0, 1, 1]) | Shift(1) & Shift(2) & Shift(3) m2 = Mapping([0, 1, 1]) | Scale(2) & Scale(3) & Scale(3) sel = {1: m1, 2: m2} a = np.zeros((5, 6), dtype=np.int32) a[:, 1:3] = 1 a[:, 4:5] = 2 mask = selector.LabelMapperArray(a) rs = selector.RegionsSelector(inputs=('x', 'y'), outputs=('ra', 'dec', 'lam'), selector=sel, label_mapper=mask) tree = {'model': rs} helpers.assert_roundtrip_tree(tree, tmpdir, extensions=GWCSExtension()) def test_LabelMapperArray_str(tmpdir): a = np.array([["label1", "", "label2"], ["label1", "", ""], ["label1", "label2", "label2"]]) mask = selector.LabelMapperArray(a) tree = {'model': mask} helpers.assert_roundtrip_tree(tree, tmpdir, extensions=GWCSExtension()) def test_labelMapperArray_int(tmpdir): a = np.array([[1, 0, 2], [1, 0, 0], [1, 2, 2]]) mask = selector.LabelMapperArray(a) tree = {'model': mask} helpers.assert_roundtrip_tree(tree, tmpdir, extensions=GWCSExtension()) def test_LabelMapperDict(tmpdir): dmapper = create_scalar_mapper() sel = selector.LabelMapperDict(('x', 'y'), dmapper, inputs_mapping=Mapping((0,), n_inputs=2), atol=1e-3) tree = {'model': sel} helpers.assert_roundtrip_tree(tree, tmpdir, extensions=GWCSExtension()) def test_LabelMapperRange(tmpdir): m = [] for i in np.arange(9) * .1: c0_0, c1_0, c0_1, c1_1 = np.ones((4,)) * i m.append(Polynomial2D(2, c0_0=c0_0, c1_0=c1_0, c0_1=c0_1, c1_1=c1_1)) keys = np.array([[4.88, 5.64], [5.75, 6.5], [6.67, 7.47], [7.7, 8.63], [8.83, 9.96], [10.19, 11.49], [11.77, 13.28], [13.33, 15.34], [15.56, 18.09]]) rmapper = {} for k, v in zip(keys, m): rmapper[tuple(k)] = v sel = selector.LabelMapperRange(('x', 'y'), rmapper, inputs_mapping=Mapping((0,), n_inputs=2)) tree = {'model': sel} helpers.assert_roundtrip_tree(tree, tmpdir, extensions=GWCSExtension()) gwcs-0.12.0/gwcs/tags/tests/test_transforms.py0000644000732200020070000000253213600426757023572 0ustar denchevaSTSCI\science00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst # -*- coding: utf-8 -*- import pytest import numpy as np from astropy.modeling.models import Identity from astropy import units as u from asdf.tests import helpers from ... import spectroscopy as sp from ... import geometry sell_glass = sp.SellmeierGlass(B_coef=[0.58339748, 0.46085267, 3.8915394], C_coef=[0.00252643, 0.010078333, 1200.556]) sell_zemax = sp.SellmeierZemax(65, 35, 0, 0, [0.58339748, 0.46085267, 3.8915394], [0.00252643, 0.010078333, 1200.556], [-2.66e-05, 0.0, 0.0]) snell = sp.Snell3D() todircos = geometry.ToDirectionCosines() fromdircos = geometry.FromDirectionCosines() transforms = [todircos, fromdircos, snell, sell_glass, sell_zemax, sell_zemax & todircos| snell & Identity(1) | fromdircos, sell_glass & todircos | snell & Identity(1) | fromdircos, sp.WavelengthFromGratingEquation(50000, -1), sp.AnglesFromGratingEquation3D(20000, 1), sp.WavelengthFromGratingEquation(15000*1 / u.m, -1), ] @pytest.mark.parametrize(('model'), transforms) def test_transforms(tmpdir, model): tree = {'model': model} helpers.assert_roundtrip_tree(tree, tmpdir) gwcs-0.12.0/gwcs/tags/tests/test_wcs.py0000644000732200020070000000750613600426757022176 0ustar denchevaSTSCI\science00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst # -*- coding: utf-8 -*- import pytest import warnings astropy = pytest.importorskip('astropy', minversion='3.0') from astropy.modeling import models from astropy import coordinates as coord from astropy import units as u from astropy import time from asdf import AsdfFile from asdf.tests import helpers from ... import coordinate_frames as cf from ... import wcs def test_create_wcs(tmpdir): m1 = models.Shift(12.4) & models.Shift(-2) m2 = models.Scale(2) & models.Scale(-2) icrs = cf.CelestialFrame(name='icrs', reference_frame=coord.ICRS()) det = cf.Frame2D(name='detector', axes_order=(0, 1)) gw1 = wcs.WCS(output_frame='icrs', input_frame='detector', forward_transform=m1) gw2 = wcs.WCS(output_frame='icrs', forward_transform=m1) gw3 = wcs.WCS(output_frame=icrs, input_frame=det, forward_transform=m1) tree = { 'gw1': gw1, 'gw2': gw2, 'gw3': gw3 } helpers.assert_roundtrip_tree(tree, tmpdir) def test_composite_frame(tmpdir): icrs = coord.ICRS() fk5 = coord.FK5() cel1 = cf.CelestialFrame(reference_frame=icrs) cel2 = cf.CelestialFrame(reference_frame=fk5) spec1 = cf.SpectralFrame(name='freq', unit=(u.Hz, ), axes_order=(2, )) spec2 = cf.SpectralFrame(name='wave', unit=(u.m, ), axes_order=(2, )) comp1 = cf.CompositeFrame([cel1, spec1]) comp2 = cf.CompositeFrame([cel2, spec2]) comp = cf.CompositeFrame([comp1, cf.SpectralFrame(axes_order=(3, ), unit=(u.m, ))]) tree = { 'comp1': comp1, 'comp2': comp2, 'comp': comp } helpers.assert_roundtrip_tree(tree, tmpdir) def create_test_frames(): """Creates an array of frames to be used for testing.""" # Suppress warnings from astropy that are caused by having 'dubious' dates # that are too far in the future. It's not a concern for the purposes of # unit tests. See issue #5809 on the astropy GitHub for discussion. from astropy._erfa import ErfaWarning warnings.simplefilter("ignore", ErfaWarning) frames = [ cf.CelestialFrame(reference_frame=coord.ICRS()), cf.CelestialFrame( reference_frame=coord.FK5(equinox=time.Time('2010-01-01'))), cf.CelestialFrame( reference_frame=coord.FK4( equinox=time.Time('2010-01-01'), obstime=time.Time('2015-01-01')) ), cf.CelestialFrame( reference_frame=coord.FK4NoETerms( equinox=time.Time('2010-01-01'), obstime=time.Time('2015-01-01')) ), cf.CelestialFrame( reference_frame=coord.Galactic()), cf.CelestialFrame( reference_frame=coord.Galactocentric( # A default galcen_coord is used since none is provided here galcen_distance=5.0 * u.m, z_sun=3 * u.pc, roll=3 * u.deg) ), cf.CelestialFrame( reference_frame=coord.GCRS( obstime=time.Time('2010-01-01'), obsgeoloc=[1, 3, 2000] * u.pc, obsgeovel=[2, 1, 8] * (u.m / u.s))), cf.CelestialFrame( reference_frame=coord.CIRS( obstime=time.Time('2010-01-01'))), cf.CelestialFrame( reference_frame=coord.ITRS( obstime=time.Time('2022-01-03'))), cf.CelestialFrame( reference_frame=coord.PrecessedGeocentric( obstime=time.Time('2010-01-01'), obsgeoloc=[1, 3, 2000] * u.pc, obsgeovel=[2, 1, 8] * (u.m / u.s))), cf.StokesFrame(), cf.TemporalFrame(time.Time("2011-01-01")) ] return frames def test_frames(tmpdir): tree = { 'frames': create_test_frames() } helpers.assert_roundtrip_tree(tree, tmpdir) gwcs-0.12.0/gwcs/tags/wcs.py0000644000732200020070000002062013600426757017765 0ustar denchevaSTSCI\science00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst # -*- coding: utf-8 -*- import astropy.time from asdf import yamlutil from ..gwcs_types import GWCSType from ..coordinate_frames import (Frame2D, CoordinateFrame, CelestialFrame, SpectralFrame, TemporalFrame, CompositeFrame, StokesFrame) from ..wcs import WCS _REQUIRES = ['astropy'] __all__ = ["WCSType", "CelestialFrameType", "CompositeFrameType", "FrameType", "SpectralFrameType", "StepType", "TemporalFrameType", "StokesFrameType"] class WCSType(GWCSType): name = "wcs" requires = _REQUIRES types = [WCS] version = '1.0.0' @classmethod def from_tree(cls, node, ctx): steps = [(x['frame'], x.get('transform')) for x in node['steps']] name = node['name'] return WCS(steps, name=name) @classmethod def to_tree(cls, gwcsobj, ctx): def get_frame(frame_name): frame = getattr(gwcsobj, frame_name) if frame is None: return frame_name return frame frames = gwcsobj.available_frames steps = [] for i in range(len(frames) - 1): frame_name = frames[i] frame = get_frame(frame_name) transform = gwcsobj.get_transform(frames[i], frames[i + 1]) steps.append(StepType({'frame': frame, 'transform': transform})) frame_name = frames[-1] frame = get_frame(frame_name) steps.append(StepType({'frame': frame})) return {'name': gwcsobj.name, 'steps': yamlutil.custom_tree_to_tagged_tree(steps, ctx)} @classmethod def assert_equal(cls, old, new): from asdf.tests import helpers assert old.name == new.name # nosec assert len(old.available_frames) == len(new.available_frames) # nosec for (old_frame, old_transform), (new_frame, new_transform) in zip( old.pipeline, new.pipeline): helpers.assert_tree_match(old_frame, new_frame) helpers.assert_tree_match(old_transform, new_transform) class StepType(dict, GWCSType): name = "step" requires = _REQUIRES version = '1.0.0' class FrameType(GWCSType): name = "frame" requires = _REQUIRES types = [CoordinateFrame] version = '1.0.0' @classmethod def _from_tree(cls, node, ctx): kwargs = {'name': node['name']} if 'axes_type' in node and 'naxes' in node: kwargs.update({ 'axes_type': node['axes_type'], 'naxes': node['naxes']}) if 'axes_names' in node: kwargs['axes_names'] = node['axes_names'] if 'reference_frame' in node: kwargs['reference_frame'] = yamlutil.tagged_tree_to_custom_tree( node['reference_frame'], ctx) if 'axes_order' in node: kwargs['axes_order'] = tuple(node['axes_order']) if 'unit' in node: kwargs['unit'] = tuple( yamlutil.tagged_tree_to_custom_tree(node['unit'], ctx)) if 'axis_physical_types' in node: kwargs['axis_physical_types'] = tuple(node['axis_physical_types']) return kwargs @classmethod def _to_tree(cls, frame, ctx): node = {} node['name'] = frame.name # We want to check that it is exactly this type and not a subclass if type(frame) is CoordinateFrame: node['axes_type'] = frame.axes_type node['naxes'] = frame.naxes if frame.axes_order is not None: node['axes_order'] = list(frame.axes_order) if frame.axes_names is not None: node['axes_names'] = list(frame.axes_names) if frame.reference_frame is not None: node['reference_frame'] = yamlutil.custom_tree_to_tagged_tree( frame.reference_frame, ctx) if frame.unit is not None: node['unit'] = yamlutil.custom_tree_to_tagged_tree( list(frame.unit), ctx) if frame.axis_physical_types is not None: node['axis_physical_types'] = list(frame.axis_physical_types) return node @classmethod def _assert_equal(cls, old, new): from asdf.tests import helpers assert old.name == new.name # nosec assert old.axes_order == new.axes_order # nosec assert old.axes_names == new.axes_names # nosec assert type(old.reference_frame) is type(new.reference_frame) # nosec assert old.unit == new.unit # nosec if old.reference_frame is not None: for name in old.reference_frame.get_frame_attr_names().keys(): helpers.assert_tree_match( getattr(old.reference_frame, name), getattr(new.reference_frame, name)) @classmethod def assert_equal(cls, old, new): cls._assert_equal(old, new) @classmethod def from_tree(cls, node, ctx): node = cls._from_tree(node, ctx) return CoordinateFrame(**node) @classmethod def to_tree(cls, frame, ctx): return cls._to_tree(frame, ctx) class Frame2DType(FrameType): name = "frame2d" types = [Frame2D] @classmethod def from_tree(cls, node, ctx): node = cls._from_tree(node, ctx) return Frame2D(**node) class CelestialFrameType(FrameType): name = "celestial_frame" types = [CelestialFrame] @classmethod def from_tree(cls, node, ctx): node = cls._from_tree(node, ctx) return CelestialFrame(**node) @classmethod def to_tree(cls, frame, ctx): return cls._to_tree(frame, ctx) @classmethod def assert_equal(cls, old, new): cls._assert_equal(old, new) assert old.reference_position == new.reference_position # nosec class SpectralFrameType(FrameType): name = "spectral_frame" types = [SpectralFrame] version = "1.0.0" @classmethod def from_tree(cls, node, ctx): node = cls._from_tree(node, ctx) if 'reference_position' in node: node['reference_position'] = node['reference_position'].upper() return SpectralFrame(**node) @classmethod def to_tree(cls, frame, ctx): node = cls._to_tree(frame, ctx) if frame.reference_position is not None: node['reference_position'] = frame.reference_position.lower() return node class CompositeFrameType(FrameType): name = "composite_frame" types = [CompositeFrame] @classmethod def from_tree(cls, node, ctx): if len(node) != 2: raise ValueError("CompositeFrame has extra properties") name = node['name'] frames = node['frames'] return CompositeFrame(frames, name) @classmethod def to_tree(cls, frame, ctx): return { 'name': frame.name, 'frames': yamlutil.custom_tree_to_tagged_tree(frame.frames, ctx) } @classmethod def assert_equal(cls, old, new): from asdf.tests import helpers assert old.name == new.name # nosec for old_frame, new_frame in zip(old.frames, new.frames): helpers.assert_tree_match(old_frame, new_frame) class TemporalFrameType(FrameType): name = "temporal_frame" requires = _REQUIRES types = [TemporalFrame] version = '1.0.0' @classmethod def from_tree(cls, node, ctx): node = cls._from_tree(node, ctx) return TemporalFrame(**node) @classmethod def to_tree(cls, frame, ctx): return cls._to_tree(frame, ctx) @classmethod def assert_equal(cls, old, new): assert old.name == new.name # nosec assert old.axes_order == new.axes_order # nosec assert old.axes_names == new.axes_names # nosec assert old.unit == new.unit # nosec assert old.reference_frame == new.reference_frame # nosec class StokesFrameType(FrameType): name = "stokes_frame" types = [StokesFrame] @classmethod def from_tree(cls, node, ctx): node = cls._from_tree(node, ctx) return StokesFrame(**node) @classmethod def _to_tree(cls, frame, ctx): node = {} node['name'] = frame.name if frame.axes_order: node['axes_order'] = list(frame.axes_order) return node @classmethod def assert_equal(cls, old, new): from asdf.tests import helpers assert old.name == new.name # nosec assert old.axes_order == new.axes_order # nosec gwcs-0.12.0/gwcs/tests/0000755000732200020070000000000013600427270017012 5ustar denchevaSTSCI\science00000000000000gwcs-0.12.0/gwcs/tests/__init__.py0000644000732200020070000000017113600426757021133 0ustar denchevaSTSCI\science00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This packages contains affiliated package tests. """ gwcs-0.12.0/gwcs/tests/conftest.py0000644000732200020070000002434613600426757021233 0ustar denchevaSTSCI\science00000000000000""" This file contains a set of pytest fixtures which are different gwcses for testing. """ import pytest import numpy as np import astropy.units as u from astropy import coordinates as coord from astropy.modeling import models from astropy.time import Time from .. import coordinate_frames as cf from .. import spectroscopy as sp from .. import wcs # frames detector_1d = cf.CoordinateFrame(name='detector', axes_order=(0,), naxes=1, axes_type="detector") detector_2d = cf.Frame2D(name='detector', axes_order=(0, 1)) icrs_sky_frame = cf.CelestialFrame(reference_frame=coord.ICRS(), axes_order=(0, 1)) freq_frame = cf.SpectralFrame(name='freq', unit=u.Hz, axes_order=(0, )) wave_frame = cf.SpectralFrame(name='wave', unit=u.m, axes_order=(2, ), axes_names=('lambda', )) # transforms model_2d_shift = models.Shift(1) & models.Shift(2) model_1d_scale = models.Scale(2) @pytest.fixture def gwcs_2d_spatial_shift(): """ A simple one step spatial WCS, in ICRS with a 1 and 2 px shift. """ pipe = [(detector_2d, model_2d_shift), (icrs_sky_frame, None)] return wcs.WCS(pipe) @pytest.fixture def gwcs_2d_spatial_reordered(): """ A simple one step spatial WCS, in ICRS with a 1 and 2 px shift. """ out_frame = cf.CelestialFrame(reference_frame=coord.ICRS(), axes_order=(1, 0)) return wcs.WCS(model_2d_shift | models.Mapping((1, 0)), input_frame=detector_2d, output_frame=out_frame) @pytest.fixture def gwcs_1d_freq(): return wcs.WCS([(detector_1d, model_1d_scale), (freq_frame, None)]) @pytest.fixture def gwcs_3d_spatial_wave(): comp1 = cf.CompositeFrame([icrs_sky_frame, wave_frame]) m = model_2d_shift & model_1d_scale detector_frame = cf.CoordinateFrame(name="detector", naxes=3, axes_order=(0, 1, 2), axes_type=("pixel", "pixel", "pixel"), axes_names=("x", "y", "z"), unit=(u.pix, u.pix, u.pix)) return wcs.WCS([(detector_frame, m), (comp1, None)]) @pytest.fixture def gwcs_2d_shift_scale(): m1 = models.Shift(1) & models.Shift(2) m2 = models.Scale(5) & models.Scale(10) m3 = m1 | m2 pipe = [(detector_2d, m3), (icrs_sky_frame, None)] return wcs.WCS(pipe) @pytest.fixture def gwcs_1d_freq_quantity(): detector_1d = cf.CoordinateFrame(name='detector', axes_order=(0,), naxes=1, unit=u.pix, axes_type="detector") return wcs.WCS([(detector_1d, models.Multiply(1 * u.Hz / u.pix)), (freq_frame, None)]) @pytest.fixture def gwcs_2d_shift_scale_quantity(): m4 = models.Shift(1 * u.pix) & models.Shift(2 * u.pix) m5 = models.Scale(5 * u.deg) m6 = models.Scale(10 * u.deg) m5.input_units_equivalencies = {'x': u.pixel_scale(1 * u.deg / u.pix)} m6.input_units_equivalencies = {'x': u.pixel_scale(1 * u.deg / u.pix)} m5.inverse = models.Scale(1. / 5 * u.pix) m6.inverse = models.Scale(1. / 10 * u.pix) m5.inverse.input_units_equivalencies = { 'x': u.pixel_scale(1 * u.pix / u.deg) } m6.inverse.input_units_equivalencies = { 'x': u.pixel_scale(1 * u.pix / u.deg) } m7 = m5 & m6 m8 = m4 | m7 pipe2 = [(detector_2d, m8), (icrs_sky_frame, None)] return wcs.WCS(pipe2) @pytest.fixture def gwcs_3d_identity_units(): """ A simple 1-1 gwcs that converts from pixels to arcseconds """ identity = (models.Multiply(1 * u.arcsec / u.pixel) & models.Multiply(1 * u.arcsec / u.pixel) & models.Multiply(1 * u.nm / u.pixel)) sky_frame = cf.CelestialFrame(axes_order=(0, 1), name='icrs', reference_frame=coord.ICRS(), axes_names=("longitude", "latitude")) wave_frame = cf.SpectralFrame(axes_order=(2, ), unit=u.nm, axes_names=("wavelength",)) frame = cf.CompositeFrame([sky_frame, wave_frame]) detector_frame = cf.CoordinateFrame(name="detector", naxes=3, axes_order=(0, 1, 2), axes_type=("pixel", "pixel", "pixel"), axes_names=("x", "y", "z"), unit=(u.pix, u.pix, u.pix)) return wcs.WCS(forward_transform=identity, output_frame=frame, input_frame=detector_frame) @pytest.fixture def gwcs_4d_identity_units(): """ A simple 1-1 gwcs that converts from pixels to arcseconds """ identity = (models.Multiply(1*u.arcsec/u.pixel) & models.Multiply(1*u.arcsec/u.pixel) & models.Multiply(1*u.nm/u.pixel) & models.Multiply(1*u.s/u.pixel)) sky_frame = cf.CelestialFrame(axes_order=(0, 1), name='icrs', reference_frame=coord.ICRS()) wave_frame = cf.SpectralFrame(axes_order=(2, ), unit=u.nm) time_frame = cf.TemporalFrame(axes_order=(3, ), unit=u.s, reference_frame=Time("2000-01-01T00:00:00")) frame = cf.CompositeFrame([sky_frame, wave_frame, time_frame]) detector_frame = cf.CoordinateFrame(name="detector", naxes=4, axes_order=(0, 1, 2, 3), axes_type=("pixel", "pixel", "pixel", "pixel"), axes_names=("x", "y", "z", "s"), unit=(u.pix, u.pix, u.pix, u.pix)) return wcs.WCS(forward_transform=identity, output_frame=frame, input_frame=detector_frame) @pytest.fixture def gwcs_simple_imaging_units(): shift_by_crpix = models.Shift(-2048*u.pix) & models.Shift(-1024*u.pix) matrix = np.array([[1.290551569736E-05, 5.9525007864732E-06], [5.0226382102765E-06 , -1.2644844123757E-05]]) rotation = models.AffineTransformation2D(matrix * u.deg, translation=[0, 0] * u.deg) rotation.input_units_equivalencies = {"x": u.pixel_scale(1*u.deg/u.pix), "y": u.pixel_scale(1*u.deg/u.pix)} rotation.inverse = models.AffineTransformation2D(np.linalg.inv(matrix) * u.pix, translation=[0, 0] * u.pix) rotation.inverse.input_units_equivalencies = {"x": u.pixel_scale(1*u.pix/u.deg), "y": u.pixel_scale(1*u.pix/u.deg)} tan = models.Pix2Sky_TAN() celestial_rotation = models.RotateNative2Celestial(5.63056810618*u.deg, -72.05457184279*u.deg, 180*u.deg) det2sky = shift_by_crpix | rotation | tan | celestial_rotation det2sky.name = "linear_transform" detector_frame = cf.Frame2D(name="detector", axes_names=("x", "y"), unit=(u.pix, u.pix)) sky_frame = cf.CelestialFrame(reference_frame=coord.ICRS(), name='icrs', unit=(u.deg, u.deg)) pipeline = [(detector_frame, det2sky), (sky_frame, None) ] return wcs.WCS(pipeline) @pytest.fixture def gwcs_stokes_lookup(): transform = models.Tabular1D([0, 1, 2, 3] * u.pix, [0, 1, 2, 3] * u.one, method="nearest", fill_value=np.nan, bounds_error=False) frame = cf.StokesFrame() detector_frame = cf.CoordinateFrame(name="detector", naxes=1, axes_order=(0,), axes_type=("pixel",), axes_names=("x",), unit=(u.pix,)) return wcs.WCS(forward_transform=transform, output_frame=frame, input_frame=detector_frame) @pytest.fixture def gwcs_3spectral_orders(): comp1 = cf.CompositeFrame([icrs_sky_frame, wave_frame]) detector_frame = cf.Frame2D(name="detector", axes_names=("x", "y"), unit=(u.pix, u.pix)) m = model_2d_shift & model_1d_scale return wcs.WCS([(detector_frame, m), (comp1, None)]) @pytest.fixture def gwcs_with_frames_strings(): transform = models.Shift(1) & models.Shift(1) & models.Polynomial2D(1) pipe = [('detector', transform), ('world', None) ] return wcs.WCS(pipe) @pytest.fixture def sellmeier_glass(): B_coef = [0.58339748, 0.46085267, 3.8915394] C_coef = [0.00252643, 0.010078333, 1200.556] return sp.SellmeierGlass(B_coef, C_coef) @pytest.fixture def sellmeier_zemax(): B_coef = [0.58339748, 0.46085267, 3.8915394] C_coef = [0.00252643, 0.010078333, 1200.556] D_coef = [-2.66e-05, 0.0, 0.0] E_coef = [0., 0., 0.] return sp.SellmeierZemax(65, 35, 0, 0, B_coef = B_coef, C_coef=C_coef, D_coef=D_coef, E_coef=E_coef) @pytest.fixture def gwcs_3d_galactic_spectral(): """ This fixture has the axes ordered as lat, spectral, lon. """ # lat,wav,lon crpix1, crpix2, crpix3 = 29, 39, 44 crval1, crval2, crval3 = 10, 20, 25 cdelt1, cdelt2, cdelt3 = -0.1, 0.5, 0.1 shift = models.Shift(-crpix3) & models.Shift(-crpix1) scale = models.Multiply(cdelt3) & models.Multiply(cdelt1) proj = models.Pix2Sky_CAR() skyrot = models.RotateNative2Celestial(crval3, 90 + crval1, 180) celestial = shift | scale | proj | skyrot wave_model = models.Shift(-crpix2) | models.Multiply(cdelt2) | models.Shift(crval2) transform = models.Mapping((2, 0, 1)) | celestial & wave_model | models.Mapping((1, 2, 0)) transform.bounding_box = ((5, 50), (-2, 45), (-1, 35)) sky_frame = cf.CelestialFrame(axes_order=(2, 0), reference_frame=coord.Galactic(), axes_names=("Longitude", "Latitude")) wave_frame = cf.SpectralFrame(axes_order=(1, ), unit=u.Hz, axes_names=("Frequency",)) frame = cf.CompositeFrame([sky_frame, wave_frame]) detector_frame = cf.CoordinateFrame(name="detector", naxes=3, axes_order=(0, 1, 2), axes_type=("pixel", "pixel", "pixel"), unit=(u.pix, u.pix, u.pix)) owcs = wcs.WCS(forward_transform=transform, output_frame=frame, input_frame=detector_frame) owcs.array_shape = (30, 20, 10) owcs.pixel_shape = (10, 20, 30) return owcs gwcs-0.12.0/gwcs/tests/coveragerc0000644000732200020070000000100113600426757021056 0ustar denchevaSTSCI\science00000000000000[run] source = gwcs omit = gwcs/conftest* gwcs/tests/* gwcs/tags/tests/* [report] exclude_lines = # Have to re-enable the standard pragma pragma: no cover # Don't complain about packages we have installed except ImportError # Don't complain if tests don't hit assertions raise AssertionError raise NotImplementedError # Don't complain about script hooks def main\(.*\): # Ignore branches that don't pertain to this version of Python pragma: py{ignore_python_version}gwcs-0.12.0/gwcs/tests/data/0000755000732200020070000000000013600427270017723 5ustar denchevaSTSCI\science00000000000000gwcs-0.12.0/gwcs/tests/data/acs.hdr0000644000732200020070000007760213600426757021215 0ustar denchevaSTSCI\science00000000000000SIMPLE = T / Fits standard BITPIX = 16 / Bits per pixel NAXIS = 0 / Number of axes EXTEND = T / File may contain extensions ORIGIN = 'NOAO-IRAF FITS Image Kernel July 2003' / FITS file originator IRAF-TLM= '2014-04-11T18:05:59' / Time of last modification NEXTEND = 12 / Number of standard extensions DATE = '2007-02-08T21:38:46' / date this file was written (yyyy-mm-dd) FILENAME= 'j94f05bgq_flt.fits' / name of file FILETYPE= 'SCI ' / type of data found in data file TELESCOP= 'HST' / telescope used to acquire data INSTRUME= 'ACS ' / identifier for instrument used to acquire data EQUINOX = 2000.0 / equinox of celestial coord. system / DATA DESCRIPTION KEYWORDS ROOTNAME= 'j94f05bgq ' / rootname of the observation set IMAGETYP= 'EXT ' / type of exposure identifier PRIMESI = 'ACS ' / instrument designated as prime / TARGET INFORMATION TARGNAME= 'NGC104 ' / proposer's target name RA_TARG = 5.655000000000E+00 / right ascension of the target (deg) (J2000) DEC_TARG= -7.207055555556E+01 / declination of the target (deg) (J2000) / PROPOSAL INFORMATION PROPOSID= 10368 / PEP proposal identifier LINENUM = '05.004 ' / proposal logsheet line number PR_INV_L= 'Riess ' / last name of principal investigator PR_INV_F= 'Adam ' / first name of principal investigator PR_INV_M= ' ' / middle name / initial of principal investigat / EXPOSURE INFORMATION SUNANGLE= 67.819656 / angle between sun and V1 axis MOONANGL= 57.400970 / angle between moon and V1 axis SUN_ALT = -5.746915 / altitude of the sun above Earth's limb FGSLOCK = 'FINE ' / commanded FGS lock (FINE,COARSE,GYROS,UNKNOWN) GYROMODE= '3' / observation scheduled with only two gyros (Y/N) REFFRAME= 'GSC1 ' / guide star catalog version DATE-OBS= '2005-03-07' / UT date of start of observation (yyyy-mm-dd) TIME-OBS= '06:51:26' / UT time of start of observation (hh:mm:ss) EXPSTART= 5.343628571938E+04 / exposure start time (Modified Julian Date) EXPEND = 5.343629036114E+04 / exposure end time (Modified Julian Date) EXPTIME = 400.000000 / exposure duration (seconds)--calculated EXPFLAG = 'NORMAL ' / Exposure interruption indicator QUALCOM1= ' ' QUALCOM2= ' ' QUALCOM3= ' ' QUALITY = ' ' / POINTING INFORMATION PA_V3 = 337.125305 / position angle of V3-axis of HST (deg) / TARGET OFFSETS (POSTARGS) POSTARG1= 0.000000 / POSTARG in axis 1 direction POSTARG2= 0.000000 / POSTARG in axis 2 direction / DIAGNOSTIC KEYWORDS OPUS_VER= 'OPUS 2006_6 ' / OPUS software system version number CAL_VER = '4.6.1 (13-Mar-2006)' / CALACS code version PROCTIME= 5.413989813657E+04 / Pipeline processing time (MJD) / SCIENCE INSTRUMENT CONFIGURATION OBSTYPE = 'IMAGING ' / observation type - imaging or spectroscopic OBSMODE = 'ACCUM ' / operating mode CTEIMAGE= 'NONE' / type of Charge Transfer Image, if applicable SCLAMP = 'NONE ' / lamp status, NONE or name of lamp which is on NRPTEXP = 1 / number of repeat exposures in set: default 1 SUBARRAY= F / data from a subarray (T) or full frame (F) DETECTOR= 'WFC' / detector in use: WFC, HRC, or SBC FILTER1 = 'F606W ' / element selected from filter wheel 1 FILTER2 = 'CLEAR2L ' / element selected from filter wheel 2 FWOFFSET= 0 / computed filter wheel offset FWERROR = F / filter wheel position error flag LRFWAVE = 0.000000 / proposed linear ramp filter wavelength APERTURE= 'WFC ' / aperture name PROPAPER= 'WFC ' / proposed aperture name DIRIMAGE= 'NONE ' / direct image for grism or prism exposure CTEDIR = 'NONE ' / CTE measurement direction: serial or parallel CRSPLIT = 1 / number of cosmic ray split exposures / CALIBRATION SWITCHES: PERFORM, OMIT, COMPLETE STATFLAG= F / Calculate statistics? WRTERR = T / write out error array extension DQICORR = 'COMPLETE' / data quality initialization ATODCORR= 'OMIT ' / correct for A to D conversion errors BLEVCORR= 'COMPLETE' / subtract bias level computed from overscan img BIASCORR= 'COMPLETE' / Subtract bias image FLSHCORR= 'OMIT ' / post flash correction CRCORR = 'OMIT ' / combine observations to reject cosmic rays EXPSCORR= 'COMPLETE' / process individual observations after cr-reject SHADCORR= 'OMIT ' / apply shutter shading correction DARKCORR= 'COMPLETE' / Subtract dark image FLATCORR= 'COMPLETE' / flat field data PHOTCORR= 'COMPLETE' / populate photometric header keywords RPTCORR = 'OMIT ' / add individual repeat observations DRIZCORR= 'COMPLETE' / drizzle processing / CALIBRATION REFERENCE FILES BPIXTAB = 'jref$q860440tj_bpx.fits' / bad pixel table CCDTAB = 'jref$o151506fj_ccd.fits' / CCD calibration parameters ATODTAB = 'jref$kcb1734hj_a2d.fits' / analog to digital correction file OSCNTAB = 'jref$lch1459bj_osc.fits' / CCD overscan table BIASFILE= 'jref$p3v2228mj_bia.fits' / bias image file name FLSHFILE= 'jref$nad14594j_fls.fits' / post flash correction file name CRREJTAB= 'jref$n4e12511j_crr.fits' / cosmic ray rejection parameters SHADFILE= 'jref$kcb17349j_shd.fits' / shutter shading correction file DARKFILE= 'jref$p3v2228qj_drk.fits' / dark image file name PFLTFILE= 'jref$nar1136nj_pfl.fits' / pixel to pixel flat field file name DFLTFILE= 'N/A ' / delta flat field file name LFLTFILE= 'N/A ' / low order flat PHOTTAB = 'N/A ' / Photometric throughput table GRAPHTAB= 'mtab$r1m18595m_tmg.fits' / the HST graph table COMPTAB = 'mtab$r1j2146sm_tmc.fits' / the HST components table IDCTAB = 'postsm4_idc.fits' / image distortion correction table DGEOFILE= 'jref$qbu16424j_dxy.fits' / Distortion correction image MDRIZTAB= 'jref$p3p16511j_mdz.fits' / MultiDrizzle parameter table CFLTFILE= 'N/A ' / Coronagraphic spot image SPOTTAB = 'N/A ' / Coronagraphic spot offset table / COSMIC RAY REJECTION ALGORITHM PARAMETERS MEANEXP = 0.000000 / reference exposure time for parameters SCALENSE= 0.000000 / multiplicative scale factor applied to noise INITGUES= ' ' / initial guess method (MIN or MED) SKYSUB = ' ' / sky value subtracted (MODE or NONE) SKYSUM = 0.0 / sky level from the sum of all constituent image CRSIGMAS= ' ' / statistical rejection criteria CRRADIUS= 0.000000 / rejection propagation radius (pixels) CRTHRESH= 0.000000 / rejection propagation threshold BADINPDQ= 0 / data quality flag bits to reject REJ_RATE= 0.0 / rate at which pixels are affected by cosmic ray CRMASK = F / flag CR-rejected pixels in input files (T/F) / OTFR KEYWORDS T_SGSTAR= ' ' / OMS calculated guide star control / PATTERN KEYWORDS PATTERN1= 'NONE ' / primary pattern type P1_SHAPE= ' ' / primary pattern shape P1_PURPS= ' ' / primary pattern purpose P1_NPTS = 0 / number of points in primary pattern P1_PSPAC= 0.000000 / point spacing for primary pattern (arc-sec) P1_LSPAC= 0.000000 / line spacing for primary pattern (arc-sec) P1_ANGLE= 0.000000 / angle between sides of parallelogram patt (deg) P1_FRAME= ' ' / coordinate frame of primary pattern P1_ORINT= 0.000000 / orientation of pattern to coordinate frame (deg P1_CENTR= ' ' / center pattern relative to pointing (yes/no) PATTSTEP= 0 / position number of this point in the pattern / POST FLASH PARAMETERS FLASHDUR= 1.0 / Exposure time in seconds: 0.1 to 409.5 FLASHCUR= 'MED ' / Post flash current: OFF, LOW, MED, HIGH FLASHSTA= 'SUCCESSFUL ' / Status: SUCCESSFUL, ABORTED, NOT PERFORMED SHUTRPOS= 'B ' / Shutter position: A or B / ENGINEERING PARAMETERS CCDAMP = 'ABCD' / CCD Amplifier Readout Configuration CCDGAIN = 1 / commanded gain of CCD CCDOFSTA= 3 / commanded CCD bias offset for amplifier A CCDOFSTB= 3 / commanded CCD bias offset for amplifier B CCDOFSTC= 3 / commanded CCD bias offset for amplifier C CCDOFSTD= 3 / commanded CCD bias offset for amplifier D / CALIBRATED ENGINEERING PARAMETERS ATODGNA = 9.9989998E-01 / calibrated gain for amplifier A ATODGNB = 9.7210002E-01 / calibrated gain for amplifier B ATODGNC = 1.0107000E+00 / calibrated gain for amplifier C ATODGND = 1.0180000E+00 / calibrated gain for amplifier D READNSEA= 4.9699998E+00 / calibrated read noise for amplifier A READNSEB= 4.8499999E+00 / calibrated read noise for amplifier B READNSEC= 5.2399998E+00 / calibrated read noise for amplifier C READNSED= 4.8499999E+00 / calibrated read noise for amplifier D BIASLEVA= 2.4281760E+03 / bias level for amplifier A BIASLEVB= 2.5189324E+03 / bias level for amplifier B BIASLEVC= 2.4417756E+03 / bias level for amplifier C BIASLEVD= 2.4699448E+03 / bias level for amplifier D / ASSOCIATION KEYWORDS ASN_ID = 'NONE ' / unique identifier assigned to association ASN_TAB = 'NONE ' / name of the association table ASN_MTYP= ' ' / Role of the Member in the Association UPWCSVER= '1.1.4.dev31014' / Version of STWCS used to updated the WCS PYWCSVER= '1.12.1.dev4001' / Version of PYWCS used to updated the WCS HISTORY CCD parameters table: HISTORY reference table jref$o151506fj_ccd.fits HISTORY inflight HISTORY June 2002 HISTORY Uncertainty array initialized. HISTORY DQICORR complete ... HISTORY values checked for saturation HISTORY DQ array initialized ... HISTORY reference table jref$q860440tj_bpx.fits HISTORY BLEVCORR complete; bias level from overscan was subtracted. HISTORY BLEVCORR does not include correction for drift along lines. HISTORY Overscan region table: HISTORY reference table jref$lch1459bj_osc.fits HISTORY BIASCORR complete ... HISTORY reference image jref$p3v2228mj_bia.fits HISTORY INFLIGHT 05/03/2005 23/03/2005 HISTORY Superbias by Ray Lucas from proposal 10367 or 10370 HISTORY CCD parameters table: HISTORY reference table jref$o151506fj_ccd.fits HISTORY inflight HISTORY June 2002 HISTORY DARKCORR complete ... HISTORY reference image jref$p3v2228qj_drk.fits HISTORY INFLIGHT 05/03/2005 23/03/2005 HISTORY Superdark by Ray Lucas from proposal 10367 or 10370 HISTORY FLATCORR complete ... HISTORY reference image jref$nar1136nj_pfl.fits HISTORY InFlight 18/04/2002 - 09/05/2002 HISTORY F606W step +1 flat w/ mote shifted to -1 step HISTORY PHOTCORR complete ... HISTORY reference table mtab$r1m18595m_tmg.fits HISTORY reference table mtab$r1j2146sm_tmc.fits HISTORY EXPSCORR complete ... TDDCORR = 'PERFORM ' WFCTDD = 'T ' DISTNAME= 'j94f05bgq_postsm4-v971826kj-x5u17177j' SIPNAME = 'j94f05bgq_postsm4' ORIGIN = 'NOAO-IRAF FITS Image Kernel July 2003' / FITS file originator EXTNAME = 'SCI ' / Extension name EXTVER = 1 / Extension version DATE = '2007-02-08T21:38:47' / Date FITS file was generated IRAF-TLM= '13:38:23 (20/08/2008)' / Time of last modification INHERIT = T / inherit the primary header EXPNAME = 'j94f05bgq ' / exposure identifier BUNIT = 'ELECTRONS' / brightness units CCDCHIP = 2 / CCD chip (1 or 2) WCSAXES = 2 / number of World Coordinate System axes CRPIX1 = 2048.0 / x-coordinate of reference pixel CRPIX2 = 1024.0 / y-coordinate of reference pixel CRVAL1 = 5.63056810618 / first axis value at reference pixel CRVAL2 = -72.0545718428 / second axis value at reference pixel CTYPE1 = 'RA---TAN-SIP' / the coordinate type for the first axis CTYPE2 = 'DEC--TAN-SIP' / the coordinate type for the second axis CD1_1 = 1.29058667557984E-05 / partial of first axis coordinate w.r.t. x CD1_2 = 5.95320245884555E-06 / partial of first axis coordinate w.r.t. y CD2_1 = 5.02215195623825E-06 / partial of second axis coordinate w.r.t. x CD2_2 = -1.2645010396976E-05 / partial of second axis coordinate w.r.t. y LTV1 = 0.0000000E+00 / offset in X to subsection start LTV2 = 0.0000000E+00 / offset in Y to subsection start LTM1_1 = 1.0 / reciprocal of sampling rate in X LTM2_2 = 1.0 / reciprocal of sampling rate in Y ORIENTAT= 154.7891975615789 / position angle of image y axis (deg. e of n) RA_APER = 5.655000000000E+00 / RA of aperture reference position DEC_APER= -7.207055555556E+01 / Declination of aperture reference position PA_APER = 154.533 / Position Angle of reference aperture center (de VAFACTOR= 1.000018683511E+00 / velocity aberration plate scale factor CENTERA1= 2073 / subarray axis1 center pt in unbinned dect. pix CENTERA2= 1035 / subarray axis2 center pt in unbinned dect. pix SIZAXIS1= 4096 / subarray axis1 size in unbinned detector pixels SIZAXIS2= 2048 / subarray axis2 size in unbinned detector pixels BINAXIS1= 1 / axis1 data bin size in unbinned detector pixels BINAXIS2= 1 / axis2 data bin size in unbinned detector pixels PHOTMODE= 'ACS WFC1 F606W' / observation con PHOTFLAM= 7.9064521E-20 / inverse sensitivity, ergs/cm2/Ang/electron PHOTZPT = -2.1100000E+01 / ST magnitude zero point PHOTPLAM= 5.9176797E+03 / Pivot wavelength (Angstroms) PHOTBW = 6.7231146E+02 / RMS bandwidth of filter plus detector NCOMBINE= 1 / number of image sets combined during CR rejecti FILLCNT = 0 / number of segments containing fill ERRCNT = 0 / number of segments containing errors PODPSFF = F / podps fill present (T/F) STDCFFF = F / ST DDF fill present (T/F) STDCFFP = 'x5569 ' / ST DDF fill pattern (hex) WFCMPRSD= F / was WFC data compressed? (T/F) CBLKSIZ = 0 / size of compression block in 2-byte words LOSTPIX = 0 / #pixels lost due to buffer overflow COMPTYP = 'None ' / compression type performed (Partial/Full/None) NGOODPIX= 7822781 / number of good pixels SDQFLAGS= 31743 / serious data quality flags GOODMIN = -2.5959351E+02 / minimum value of good pixels GOODMAX = 6.5220551E+04 / maximum value of good pixels GOODMEAN= 2.0491536E+02 / mean value of good pixels SOFTERRS= 0 / number of soft error pixels (DQF=1) SNRMIN = -8.0327058E-01 / minimum signal to noise of good pixels SNRMAX = 2.1379723E+02 / maximum signal to noise of good pixels SNRMEAN = 1.0889255E+01 / mean value of signal to noise of good pixels MEANDARK= 1.5474443E+00 / average of the dark values subtracted MEANBLEV= 2.4558604E+03 / average of all bias levels subtracted MEANFLSH= 0.000000 / Mean number of counts in post flash exposure OCRVAL1 = 5.63056810618 / first axis value at reference pixel OCRVAL2 = -72.05457184279 / second axis value at reference pixel OCRPIX2 = 1024.0 / y-coordinate of reference pixel OCRPIX1 = 2048.0 / x-coordinate of reference pixel ONAXIS2 = 2048 / Axis length ONAXIS1 = 4096 / Axis length OCD2_2 = -1.26445E-05 / partial of second axis coordinate w.r.t. y OCD2_1 = 5.02243E-06 / partial of second axis coordinate w.r.t. x OORIENTA= 154.7886863186197 / position angle of image y axis (deg. e of n) OCTYPE1 = 'RA---TAN' / the coordinate type for the first axis OCD1_1 = 1.29046E-05 / partial of first axis coordinate w.r.t. x OCD1_2 = 5.9531E-06 / partial of first axis coordinate w.r.t. y OCTYPE2 = 'DEC--TAN' / the coordinate type for the second axis WCSCDATE= '21:39:44 (08/02/2007)' / Time WCS keywords were copied. A_0_2 = 2.16615952976212E-06 B_0_2 = -7.2168814507744E-06 A_1_1 = -5.1974576466834E-06 B_1_1 = 6.18443235774478E-06 A_2_0 = 8.55127758255650E-06 B_2_0 = -1.746491877058669E-06 A_0_3 = 1.08193519820265E-11 B_0_3 = -4.175472049274932E-10 A_1_2 = -5.234870743692412E-10 B_1_2 = -6.169265268681388E-11 A_2_1 = -3.9771547747287E-11 B_2_1 = -5.0857161673862E-10 A_3_0 = -4.7304448292227E-10 B_3_0 = 8.56763542781631E-11 A_0_4 = 1.49356171166049E-14 B_0_4 = -9.9570490655478E-15 A_1_3 = -2.4569975537746E-14 B_1_3 = 1.21743011568848E-14 A_2_2 = 3.46791267104378E-14 B_2_2 = -3.66143259286574E-14 A_3_1 = 1.97102297166030E-15 B_3_1 = -3.779506805487476E-15 A_4_0 = 2.37430106240231E-14 B_4_0 = -1.7687653826004E-14 A_ORDER = 4 B_ORDER = 4 CPERR1 = 0.08311891555786133 / Maximum error of NPOL correction for axis 1 CPERR2 = 0.0758458599448204 / Maximum error of NPOL correction for axis 2 TDDALPHA= 0.03676157754622637 TDDBETA = -0.00958719251540879 IDCSCALE= 0.05 IDCV2REF= 256.6222229003906 IDCV3REF= 302.2264099121094 IDCTHETA= 0.0 OCX10 = 0.001959713482071437 OCX11 = 0.04983122487595928 OCY10 = 0.05027393143048926 OCY11 = 0.00148847536166365 SORIENTA= 154.7925383197021 / position angle of image y axis (deg. e of n) SCRVAL1 = 5.63056810618 / first axis value at reference pixel SNAXIS2 = 2048 / Axis length SNAXIS1 = 4096 / Axis length SCRVAL2 = -72.05457184279 / second axis value at reference pixel SCTYPE1 = 'RA---TAN-SIP' / the coordinate type for the first axis SCTYPE2 = 'DEC--TAN-SIP' / the coordinate type for the second axis SCD2_2 = -1.264489181627715E-05 / partial of second axis coordinate w.r.t. y SCD2_1 = 5.022886862247075E-06 / partial of second axis coordinate w.r.t. x SCD1_2 = 5.952245949610081E-06 / partial of first axis coordinate w.r.t. y SCRPIX2 = 1024.0 / y-coordinate of reference pixel SCRPIX1 = 2048.0 / x-coordinate of reference pixel SCD1_1 = 1.290545120875315E-05 / partial of first axis coordinate w.r.t. x IDCXREF = 2048.0 IDCYREF = 1024.0 WCSNAMEO= 'OPUS ' WCSAXESO= 2 CRPIX1O = 2048 CRPIX2O = 1024 CDELT1O = 1 CDELT2O = 1 CUNIT1O = 'deg ' CUNIT2O = 'deg ' CTYPE1O = 'RA---TAN-SIP' CTYPE2O = 'DEC--TAN-SIP' CRVAL1O = 5.63056810618 CRVAL2O = -72.0545718428 LONPOLEO= 180 LATPOLEO= -72.0545718428 RESTFRQO= 0 RESTWAVO= 0 CD1_1O = 1.29056256334E-05 CD1_2O = 5.9530912342E-06 CD2_1O = 5.02205812656E-06 CD2_2O = -1.26447741482E-05 IDCTAB = 'postsm4_idc.fits' WCSNAME = 'IDC_postsm4' NPOLEXT = 'jref$v971826kj_npl.fits' / WFC CCD CHIP IDENTIFICATION / World Coordinate System and Related Parameters / READOUT DEFINITION PARAMETERS / PHOTOMETRY KEYWORDS / REPEATED EXPOSURES INFO / DATA PACKET INFORMATION / ON-BOARD COMPRESSION INFORMATION / IMAGE STATISTICS AND DATA QUALITY FLAGS HISTORY The following throughput tables were used: crotacomp$hst_ota_007_syn.fit HISTORY s, cracscomp$acs_wfc_im123_004_syn.fits, cracscomp$acs_f606w_005_syn.fit HISTORY s, cracscomp$acs_wfc_ebe_win12f_005_syn.fits, cracscomp$acs_wfc_ccd1_017 HISTORY _syn.fits gwcs-0.12.0/gwcs/tests/data/acs_wfc.hdr0000644000732200020070000010260513600426757022044 0ustar denchevaSTSCI\science00000000000000SIMPLE = T / Fits standard BITPIX = 16 / Bits per pixel NAXIS = 0 / Number of axes EXTEND = T / File may contain extensions ORIGIN = 'NOAO-IRAF FITS Image Kernel July 2003' / FITS file originator IRAF-TLM= '2014-04-11T18:05:59' / Time of last modification NEXTEND = 12 / Number of standard extensions DATE = '2007-02-08T21:38:46' / date this file was written (yyyy-mm-dd) FILENAME= 'j94f05bgq_flt.fits' / name of file FILETYPE= 'SCI ' / type of data found in data file TELESCOP= 'HST' / telescope used to acquire data INSTRUME= 'ACS ' / identifier for instrument used to acquire data EQUINOX = 2000.0 / equinox of celestial coord. system / DATA DESCRIPTION KEYWORDS ROOTNAME= 'j94f05bgq ' / rootname of the observation set IMAGETYP= 'EXT ' / type of exposure identifier PRIMESI = 'ACS ' / instrument designated as prime / TARGET INFORMATION TARGNAME= 'NGC104 ' / proposer's target name RA_TARG = 5.655000000000E+00 / right ascension of the target (deg) (J2000) DEC_TARG= -7.207055555556E+01 / declination of the target (deg) (J2000) / PROPOSAL INFORMATION PROPOSID= 10368 / PEP proposal identifier LINENUM = '05.004 ' / proposal logsheet line number PR_INV_L= 'Riess ' / last name of principal investigator PR_INV_F= 'Adam ' / first name of principal investigator PR_INV_M= ' ' / middle name / initial of principal investigat / EXPOSURE INFORMATION SUNANGLE= 67.819656 / angle between sun and V1 axis MOONANGL= 57.400970 / angle between moon and V1 axis SUN_ALT = -5.746915 / altitude of the sun above Earth's limb FGSLOCK = 'FINE ' / commanded FGS lock (FINE,COARSE,GYROS,UNKNOWN) GYROMODE= '3' / observation scheduled with only two gyros (Y/N) REFFRAME= 'GSC1 ' / guide star catalog version DATE-OBS= '2005-03-07' / UT date of start of observation (yyyy-mm-dd) TIME-OBS= '06:51:26' / UT time of start of observation (hh:mm:ss) EXPSTART= 5.343628571938E+04 / exposure start time (Modified Julian Date) EXPEND = 5.343629036114E+04 / exposure end time (Modified Julian Date) EXPTIME = 400.000000 / exposure duration (seconds)--calculated EXPFLAG = 'NORMAL ' / Exposure interruption indicator QUALCOM1= ' ' QUALCOM2= ' ' QUALCOM3= ' ' QUALITY = ' ' / POINTING INFORMATION PA_V3 = 337.125305 / position angle of V3-axis of HST (deg) / TARGET OFFSETS (POSTARGS) POSTARG1= 0.000000 / POSTARG in axis 1 direction POSTARG2= 0.000000 / POSTARG in axis 2 direction / DIAGNOSTIC KEYWORDS OPUS_VER= 'OPUS 2006_6 ' / OPUS software system version number CAL_VER = '4.6.1 (13-Mar-2006)' / CALACS code version PROCTIME= 5.413989813657E+04 / Pipeline processing time (MJD) / SCIENCE INSTRUMENT CONFIGURATION OBSTYPE = 'IMAGING ' / observation type - imaging or spectroscopic OBSMODE = 'ACCUM ' / operating mode CTEIMAGE= 'NONE' / type of Charge Transfer Image, if applicable SCLAMP = 'NONE ' / lamp status, NONE or name of lamp which is on NRPTEXP = 1 / number of repeat exposures in set: default 1 SUBARRAY= F / data from a subarray (T) or full frame (F) DETECTOR= 'WFC' / detector in use: WFC, HRC, or SBC FILTER1 = 'F606W ' / element selected from filter wheel 1 FILTER2 = 'CLEAR2L ' / element selected from filter wheel 2 FWOFFSET= 0 / computed filter wheel offset FWERROR = F / filter wheel position error flag LRFWAVE = 0.000000 / proposed linear ramp filter wavelength APERTURE= 'WFC ' / aperture name PROPAPER= 'WFC ' / proposed aperture name DIRIMAGE= 'NONE ' / direct image for grism or prism exposure CTEDIR = 'NONE ' / CTE measurement direction: serial or parallel CRSPLIT = 1 / number of cosmic ray split exposures / CALIBRATION SWITCHES: PERFORM, OMIT, COMPLETE STATFLAG= F / Calculate statistics? WRTERR = T / write out error array extension DQICORR = 'COMPLETE' / data quality initialization ATODCORR= 'OMIT ' / correct for A to D conversion errors BLEVCORR= 'COMPLETE' / subtract bias level computed from overscan img BIASCORR= 'COMPLETE' / Subtract bias image FLSHCORR= 'OMIT ' / post flash correction CRCORR = 'OMIT ' / combine observations to reject cosmic rays EXPSCORR= 'COMPLETE' / process individual observations after cr-reject SHADCORR= 'OMIT ' / apply shutter shading correction DARKCORR= 'COMPLETE' / Subtract dark image FLATCORR= 'COMPLETE' / flat field data PHOTCORR= 'COMPLETE' / populate photometric header keywords RPTCORR = 'OMIT ' / add individual repeat observations DRIZCORR= 'COMPLETE' / drizzle processing / CALIBRATION REFERENCE FILES BPIXTAB = 'jref$q860440tj_bpx.fits' / bad pixel table CCDTAB = 'jref$o151506fj_ccd.fits' / CCD calibration parameters ATODTAB = 'jref$kcb1734hj_a2d.fits' / analog to digital correction file OSCNTAB = 'jref$lch1459bj_osc.fits' / CCD overscan table BIASFILE= 'jref$p3v2228mj_bia.fits' / bias image file name FLSHFILE= 'jref$nad14594j_fls.fits' / post flash correction file name CRREJTAB= 'jref$n4e12511j_crr.fits' / cosmic ray rejection parameters SHADFILE= 'jref$kcb17349j_shd.fits' / shutter shading correction file DARKFILE= 'jref$p3v2228qj_drk.fits' / dark image file name PFLTFILE= 'jref$nar1136nj_pfl.fits' / pixel to pixel flat field file name DFLTFILE= 'N/A ' / delta flat field file name LFLTFILE= 'N/A ' / low order flat PHOTTAB = 'N/A ' / Photometric throughput table GRAPHTAB= 'mtab$r1m18595m_tmg.fits' / the HST graph table COMPTAB = 'mtab$r1j2146sm_tmc.fits' / the HST components table IDCTAB = 'postsm4_idc.fits' / image distortion correction table DGEOFILE= 'jref$qbu16424j_dxy.fits' / Distortion correction image MDRIZTAB= 'jref$p3p16511j_mdz.fits' / MultiDrizzle parameter table CFLTFILE= 'N/A ' / Coronagraphic spot image SPOTTAB = 'N/A ' / Coronagraphic spot offset table / COSMIC RAY REJECTION ALGORITHM PARAMETERS MEANEXP = 0.000000 / reference exposure time for parameters SCALENSE= 0.000000 / multiplicative scale factor applied to noise INITGUES= ' ' / initial guess method (MIN or MED) SKYSUB = ' ' / sky value subtracted (MODE or NONE) SKYSUM = 0.0 / sky level from the sum of all constituent image CRSIGMAS= ' ' / statistical rejection criteria CRRADIUS= 0.000000 / rejection propagation radius (pixels) CRTHRESH= 0.000000 / rejection propagation threshold BADINPDQ= 0 / data quality flag bits to reject REJ_RATE= 0.0 / rate at which pixels are affected by cosmic ray CRMASK = F / flag CR-rejected pixels in input files (T/F) / OTFR KEYWORDS T_SGSTAR= ' ' / OMS calculated guide star control / PATTERN KEYWORDS PATTERN1= 'NONE ' / primary pattern type P1_SHAPE= ' ' / primary pattern shape P1_PURPS= ' ' / primary pattern purpose P1_NPTS = 0 / number of points in primary pattern P1_PSPAC= 0.000000 / point spacing for primary pattern (arc-sec) P1_LSPAC= 0.000000 / line spacing for primary pattern (arc-sec) P1_ANGLE= 0.000000 / angle between sides of parallelogram patt (deg) P1_FRAME= ' ' / coordinate frame of primary pattern P1_ORINT= 0.000000 / orientation of pattern to coordinate frame (deg P1_CENTR= ' ' / center pattern relative to pointing (yes/no) PATTSTEP= 0 / position number of this point in the pattern / POST FLASH PARAMETERS FLASHDUR= 1.0 / Exposure time in seconds: 0.1 to 409.5 FLASHCUR= 'MED ' / Post flash current: OFF, LOW, MED, HIGH FLASHSTA= 'SUCCESSFUL ' / Status: SUCCESSFUL, ABORTED, NOT PERFORMED SHUTRPOS= 'B ' / Shutter position: A or B / ENGINEERING PARAMETERS CCDAMP = 'ABCD' / CCD Amplifier Readout Configuration CCDGAIN = 1 / commanded gain of CCD CCDOFSTA= 3 / commanded CCD bias offset for amplifier A CCDOFSTB= 3 / commanded CCD bias offset for amplifier B CCDOFSTC= 3 / commanded CCD bias offset for amplifier C CCDOFSTD= 3 / commanded CCD bias offset for amplifier D / CALIBRATED ENGINEERING PARAMETERS ATODGNA = 9.9989998E-01 / calibrated gain for amplifier A ATODGNB = 9.7210002E-01 / calibrated gain for amplifier B ATODGNC = 1.0107000E+00 / calibrated gain for amplifier C ATODGND = 1.0180000E+00 / calibrated gain for amplifier D READNSEA= 4.9699998E+00 / calibrated read noise for amplifier A READNSEB= 4.8499999E+00 / calibrated read noise for amplifier B READNSEC= 5.2399998E+00 / calibrated read noise for amplifier C READNSED= 4.8499999E+00 / calibrated read noise for amplifier D BIASLEVA= 2.4281760E+03 / bias level for amplifier A BIASLEVB= 2.5189324E+03 / bias level for amplifier B BIASLEVC= 2.4417756E+03 / bias level for amplifier C BIASLEVD= 2.4699448E+03 / bias level for amplifier D / ASSOCIATION KEYWORDS ASN_ID = 'NONE ' / unique identifier assigned to association ASN_TAB = 'NONE ' / name of the association table ASN_MTYP= ' ' / Role of the Member in the Association UPWCSVER= '1.1.4.dev31014' / Version of STWCS used to updated the WCS PYWCSVER= '1.12.1.dev4001' / Version of PYWCS used to updated the WCS HISTORY CCD parameters table: HISTORY reference table jref$o151506fj_ccd.fits HISTORY inflight HISTORY June 2002 HISTORY Uncertainty array initialized. HISTORY DQICORR complete ... HISTORY values checked for saturation HISTORY DQ array initialized ... HISTORY reference table jref$q860440tj_bpx.fits HISTORY BLEVCORR complete; bias level from overscan was subtracted. HISTORY BLEVCORR does not include correction for drift along lines. HISTORY Overscan region table: HISTORY reference table jref$lch1459bj_osc.fits HISTORY BIASCORR complete ... HISTORY reference image jref$p3v2228mj_bia.fits HISTORY INFLIGHT 05/03/2005 23/03/2005 HISTORY Superbias by Ray Lucas from proposal 10367 or 10370 HISTORY CCD parameters table: HISTORY reference table jref$o151506fj_ccd.fits HISTORY inflight HISTORY June 2002 HISTORY DARKCORR complete ... HISTORY reference image jref$p3v2228qj_drk.fits HISTORY INFLIGHT 05/03/2005 23/03/2005 HISTORY Superdark by Ray Lucas from proposal 10367 or 10370 HISTORY FLATCORR complete ... HISTORY reference image jref$nar1136nj_pfl.fits HISTORY InFlight 18/04/2002 - 09/05/2002 HISTORY F606W step +1 flat w/ mote shifted to -1 step HISTORY PHOTCORR complete ... HISTORY reference table mtab$r1m18595m_tmg.fits HISTORY reference table mtab$r1j2146sm_tmc.fits HISTORY EXPSCORR complete ... TDDCORR = 'PERFORM ' WFCTDD = 'T ' NPOLFILE= 'jref$v971826kj_npl.fits' D2IMFILE= 'jref$x5u17177j_d2i.fits' DISTNAME= 'j94f05bgq_postsm4-v971826kj-x5u17177j' SIPNAME = 'j94f05bgq_postsm4' ORIGIN = 'NOAO-IRAF FITS Image Kernel July 2003' / FITS file originator EXTNAME = 'SCI ' / Extension name EXTVER = 1 / Extension version DATE = '2007-02-08T21:38:47' / Date FITS file was generated IRAF-TLM= '13:38:23 (20/08/2008)' / Time of last modification INHERIT = T / inherit the primary header EXPNAME = 'j94f05bgq ' / exposure identifier BUNIT = 'ELECTRONS' / brightness units CCDCHIP = 2 / CCD chip (1 or 2) WCSAXES = 2 / number of World Coordinate System axes CRPIX1 = 2048.0 / x-coordinate of reference pixel CRPIX2 = 1024.0 / y-coordinate of reference pixel CRVAL1 = 5.63056810618 / first axis value at reference pixel CRVAL2 = -72.0545718428 / second axis value at reference pixel CTYPE1 = 'RA---TAN-SIP' / the coordinate type for the first axis CTYPE2 = 'DEC--TAN-SIP' / the coordinate type for the second axis CD1_1 = 1.29058667557984E-05 / partial of first axis coordinate w.r.t. x CD1_2 = 5.95320245884555E-06 / partial of first axis coordinate w.r.t. y CD2_1 = 5.02215195623825E-06 / partial of second axis coordinate w.r.t. x CD2_2 = -1.2645010396976E-05 / partial of second axis coordinate w.r.t. y LTV1 = 0.0000000E+00 / offset in X to subsection start LTV2 = 0.0000000E+00 / offset in Y to subsection start LTM1_1 = 1.0 / reciprocal of sampling rate in X LTM2_2 = 1.0 / reciprocal of sampling rate in Y ORIENTAT= 154.7891975615789 / position angle of image y axis (deg. e of n) RA_APER = 5.655000000000E+00 / RA of aperture reference position DEC_APER= -7.207055555556E+01 / Declination of aperture reference position PA_APER = 154.533 / Position Angle of reference aperture center (de VAFACTOR= 1.000018683511E+00 / velocity aberration plate scale factor CENTERA1= 2073 / subarray axis1 center pt in unbinned dect. pix CENTERA2= 1035 / subarray axis2 center pt in unbinned dect. pix SIZAXIS1= 4096 / subarray axis1 size in unbinned detector pixels SIZAXIS2= 2048 / subarray axis2 size in unbinned detector pixels BINAXIS1= 1 / axis1 data bin size in unbinned detector pixels BINAXIS2= 1 / axis2 data bin size in unbinned detector pixels PHOTMODE= 'ACS WFC1 F606W' / observation con PHOTFLAM= 7.9064521E-20 / inverse sensitivity, ergs/cm2/Ang/electron PHOTZPT = -2.1100000E+01 / ST magnitude zero point PHOTPLAM= 5.9176797E+03 / Pivot wavelength (Angstroms) PHOTBW = 6.7231146E+02 / RMS bandwidth of filter plus detector NCOMBINE= 1 / number of image sets combined during CR rejecti FILLCNT = 0 / number of segments containing fill ERRCNT = 0 / number of segments containing errors PODPSFF = F / podps fill present (T/F) STDCFFF = F / ST DDF fill present (T/F) STDCFFP = 'x5569 ' / ST DDF fill pattern (hex) WFCMPRSD= F / was WFC data compressed? (T/F) CBLKSIZ = 0 / size of compression block in 2-byte words LOSTPIX = 0 / #pixels lost due to buffer overflow COMPTYP = 'None ' / compression type performed (Partial/Full/None) NGOODPIX= 7822781 / number of good pixels SDQFLAGS= 31743 / serious data quality flags GOODMIN = -2.5959351E+02 / minimum value of good pixels GOODMAX = 6.5220551E+04 / maximum value of good pixels GOODMEAN= 2.0491536E+02 / mean value of good pixels SOFTERRS= 0 / number of soft error pixels (DQF=1) SNRMIN = -8.0327058E-01 / minimum signal to noise of good pixels SNRMAX = 2.1379723E+02 / maximum signal to noise of good pixels SNRMEAN = 1.0889255E+01 / mean value of signal to noise of good pixels MEANDARK= 1.5474443E+00 / average of the dark values subtracted MEANBLEV= 2.4558604E+03 / average of all bias levels subtracted MEANFLSH= 0.000000 / Mean number of counts in post flash exposure OCRVAL1 = 5.63056810618 / first axis value at reference pixel OCRVAL2 = -72.05457184279 / second axis value at reference pixel OCRPIX2 = 1024.0 / y-coordinate of reference pixel OCRPIX1 = 2048.0 / x-coordinate of reference pixel ONAXIS2 = 2048 / Axis length ONAXIS1 = 4096 / Axis length OCD2_2 = -1.26445E-05 / partial of second axis coordinate w.r.t. y OCD2_1 = 5.02243E-06 / partial of second axis coordinate w.r.t. x OORIENTA= 154.7886863186197 / position angle of image y axis (deg. e of n) OCTYPE1 = 'RA---TAN' / the coordinate type for the first axis OCD1_1 = 1.29046E-05 / partial of first axis coordinate w.r.t. x OCD1_2 = 5.9531E-06 / partial of first axis coordinate w.r.t. y OCTYPE2 = 'DEC--TAN' / the coordinate type for the second axis WCSCDATE= '21:39:44 (08/02/2007)' / Time WCS keywords were copied. A_0_2 = 2.16615952976212E-06 B_0_2 = -7.2168814507744E-06 A_1_1 = -5.1974576466834E-06 B_1_1 = 6.18443235774478E-06 A_2_0 = 8.55127758255650E-06 B_2_0 = -1.746491877058669E-06 A_0_3 = 1.08193519820265E-11 B_0_3 = -4.175472049274932E-10 A_1_2 = -5.234870743692412E-10 B_1_2 = -6.169265268681388E-11 A_2_1 = -3.9771547747287E-11 B_2_1 = -5.0857161673862E-10 A_3_0 = -4.7304448292227E-10 B_3_0 = 8.56763542781631E-11 A_0_4 = 1.49356171166049E-14 B_0_4 = -9.9570490655478E-15 A_1_3 = -2.4569975537746E-14 B_1_3 = 1.21743011568848E-14 A_2_2 = 3.46791267104378E-14 B_2_2 = -3.66143259286574E-14 A_3_1 = 1.97102297166030E-15 B_3_1 = -3.779506805487476E-15 A_4_0 = 2.37430106240231E-14 B_4_0 = -1.7687653826004E-14 A_ORDER = 4 B_ORDER = 4 D2IMERR1= 0.002770500956103206 / Maximum error of NPOL correction for axis 1 D2IMDIS1= 'Lookup ' / Detector to image correction type D2IM1 = 'EXTVER: 1' / Version number of WCSDVARR extension containing d2im loo D2IM1 = 'NAXES: 2' / Number of independent variables in d2im function D2IM1 = 'AXIS.1: 1' / Axis number of the jth independent variable in a d2im fu D2IM1 = 'AXIS.2: 2' / Axis number of the jth independent variable in a d2im fu CPERR1 = 0.08311891555786133 / Maximum error of NPOL correction for axis 1 CPDIS1 = 'Lookup ' / Prior distortion function type DP1 = 'EXTVER: 1' / Version number of WCSDVARR extension containing lookup d DP1 = 'NAXES: 2' / Number of independent variables in distortion function DP1 = 'AXIS.1: 1' / Axis number of the jth independent variable in a distort DP1 = 'AXIS.2: 2' / Axis number of the jth independent variable in a distort CPERR2 = 0.0758458599448204 / Maximum error of NPOL correction for axis 2 CPDIS2 = 'Lookup ' / Prior distortion function type DP2 = 'EXTVER: 2' / Version number of WCSDVARR extension containing lookup d DP2 = 'NAXES: 2' / Number of independent variables in distortion function DP2 = 'AXIS.1: 1' / Axis number of the jth independent variable in a distort DP2 = 'AXIS.2: 2' / Axis number of the jth independent variable in a distort TDDALPHA= 0.03676157754622637 TDDBETA = -0.00958719251540879 IDCSCALE= 0.05 IDCV2REF= 256.6222229003906 IDCV3REF= 302.2264099121094 IDCTHETA= 0.0 OCX10 = 0.001959713482071437 OCX11 = 0.04983122487595928 OCY10 = 0.05027393143048926 OCY11 = 0.00148847536166365 SORIENTA= 154.7925383197021 / position angle of image y axis (deg. e of n) SCRVAL1 = 5.63056810618 / first axis value at reference pixel SNAXIS2 = 2048 / Axis length SNAXIS1 = 4096 / Axis length SCRVAL2 = -72.05457184279 / second axis value at reference pixel SCTYPE1 = 'RA---TAN-SIP' / the coordinate type for the first axis SCTYPE2 = 'DEC--TAN-SIP' / the coordinate type for the second axis SCD2_2 = -1.264489181627715E-05 / partial of second axis coordinate w.r.t. y SCD2_1 = 5.022886862247075E-06 / partial of second axis coordinate w.r.t. x SCD1_2 = 5.952245949610081E-06 / partial of first axis coordinate w.r.t. y SCRPIX2 = 1024.0 / y-coordinate of reference pixel SCRPIX1 = 2048.0 / x-coordinate of reference pixel SCD1_1 = 1.290545120875315E-05 / partial of first axis coordinate w.r.t. x IDCXREF = 2048.0 IDCYREF = 1024.0 D2IMEXT = 'jref$x5u17177j_d2i.fits' WCSNAMEO= 'OPUS ' WCSAXESO= 2 CRPIX1O = 2048 CRPIX2O = 1024 CDELT1O = 1 CDELT2O = 1 CUNIT1O = 'deg ' CUNIT2O = 'deg ' CTYPE1O = 'RA---TAN-SIP' CTYPE2O = 'DEC--TAN-SIP' CRVAL1O = 5.63056810618 CRVAL2O = -72.0545718428 LONPOLEO= 180 LATPOLEO= -72.0545718428 RESTFRQO= 0 RESTWAVO= 0 CD1_1O = 1.29056256334E-05 CD1_2O = 5.9530912342E-06 CD2_1O = 5.02205812656E-06 CD2_2O = -1.26447741482E-05 IDCTAB = 'postsm4_idc.fits' WCSNAME = 'IDC_postsm4' NPOLEXT = 'jref$v971826kj_npl.fits' / WFC CCD CHIP IDENTIFICATION / World Coordinate System and Related Parameters / READOUT DEFINITION PARAMETERS / PHOTOMETRY KEYWORDS / REPEATED EXPOSURES INFO / DATA PACKET INFORMATION / ON-BOARD COMPRESSION INFORMATION / IMAGE STATISTICS AND DATA QUALITY FLAGS HISTORY The following throughput tables were used: crotacomp$hst_ota_007_syn.fit HISTORY s, cracscomp$acs_wfc_im123_004_syn.fits, cracscomp$acs_f606w_005_syn.fit HISTORY s, cracscomp$acs_wfc_ebe_win12f_005_syn.fits, cracscomp$acs_wfc_ccd1_017 HISTORY _syn.fits gwcs-0.12.0/gwcs/tests/data/simple_wcs2.hdr0000644000732200020070000000550013600426757022662 0ustar denchevaSTSCI\science00000000000000WCSAXES = 2 / Number of coordinate axes CRPIX1 = 507.0 / Pixel coordinate of reference point CRPIX2 = 507.0 / Pixel coordinate of reference point PC1_1 = 7.70605644414E-06 / Coordinate transformation matrix element PC1_2 = 3.29130820267E-05 / Coordinate transformation matrix element PC2_1 = 3.68234230443E-05 / Coordinate transformation matrix element PC2_2 = -6.77287573742E-06 / Coordinate transformation matrix element CDELT1 = 1.0 / [deg] Coordinate increment at reference point CDELT2 = 1.0 / [deg] Coordinate increment at reference point CUNIT1 = 'deg' / Units of coordinate increment and value CUNIT2 = 'deg' / Units of coordinate increment and value CTYPE1 = 'RA---TAN-SIP' / Right ascension, gnomonic projection CTYPE2 = 'DEC--TAN-SIP' / Declination, gnomonic projection CRVAL1 = 251.204239952 / [deg] Coordinate value at reference point CRVAL2 = 57.5817453704 / [deg] Coordinate value at reference point LONPOLE = 180.0 / [deg] Native longitude of celestial pole LATPOLE = 57.5817453704 / [deg] Native latitude of celestial pole WCSNAME = 'IDC_w3m18525i' / Coordinate system title RADESYS = 'ICRS' / Equatorial coordinate system END gwcs-0.12.0/gwcs/tests/setup_package.py0000644000732200020070000000027313600426757022212 0ustar denchevaSTSCI\science00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst def get_package_data(): return { _ASTROPY_PACKAGE_NAME_ + '.tests': ['coveragerc', 'data/*.hdr'] # noqa } gwcs-0.12.0/gwcs/tests/test_api.py0000644000732200020070000003747613600426757021226 0ustar denchevaSTSCI\science00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Tests the API defined in astropy APE 14 (https://doi.org/10.5281/zenodo.1188875). """ import numpy as np import pytest from numpy.testing import assert_allclose, assert_array_equal import astropy.units as u from astropy import time from astropy import coordinates as coord from astropy.wcs.wcsapi import HighLevelWCSWrapper # Shorthand the name of the 2d gwcs fixture @pytest.fixture def wcsobj(request): return request.getfixturevalue(request.param) wcs_objs = pytest.mark.parametrize("wcsobj", ['gwcs_2d_spatial_shift'], indirect=True) @pytest.fixture def wcs_ndim_types_units(request): """ Generate a wcs and the expected ndim, types, and units. """ ndim = {'gwcs_2d_spatial_shift': (2, 2), 'gwcs_2d_spatial_reordered': (2, 2), 'gwcs_1d_freq': (1, 1), 'gwcs_3d_spatial_wave': (3, 3), 'gwcs_4d_identity_units': (4, 4)} types = {'gwcs_2d_spatial_shift': ("pos.eq.ra", "pos.eq.dec"), 'gwcs_2d_spatial_reordered': ("pos.eq.dec", "pos.eq.ra"), 'gwcs_1d_freq': ("em.freq",), 'gwcs_3d_spatial_wave': ("pos.eq.ra", "pos.eq.dec", "em.wl"), 'gwcs_4d_identity_units': ("pos.eq.ra", "pos.eq.dec", "em.wl", "time")} units = {'gwcs_2d_spatial_shift': ("deg", "deg"), 'gwcs_2d_spatial_reordered': ("deg", "deg"), 'gwcs_1d_freq': ("Hz",), 'gwcs_3d_spatial_wave': ("deg", "deg", "m"), 'gwcs_4d_identity_units': ("deg", "deg", "nm", "s")} return (request.getfixturevalue(request.param), ndim[request.param], types[request.param], units[request.param]) # # x, y inputs - scalar and array x, y = 1, 2 xarr, yarr = np.ones((3, 4)), np.ones((3, 4)) + 1 fixture_names = ['gwcs_2d_spatial_shift', 'gwcs_2d_spatial_reordered', 'gwcs_1d_freq', 'gwcs_3d_spatial_wave', 'gwcs_4d_identity_units'] fixture_wcs_ndim_types_units = pytest.mark.parametrize("wcs_ndim_types_units", fixture_names, indirect=True) all_wcses_names = fixture_names + ['gwcs_3d_identity_units', 'gwcs_stokes_lookup', 'gwcs_3d_galactic_spectral'] fixture_all_wcses = pytest.mark.parametrize("wcsobj", all_wcses_names, indirect=True) @fixture_all_wcses def test_lowlevel_types(wcsobj): pytest.importorskip("typeguard") try: # Skip this on older versions of astropy where it dosen't exist. from astropy.wcs.wcsapi.tests.utils import validate_low_level_wcs_types except ImportError: return validate_low_level_wcs_types(wcsobj) @fixture_all_wcses def test_names(wcsobj): assert wcsobj.world_axis_names == wcsobj.output_frame.axes_names assert wcsobj.pixel_axis_names == wcsobj.input_frame.axes_names @fixture_wcs_ndim_types_units def test_pixel_n_dim(wcs_ndim_types_units): wcsobj, ndims, *_ = wcs_ndim_types_units assert wcsobj.pixel_n_dim == ndims[0] @fixture_wcs_ndim_types_units def test_world_n_dim(wcs_ndim_types_units): wcsobj, ndims, *_ = wcs_ndim_types_units assert wcsobj.world_n_dim == ndims[1] @fixture_wcs_ndim_types_units def test_world_axis_physical_types(wcs_ndim_types_units): wcsobj, ndims, physical_types, world_units = wcs_ndim_types_units assert wcsobj.world_axis_physical_types == physical_types @fixture_wcs_ndim_types_units def test_world_axis_units(wcs_ndim_types_units): wcsobj, ndims, physical_types, world_units = wcs_ndim_types_units assert wcsobj.world_axis_units == world_units @pytest.mark.parametrize(("x", "y"), zip((x, xarr), (y, yarr))) def test_pixel_to_world_values(gwcs_2d_spatial_shift, x, y): wcsobj = gwcs_2d_spatial_shift assert_allclose(wcsobj.pixel_to_world_values(x, y), wcsobj(x, y, with_units=False)) @pytest.mark.parametrize(("x", "y"), zip((x, xarr), (y, yarr))) def test_pixel_to_world_values_units_2d(gwcs_2d_shift_scale_quantity, x, y): wcsobj = gwcs_2d_shift_scale_quantity call_pixel = x*u.pix, y*u.pix api_pixel = x, y call_world = wcsobj(*call_pixel, with_units=False) api_world = wcsobj.pixel_to_world_values(*api_pixel) # Check that call returns quantities and api dosen't assert all(list(isinstance(a, u.Quantity) for a in call_world)) assert all(list(not isinstance(a, u.Quantity) for a in api_world)) # Check that they are the same (and implicitly in the same units) assert_allclose(u.Quantity(call_world).value, api_world) new_call_pixel = wcsobj.invert(*call_world, with_units=False) [assert_allclose(n, p) for n, p in zip(new_call_pixel, call_pixel)] new_api_pixel = wcsobj.world_to_pixel_values(*api_world) [assert_allclose(n, p) for n, p in zip(new_api_pixel, api_pixel)] @pytest.mark.parametrize(("x"), (x, xarr)) def test_pixel_to_world_values_units_1d(gwcs_1d_freq_quantity, x): wcsobj = gwcs_1d_freq_quantity call_pixel = x * u.pix api_pixel = x call_world = wcsobj(call_pixel, with_units=False) api_world = wcsobj.pixel_to_world_values(api_pixel) # Check that call returns quantities and api dosen't assert isinstance(call_world, u.Quantity) assert not isinstance(api_world, u.Quantity) # Check that they are the same (and implicitly in the same units) assert_allclose(u.Quantity(call_world).value, api_world) new_call_pixel = wcsobj.invert(call_world, with_units=False) assert_allclose(new_call_pixel, call_pixel) new_api_pixel = wcsobj.world_to_pixel_values(api_world) assert_allclose(new_api_pixel, api_pixel) @pytest.mark.parametrize(("x", "y"), zip((x, xarr), (y, yarr))) def test_array_index_to_world_values(gwcs_2d_spatial_shift, x, y): wcsobj = gwcs_2d_spatial_shift assert_allclose(wcsobj.array_index_to_world_values(x, y), wcsobj(y, x, with_units=False)) def test_world_axis_object_components_2d(gwcs_2d_spatial_shift): waoc = gwcs_2d_spatial_shift.world_axis_object_components assert waoc == [('celestial', 0, 'spherical.lon'), ('celestial', 1, 'spherical.lat')] def test_world_axis_object_components_1d(gwcs_1d_freq): waoc = gwcs_1d_freq.world_axis_object_components assert waoc == [('spectral', 0, 'value')] def test_world_axis_object_components_4d(gwcs_4d_identity_units): waoc = gwcs_4d_identity_units.world_axis_object_components assert waoc == [('celestial', 0, 'spherical.lon'), ('celestial', 1, 'spherical.lat'), ('spectral', 0, 'value'), ('temporal', 0, 'value')] def test_world_axis_object_classes_2d(gwcs_2d_spatial_shift): waoc = gwcs_2d_spatial_shift.world_axis_object_classes assert waoc['celestial'][0] is coord.SkyCoord assert waoc['celestial'][1] == tuple() assert 'frame' in waoc['celestial'][2] assert 'unit' in waoc['celestial'][2] assert isinstance(waoc['celestial'][2]['frame'], coord.ICRS) assert waoc['celestial'][2]['unit'] == (u.deg, u.deg) def test_world_axis_object_classes_4d(gwcs_4d_identity_units): waoc = gwcs_4d_identity_units.world_axis_object_classes assert waoc['celestial'][0] is coord.SkyCoord assert waoc['celestial'][1] == tuple() assert 'frame' in waoc['celestial'][2] assert 'unit' in waoc['celestial'][2] assert isinstance(waoc['celestial'][2]['frame'], coord.ICRS) assert waoc['celestial'][2]['unit'] == (u.deg, u.deg) temporal = waoc['temporal'] assert temporal[0] is time.Time assert temporal[1] == tuple() assert temporal[2] == {'unit': u.s, 'format': 'isot', 'scale': 'utc', 'precision': 3, 'in_subfmt': '*', 'out_subfmt': '*', 'location': None} def _compare_frame_output(wc1, wc2): if isinstance(wc1, coord.SkyCoord): assert isinstance(wc1.frame, type(wc2.frame)) assert u.allclose(wc1.spherical.lon, wc2.spherical.lon) assert u.allclose(wc1.spherical.lat, wc2.spherical.lat) assert u.allclose(wc1.spherical.distance, wc2.spherical.distance) elif isinstance(wc1, u.Quantity): assert u.allclose(wc1, wc2) elif isinstance(wc1, time.Time): assert u.allclose((wc1 - wc2).to(u.s), 0*u.s) elif isinstance(wc1, str): assert wc1 == wc2 else: assert False, f"Can't Compare {type(wc1)}" @fixture_all_wcses def test_high_level_wrapper(wcsobj, request): if request.node.callspec.params['wcsobj'] in ('gwcs_4d_identity_units', 'gwcs_stokes_lookup'): pytest.importorskip("astropy", minversion="4.0dev0") # Remove the bounding box because the type test is a little broken with the # bounding box. del wcsobj._pipeline[0][1].bounding_box hlvl = HighLevelWCSWrapper(wcsobj) pixel_input = [3] * wcsobj.pixel_n_dim # If the model expects units we have to pass in units if wcsobj.forward_transform.uses_quantity: pixel_input *= u.pix wc1 = hlvl.pixel_to_world(*pixel_input) wc2 = wcsobj(*pixel_input, with_units=True) assert type(wc1) is type(wc2) if isinstance(wc1, (list, tuple)): for w1, w2 in zip(wc1, wc2): _compare_frame_output(w1, w2) else: _compare_frame_output(wc1, wc2) def test_stokes_wrapper(gwcs_stokes_lookup): pytest.importorskip("astropy", minversion="4.0dev0") hlvl = HighLevelWCSWrapper(gwcs_stokes_lookup) pixel_input = [0, 1, 2, 3] out = hlvl.pixel_to_world(pixel_input*u.pix) assert list(out) == ['I', 'Q', 'U', 'V'] pixel_input = [[0, 1, 2, 3], [0, 1, 2, 3], [0, 1, 2, 3], [0, 1, 2, 3],] out = hlvl.pixel_to_world(pixel_input*u.pix) expected = np.array([['I', 'Q', 'U', 'V'], ['I', 'Q', 'U', 'V'], ['I', 'Q', 'U', 'V'], ['I', 'Q', 'U', 'V']], dtype=object) assert (out == expected).all() pixel_input = [-1, 4] out = hlvl.pixel_to_world(pixel_input*u.pix) assert np.isnan(out).all() pixel_input = [[-1, 4], [1, 2]] out = hlvl.pixel_to_world(pixel_input*u.pix) assert np.isnan(np.array(out[0], dtype=float)).all() assert (out[1] == np.array(['Q', 'U'], dtype=object)).all() out = hlvl.pixel_to_world(1*u.pix) assert out == 'Q' @wcs_objs def test_array_shape(wcsobj): assert wcsobj.array_shape is None wcsobj.array_shape = (2040, 1020) assert_array_equal(wcsobj.array_shape, (2040, 1020)) @wcs_objs def test_pixel_bounds(wcsobj): assert wcsobj.pixel_bounds is None wcsobj.bounding_box = ((-0.5, 2039.5), (-0.5, 1019.5)) assert_array_equal(wcsobj.pixel_bounds, wcsobj.bounding_box) @wcs_objs def test_axis_correlation_matrix(wcsobj): assert_array_equal(wcsobj.axis_correlation_matrix, np.identity(2)) @wcs_objs def test_serialized_classes(wcsobj): assert not wcsobj.serialized_classes @wcs_objs def test_low_level_wcs(wcsobj): assert id(wcsobj.low_level_wcs) == id(wcsobj) @wcs_objs def test_pixel_to_world(wcsobj): comp = wcsobj(x, y, with_units=True) comp = wcsobj.output_frame.coordinates(comp) result = wcsobj.pixel_to_world(x, y) assert isinstance(comp, coord.SkyCoord) assert isinstance(result, coord.SkyCoord) assert_allclose(comp.data.lon, result.data.lon) assert_allclose(comp.data.lat, result.data.lat) @wcs_objs def test_array_index_to_world(wcsobj): comp = wcsobj(x, y, with_units=True) comp = wcsobj.output_frame.coordinates(comp) result = wcsobj.array_index_to_world(y, x) assert isinstance(comp, coord.SkyCoord) assert isinstance(result, coord.SkyCoord) assert_allclose(comp.data.lon, result.data.lon) assert_allclose(comp.data.lat, result.data.lat) def test_pixel_to_world_quantity(gwcs_2d_shift_scale, gwcs_2d_shift_scale_quantity): result1 = gwcs_2d_shift_scale.pixel_to_world(x, y) result2 = gwcs_2d_shift_scale_quantity.pixel_to_world(x, y) assert isinstance(result2, coord.SkyCoord) assert_allclose(result1.data.lon, result2.data.lon) assert_allclose(result1.data.lat, result2.data.lat) # test with Quantity pixel inputs result1 = gwcs_2d_shift_scale.pixel_to_world(x * u.pix, y * u.pix) result2 = gwcs_2d_shift_scale_quantity.pixel_to_world(x * u.pix, y * u.pix) assert isinstance(result2, coord.SkyCoord) assert_allclose(result1.data.lon, result2.data.lon) assert_allclose(result1.data.lat, result2.data.lat) # test for pixel units with pytest.raises(ValueError): gwcs_2d_shift_scale.pixel_to_world(x * u.Jy, y * u.Jy) def test_array_index_to_world_quantity(gwcs_2d_shift_scale, gwcs_2d_shift_scale_quantity): result0 = gwcs_2d_shift_scale.pixel_to_world(x, y) result1 = gwcs_2d_shift_scale.array_index_to_world(y, x) result2 = gwcs_2d_shift_scale_quantity.array_index_to_world(y, x) assert isinstance(result2, coord.SkyCoord) assert_allclose(result1.data.lon, result2.data.lon) assert_allclose(result1.data.lat, result2.data.lat) assert_allclose(result0.data.lon, result1.data.lon) assert_allclose(result0.data.lat, result1.data.lat) # test with Quantity pixel inputs result0 = gwcs_2d_shift_scale.pixel_to_world(x * u.pix, y * u.pix) result1 = gwcs_2d_shift_scale.array_index_to_world(y * u.pix, x * u.pix) result2 = gwcs_2d_shift_scale_quantity.array_index_to_world(y * u.pix, x * u.pix) assert isinstance(result2, coord.SkyCoord) assert_allclose(result1.data.lon, result2.data.lon) assert_allclose(result1.data.lat, result2.data.lat) assert_allclose(result0.data.lon, result1.data.lon) assert_allclose(result0.data.lat, result1.data.lat) # test for pixel units with pytest.raises(ValueError): gwcs_2d_shift_scale.array_index_to_world(x * u.Jy, y * u.Jy) def test_world_to_pixel_quantity(gwcs_2d_shift_scale, gwcs_2d_shift_scale_quantity): skycoord = gwcs_2d_shift_scale.pixel_to_world(x, y) result1 = gwcs_2d_shift_scale.world_to_pixel(skycoord) result2 = gwcs_2d_shift_scale_quantity.world_to_pixel(skycoord) assert_allclose(result1, (x, y)) assert_allclose(result2, (x, y)) def test_world_to_array_index_quantity(gwcs_2d_shift_scale, gwcs_2d_shift_scale_quantity): skycoord = gwcs_2d_shift_scale.pixel_to_world(x, y) result0 = gwcs_2d_shift_scale.world_to_pixel(skycoord) result1 = gwcs_2d_shift_scale.world_to_array_index(skycoord) result2 = gwcs_2d_shift_scale_quantity.world_to_array_index(skycoord) assert_allclose(result0, (x, y)) assert_allclose(result1, (y, x)) assert_allclose(result2, (y, x)) @pytest.fixture(params=[0, 1]) def sky_ra_dec(request, gwcs_2d_spatial_shift): ref_frame = gwcs_2d_spatial_shift.output_frame.reference_frame ra, dec = 2, 4 if request.param == 0: sky = coord.SkyCoord(ra * u.deg, dec * u.deg, frame=ref_frame) else: ra = np.ones((3, 4)) * ra dec = np.ones((3, 4)) * dec sky = coord.SkyCoord(ra * u.deg, dec * u.deg, frame=ref_frame) return sky, ra, dec def test_world_to_pixel(gwcs_2d_spatial_shift, sky_ra_dec): wcsobj = gwcs_2d_spatial_shift sky, ra, dec = sky_ra_dec assert_allclose(wcsobj.world_to_pixel(sky), wcsobj.invert(ra, dec, with_units=False)) def test_world_to_array_index(gwcs_2d_spatial_shift, sky_ra_dec): wcsobj = gwcs_2d_spatial_shift sky, ra, dec = sky_ra_dec assert_allclose(wcsobj.world_to_array_index(sky), wcsobj.invert(ra, dec, with_units=False)[::-1]) def test_world_to_pixel_values(gwcs_2d_spatial_shift, sky_ra_dec): wcsobj = gwcs_2d_spatial_shift sky, ra, dec = sky_ra_dec assert_allclose(wcsobj.world_to_pixel_values(sky), wcsobj.invert(ra, dec, with_units=False)) def test_world_to_array_index_values(gwcs_2d_spatial_shift, sky_ra_dec): wcsobj = gwcs_2d_spatial_shift sky, ra, dec = sky_ra_dec assert_allclose(wcsobj.world_to_array_index_values(sky), wcsobj.invert(ra, dec, with_units=False)[::-1]) def test_ndim_str_frames(gwcs_with_frames_strings): wcsobj = gwcs_with_frames_strings assert wcsobj.pixel_n_dim == 4 assert wcsobj.world_n_dim == 3 gwcs-0.12.0/gwcs/tests/test_api_slicing.py0000644000732200020070000004047513600426757022727 0ustar denchevaSTSCI\science00000000000000import astropy.units as u from astropy.coordinates import Galactic, SkyCoord from astropy.units import Quantity from astropy.wcs.wcsapi.sliced_low_level_wcs import SlicedLowLevelWCS from numpy.testing import assert_allclose, assert_equal EXPECTED_ELLIPSIS_REPR = """ SlicedLowLevelWCS Transformation This transformation has 3 pixel and 3 world dimensions Array shape (Numpy order): (30, 20, 10) Pixel Dim Axis Name Data size Bounds 0 None 10 (-1, 35) 1 None 20 (-2, 45) 2 None 30 (5, 50) World Dim Axis Name Physical Type Units 0 Latitude pos.galactic.lat deg 1 Frequency em.freq Hz 2 Longitude pos.galactic.lon deg Correlation between pixel and world axes: Pixel Dim World Dim 0 1 2 0 yes no yes 1 no yes no 2 yes no yes """ def test_ellipsis(gwcs_3d_galactic_spectral): wcs = SlicedLowLevelWCS(gwcs_3d_galactic_spectral, Ellipsis) assert wcs.pixel_n_dim == 3 assert wcs.world_n_dim == 3 assert wcs.array_shape == (30, 20, 10) assert wcs.pixel_shape == (10, 20, 30) assert wcs.world_axis_physical_types == ['pos.galactic.lat', 'em.freq', 'pos.galactic.lon'] assert wcs.world_axis_units == ['deg', 'Hz', 'deg'] assert_equal(wcs.axis_correlation_matrix, [[True, False, True], [False, True, False], [True, False, True]]) assert wcs.world_axis_object_components == [('celestial', 1, 'spherical.lat'), ('spectral', 0, 'value'), ('celestial', 0, 'spherical.lon')] assert wcs.world_axis_object_classes['celestial'][0] is SkyCoord assert wcs.world_axis_object_classes['celestial'][1] == () assert isinstance(wcs.world_axis_object_classes['celestial'][2]['frame'], Galactic) assert wcs.world_axis_object_classes['celestial'][2]['unit'] == (u.deg, u.deg) assert wcs.world_axis_object_classes['spectral'][0] is Quantity assert wcs.world_axis_object_classes['spectral'][1] == () assert wcs.world_axis_object_classes['spectral'][2] == {'unit': 'Hz'} assert_allclose(wcs.pixel_to_world_values(29, 39, 44), (10, 20, 25)) assert_allclose(wcs.array_index_to_world_values(44, 39, 29), (10, 20, 25)) assert_allclose(wcs.world_to_pixel_values(10, 20, 25), (29., 39., 44.)) assert_equal(wcs.world_to_array_index_values(10, 20, 25), (44, 39, 29)) assert str(wcs) == repr(wcs) == EXPECTED_ELLIPSIS_REPR.strip() assert_equal(wcs.pixel_bounds, [(-1, 35), (-2, 45), (5, 50)]) EXPECTED_SPECTRAL_SLICE_REPR = """ SlicedLowLevelWCS Transformation This transformation has 2 pixel and 2 world dimensions Array shape (Numpy order): (30, 10) Pixel Dim Axis Name Data size Bounds 0 None 10 (-1, 35) 1 None 30 (5, 50) World Dim Axis Name Physical Type Units 0 Latitude pos.galactic.lat deg 1 Longitude pos.galactic.lon deg Correlation between pixel and world axes: Pixel Dim World Dim 0 1 0 yes yes 1 yes yes """ def test_spectral_slice(gwcs_3d_galactic_spectral): wcs = SlicedLowLevelWCS(gwcs_3d_galactic_spectral, [slice(None), 10]) assert wcs.pixel_n_dim == 2 assert wcs.world_n_dim == 2 assert wcs.array_shape == (30, 10) assert wcs.pixel_shape == (10, 30) assert wcs.world_axis_physical_types == ['pos.galactic.lat', 'pos.galactic.lon'] assert wcs.world_axis_units == ['deg', 'deg'] assert_equal(wcs.axis_correlation_matrix, [[True, True], [True, True]]) assert wcs.world_axis_object_components == [('celestial', 1, 'spherical.lat'), ('celestial', 0, 'spherical.lon')] assert wcs.world_axis_object_classes['celestial'][0] is SkyCoord assert wcs.world_axis_object_classes['celestial'][1] == () assert isinstance(wcs.world_axis_object_classes['celestial'][2]['frame'], Galactic) assert wcs.world_axis_object_classes['celestial'][2]['unit'] == (u.deg, u.deg) assert_allclose(wcs.pixel_to_world_values(29, 44), (10, 25)) assert_allclose(wcs.array_index_to_world_values(44, 29), (10, 25)) assert_allclose(wcs.world_to_pixel_values(10, 25), (29., 44.)) assert_equal(wcs.world_to_array_index_values(10, 25), (44, 29)) assert_equal(wcs.pixel_bounds, [(-1, 35), (5, 50)]) assert str(wcs) == repr(wcs) == EXPECTED_SPECTRAL_SLICE_REPR.strip() EXPECTED_SPECTRAL_RANGE_REPR = """ SlicedLowLevelWCS Transformation This transformation has 3 pixel and 3 world dimensions Array shape (Numpy order): (30, 6, 10) Pixel Dim Axis Name Data size Bounds 0 None 10 (-1, 35) 1 None 6 (-6, 41) 2 None 30 (5, 50) World Dim Axis Name Physical Type Units 0 Latitude pos.galactic.lat deg 1 Frequency em.freq Hz 2 Longitude pos.galactic.lon deg Correlation between pixel and world axes: Pixel Dim World Dim 0 1 2 0 yes no yes 1 no yes no 2 yes no yes """ def test_spectral_range(gwcs_3d_galactic_spectral): wcs = SlicedLowLevelWCS(gwcs_3d_galactic_spectral, [slice(None), slice(4, 10)]) assert wcs.pixel_n_dim == 3 assert wcs.world_n_dim == 3 assert wcs.array_shape == (30, 6, 10) assert wcs.pixel_shape == (10, 6, 30) assert wcs.world_axis_physical_types == ['pos.galactic.lat', 'em.freq', 'pos.galactic.lon'] assert wcs.world_axis_units == ['deg', 'Hz', 'deg'] assert_equal(wcs.axis_correlation_matrix, [[True, False, True], [False, True, False], [True, False, True]]) assert wcs.world_axis_object_components == [('celestial', 1, 'spherical.lat'), ('spectral', 0, 'value'), ('celestial', 0, 'spherical.lon')] assert wcs.world_axis_object_classes['celestial'][0] is SkyCoord assert wcs.world_axis_object_classes['celestial'][1] == () assert isinstance(wcs.world_axis_object_classes['celestial'][2]['frame'], Galactic) assert wcs.world_axis_object_classes['celestial'][2]['unit'] == (u.deg, u.deg) assert wcs.world_axis_object_classes['spectral'][0] is Quantity assert wcs.world_axis_object_classes['spectral'][1] == () assert wcs.world_axis_object_classes['spectral'][2] == {'unit': 'Hz'} assert_allclose(wcs.pixel_to_world_values(29, 35, 44), (10, 20, 25)) assert_allclose(wcs.array_index_to_world_values(44, 35, 29), (10, 20, 25)) assert_allclose(wcs.world_to_pixel_values(10, 20, 25), (29., 35., 44.)) assert_equal(wcs.world_to_array_index_values(10, 20, 25), (44, 35, 29)) assert_equal(wcs.pixel_bounds, [(-1, 35), (-6, 41), (5, 50)]) assert str(wcs) == repr(wcs) == EXPECTED_SPECTRAL_RANGE_REPR.strip() EXPECTED_CELESTIAL_SLICE_REPR = """ SlicedLowLevelWCS Transformation This transformation has 2 pixel and 3 world dimensions Array shape (Numpy order): (30, 20) Pixel Dim Axis Name Data size Bounds 0 None 20 (-2, 45) 1 None 30 (5, 50) World Dim Axis Name Physical Type Units 0 Latitude pos.galactic.lat deg 1 Frequency em.freq Hz 2 Longitude pos.galactic.lon deg Correlation between pixel and world axes: Pixel Dim World Dim 0 1 0 no yes 1 yes no 2 no yes """ def test_celestial_slice(gwcs_3d_galactic_spectral): wcs = SlicedLowLevelWCS(gwcs_3d_galactic_spectral, [Ellipsis, 5]) assert wcs.pixel_n_dim == 2 assert wcs.world_n_dim == 3 assert wcs.array_shape == (30, 20) assert wcs.pixel_shape == (20, 30) assert wcs.world_axis_physical_types == ['pos.galactic.lat', 'em.freq', 'pos.galactic.lon'] assert wcs.world_axis_units == ['deg', 'Hz', 'deg'] assert_equal(wcs.axis_correlation_matrix, [[False, True], [True, False], [False, True]]) assert wcs.world_axis_object_components == [('celestial', 1, 'spherical.lat'), ('spectral', 0, 'value'), ('celestial', 0, 'spherical.lon')] assert wcs.world_axis_object_classes['celestial'][0] is SkyCoord assert wcs.world_axis_object_classes['celestial'][1] == () assert isinstance(wcs.world_axis_object_classes['celestial'][2]['frame'], Galactic) assert wcs.world_axis_object_classes['celestial'][2]['unit'] == (u.deg, u.deg) assert wcs.world_axis_object_classes['spectral'][0] is Quantity assert wcs.world_axis_object_classes['spectral'][1] == () assert wcs.world_axis_object_classes['spectral'][2] == {'unit': 'Hz'} assert_allclose(wcs.pixel_to_world_values(39, 44), (12.4, 20, 25)) assert_allclose(wcs.array_index_to_world_values(44, 39), (12.4, 20, 25)) assert_allclose(wcs.world_to_pixel_values(12.4, 20, 25), (39., 44.)) assert_equal(wcs.world_to_array_index_values(12.4, 20, 25), (44, 39)) assert_equal(wcs.pixel_bounds, [(-2, 45), (5, 50)]) assert str(wcs) == repr(wcs) == EXPECTED_CELESTIAL_SLICE_REPR.strip() EXPECTED_CELESTIAL_RANGE_REPR = """ SlicedLowLevelWCS Transformation This transformation has 3 pixel and 3 world dimensions Array shape (Numpy order): (30, 20, 5) Pixel Dim Axis Name Data size Bounds 0 None 5 (-6, 30) 1 None 20 (-2, 45) 2 None 30 (5, 50) World Dim Axis Name Physical Type Units 0 Latitude pos.galactic.lat deg 1 Frequency em.freq Hz 2 Longitude pos.galactic.lon deg Correlation between pixel and world axes: Pixel Dim World Dim 0 1 2 0 yes no yes 1 no yes no 2 yes no yes """ def test_celestial_range(gwcs_3d_galactic_spectral): wcs = SlicedLowLevelWCS(gwcs_3d_galactic_spectral, [Ellipsis, slice(5, 10)]) assert wcs.pixel_n_dim == 3 assert wcs.world_n_dim == 3 assert wcs.array_shape == (30, 20, 5) assert wcs.pixel_shape == (5, 20, 30) assert wcs.world_axis_physical_types == ['pos.galactic.lat', 'em.freq', 'pos.galactic.lon'] assert wcs.world_axis_units == ['deg', 'Hz', 'deg'] assert_equal(wcs.axis_correlation_matrix, [[True, False, True], [False, True, False], [True, False, True]]) assert wcs.world_axis_object_components == [('celestial', 1, 'spherical.lat'), ('spectral', 0, 'value'), ('celestial', 0, 'spherical.lon')] assert wcs.world_axis_object_classes['celestial'][0] is SkyCoord assert wcs.world_axis_object_classes['celestial'][1] == () assert isinstance(wcs.world_axis_object_classes['celestial'][2]['frame'], Galactic) assert wcs.world_axis_object_classes['celestial'][2]['unit'] == (u.deg, u.deg) assert wcs.world_axis_object_classes['spectral'][0] is Quantity assert wcs.world_axis_object_classes['spectral'][1] == () assert wcs.world_axis_object_classes['spectral'][2] == {'unit': 'Hz'} assert_allclose(wcs.pixel_to_world_values(24, 39, 44), (10, 20, 25)) assert_allclose(wcs.array_index_to_world_values(44, 39, 24), (10, 20, 25)) assert_allclose(wcs.world_to_pixel_values(10, 20, 25), (24., 39., 44.)) assert_equal(wcs.world_to_array_index_values(10, 20, 25), (44, 39, 24)) assert_equal(wcs.pixel_bounds, [(-6, 30), (-2, 45), (5, 50)]) assert str(wcs) == repr(wcs) == EXPECTED_CELESTIAL_RANGE_REPR.strip() EXPECTED_NO_SHAPE_REPR = """ SlicedLowLevelWCS Transformation This transformation has 3 pixel and 3 world dimensions Array shape (Numpy order): None Pixel Dim Axis Name Data size Bounds 0 None None None 1 None None None 2 None None None World Dim Axis Name Physical Type Units 0 Latitude pos.galactic.lat deg 1 Frequency em.freq Hz 2 Longitude pos.galactic.lon deg Correlation between pixel and world axes: Pixel Dim World Dim 0 1 2 0 yes no yes 1 no yes no 2 yes no yes """ def test_no_array_shape(gwcs_3d_galactic_spectral): gwcs_3d_galactic_spectral.pixel_shape = None gwcs_3d_galactic_spectral.array_shape = None gwcs_3d_galactic_spectral.forward_transform.bounding_box = None wcs = SlicedLowLevelWCS(gwcs_3d_galactic_spectral, Ellipsis) assert wcs.pixel_n_dim == 3 assert wcs.world_n_dim == 3 assert wcs.array_shape is None assert wcs.pixel_shape is None assert wcs.world_axis_physical_types == ['pos.galactic.lat', 'em.freq', 'pos.galactic.lon'] assert wcs.world_axis_units == ['deg', 'Hz', 'deg'] assert_equal(wcs.axis_correlation_matrix, [[True, False, True], [False, True, False], [True, False, True]]) assert wcs.world_axis_object_components == [('celestial', 1, 'spherical.lat'), ('spectral', 0, 'value'), ('celestial', 0, 'spherical.lon')] assert wcs.world_axis_object_classes['celestial'][0] is SkyCoord assert wcs.world_axis_object_classes['celestial'][1] == () assert isinstance(wcs.world_axis_object_classes['celestial'][2]['frame'], Galactic) assert wcs.world_axis_object_classes['celestial'][2]['unit'] == (u.deg, u.deg) assert wcs.world_axis_object_classes['spectral'][0] is Quantity assert wcs.world_axis_object_classes['spectral'][1] == () assert wcs.world_axis_object_classes['spectral'][2] == {'unit': 'Hz'} assert_allclose(wcs.pixel_to_world_values(29, 39, 44), (10, 20, 25)) assert_allclose(wcs.array_index_to_world_values(44, 39, 29), (10, 20, 25)) assert_allclose(wcs.world_to_pixel_values(10, 20, 25), (29., 39., 44.)) assert_equal(wcs.world_to_array_index_values(10, 20, 25), (44, 39, 29)) assert str(wcs) == repr(wcs) == EXPECTED_NO_SHAPE_REPR.strip() # Testing the WCS object having some physical types as None/Unknown EXPECTED_ELLIPSIS_REPR_NONE_TYPES = """ SlicedLowLevelWCS Transformation This transformation has 3 pixel and 3 world dimensions Array shape (Numpy order): (30, 20, 10) Pixel Dim Axis Name Data size Bounds 0 None 10 (-1, 35) 1 None 20 (-2, 45) 2 None 30 (5, 50) World Dim Axis Name Physical Type Units 0 Latitude pos.galactic.lat deg 1 Frequency None Hz 2 Longitude pos.galactic.lon deg Correlation between pixel and world axes: Pixel Dim World Dim 0 1 2 0 yes no yes 1 no yes no 2 yes no yes """ def test_ellipsis_none_types(gwcs_3d_galactic_spectral): pht = list(gwcs_3d_galactic_spectral.output_frame._axis_physical_types) pht[1] = None gwcs_3d_galactic_spectral.output_frame._axis_physical_types = tuple(pht) wcs = SlicedLowLevelWCS(gwcs_3d_galactic_spectral, Ellipsis) assert wcs.pixel_n_dim == 3 assert wcs.world_n_dim == 3 assert wcs.array_shape == (30, 20, 10) assert wcs.pixel_shape == (10, 20, 30) assert wcs.world_axis_physical_types == ['pos.galactic.lat', None, 'pos.galactic.lon'] assert wcs.world_axis_units == ['deg', 'Hz', 'deg'] assert_equal(wcs.axis_correlation_matrix, [[True, False, True], [False, True, False], [True, False, True]]) assert wcs.world_axis_object_components == [('celestial', 1, 'spherical.lat'), ('spectral', 0, 'value'), ('celestial', 0, 'spherical.lon')] assert wcs.world_axis_object_classes['celestial'][0] is SkyCoord assert wcs.world_axis_object_classes['celestial'][1] == () assert isinstance(wcs.world_axis_object_classes['celestial'][2]['frame'], Galactic) assert wcs.world_axis_object_classes['celestial'][2]['unit'] == (u.deg, u.deg) assert_allclose(wcs.pixel_to_world_values(29, 39, 44), (10, 20, 25)) assert_allclose(wcs.array_index_to_world_values(44, 39, 29), (10, 20, 25)) assert_allclose(wcs.world_to_pixel_values(10, 20, 25), (29., 39., 44.)) assert_equal(wcs.world_to_array_index_values(10, 20, 25), (44, 39, 29)) assert_equal(wcs.pixel_bounds, [(-1, 35), (-2, 45), (5, 50)]) assert str(wcs) == repr(wcs) == EXPECTED_ELLIPSIS_REPR_NONE_TYPES.strip() gwcs-0.12.0/gwcs/tests/test_coordinate_systems.py0000644000732200020070000003324513600426757024361 0ustar denchevaSTSCI\science00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst import pytest import numpy as np from numpy.testing import assert_allclose import astropy.units as u from astropy.time import Time from astropy import coordinates as coord from astropy.tests.helper import assert_quantity_allclose from .. import coordinate_frames as cf import astropy astropy_version = astropy.__version__ coord_frames = coord.builtin_frames.__all__[:] # Need to write a better test, using a dict {coord_frame: input_parameters} # For now remove OffsetFrame, issue #55 try: coord_frames.remove("SkyOffsetFrame") except ValueError: pass icrs = cf.CelestialFrame(reference_frame=coord.ICRS(), axes_order=(0, 1)) detector = cf.Frame2D(name='detector', axes_order=(0, 1)) focal = cf.Frame2D(name='focal', axes_order=(0, 1), unit=(u.m, u.m)) spec1 = cf.SpectralFrame(name='freq', unit=[u.Hz, ], axes_order=(2, )) spec2 = cf.SpectralFrame(name='wave', unit=[u.m, ], axes_order=(2, ), axes_names=('lambda', )) spec3 = cf.SpectralFrame(name='energy', unit=[u.J, ], axes_order=(2, )) spec4 = cf.SpectralFrame(name='pixel', unit=[u.pix, ], axes_order=(2, )) spec5 = cf.SpectralFrame(name='speed', unit=[u.m/u.s, ], axes_order=(2, )) comp1 = cf.CompositeFrame([icrs, spec1]) comp2 = cf.CompositeFrame([focal, spec2]) comp3 = cf.CompositeFrame([icrs, spec3]) comp4 = cf.CompositeFrame([icrs, spec4]) comp5 = cf.CompositeFrame([icrs, spec5]) comp = cf.CompositeFrame([comp1, cf.SpectralFrame(axes_order=(3,), unit=(u.m,))]) xscalar = 1 yscalar = 2 xarr = np.arange(5) yarr = np.arange(5) inputs2 = [(xscalar, yscalar), (xarr, yarr)] inputs1 = [xscalar, xarr] inputs3 = [(xscalar, yscalar, xscalar), (xarr, yarr, xarr)] def test_units(): assert(comp1.unit == (u.deg, u.deg, u.Hz)) assert(comp2.unit == (u.m, u.m, u.m)) assert(comp3.unit == (u.deg, u.deg, u.J)) assert(comp4.unit == (u.deg, u.deg, u.pix)) assert(comp5.unit == (u.deg, u.deg, u.m/u.s)) assert(comp.unit == (u.deg, u.deg, u.Hz, u.m)) @pytest.mark.parametrize('inputs', inputs2) def test_coordinates_spatial(inputs): sky_coo = icrs.coordinates(*inputs) assert isinstance(sky_coo, coord.SkyCoord) assert_allclose((sky_coo.ra.value, sky_coo.dec.value), inputs) focal_coo = focal.coordinates(*inputs) assert_allclose([coo.value for coo in focal_coo], inputs) assert [coo.unit for coo in focal_coo] == [u.m, u.m] @pytest.mark.parametrize('inputs', inputs1) def test_coordinates_spectral(inputs): wave = spec2.coordinates(inputs) assert_allclose(wave.value, inputs) assert wave.unit == 'meter' assert isinstance(wave, u.Quantity) @pytest.mark.parametrize('inputs', inputs3) def test_coordinates_composite(inputs): frame = cf.CompositeFrame([icrs, spec2]) result = frame.coordinates(*inputs) assert isinstance(result[0], coord.SkyCoord) assert_allclose((result[0].ra.value, result[0].dec.value), inputs[:2]) assert_allclose(result[1].value, inputs[2]) @pytest.mark.parametrize(('frame'), coord_frames) def test_celestial_attributes_length(frame): """ Test getting default values for CelestialFrame attributes from reference_frame. """ fr = getattr(coord, frame) if issubclass(fr.__class__, coord.BaseCoordinateFrame): cel = cf.CelestialFrame(reference_frame=fr()) assert(len(cel.axes_names) == len(cel.axes_type) == len(cel.unit) == \ len(cel.axes_order) == cel.naxes) def test_axes_type(): assert(icrs.axes_type == ('SPATIAL', 'SPATIAL')) assert(spec1.axes_type == ('SPECTRAL',)) assert(detector.axes_type == ('SPATIAL', 'SPATIAL')) assert(focal.axes_type == ('SPATIAL', 'SPATIAL')) def test_length_attributes(): with pytest.raises(ValueError): cf.CoordinateFrame(naxes=2, unit=(u.deg), axes_type=("SPATIAL", "SPATIAL"), axes_order=(0, 1)) with pytest.raises(ValueError): cf.CoordinateFrame(naxes=2, unit=(u.deg, u.deg), axes_type=("SPATIAL",), axes_order=(0, 1)) with pytest.raises(ValueError): cf.CoordinateFrame(naxes=2, unit=(u.deg, u.deg), axes_type=("SPATIAL", "SPATIAL"), axes_order=(0,)) def test_base_coordinate(): frame = cf.CoordinateFrame(naxes=2, axes_type=("SPATIAL", "SPATIAL"), axes_order=(0, 1)) assert frame.name == 'CoordinateFrame' frame = cf.CoordinateFrame(name="CustomFrame", naxes=2, axes_type=("SPATIAL", "SPATIAL"), axes_order=(0, 1)) assert frame.name == 'CustomFrame' frame.name = "DeLorean" assert frame.name == 'DeLorean' q1, q2 = frame.coordinate_to_quantity(12 * u.deg, 3 * u.arcsec) assert_quantity_allclose(q1, 12 * u.deg) assert_quantity_allclose(q2, 3 * u.arcsec) def test_temporal_relative(): t = cf.TemporalFrame(reference_frame=Time("2018-01-01T00:00:00"), unit=u.s) assert t.coordinates(10) == Time("2018-01-01T00:00:00") + 10 * u.s assert t.coordinates(10 * u.s) == Time("2018-01-01T00:00:00") + 10 * u.s a = t.coordinates((10, 20)) assert a[0] == Time("2018-01-01T00:00:00") + 10 * u.s assert a[1] == Time("2018-01-01T00:00:00") + 20 * u.s t = cf.TemporalFrame(reference_frame=Time("2018-01-01T00:00:00")) assert t.coordinates(10 * u.s) == Time("2018-01-01T00:00:00") + 10 * u.s a = t.coordinates((10, 20) * u.s) assert a[0] == Time("2018-01-01T00:00:00") + 10 * u.s assert a[1] == Time("2018-01-01T00:00:00") + 20 * u.s @pytest.mark.skipif(astropy_version<"4", reason="Requires astropy 4.0 or higher") def test_temporal_absolute(): t = cf.TemporalFrame(reference_frame=Time([], format='isot')) assert t.coordinates("2018-01-01T00:00:00") == Time("2018-01-01T00:00:00") a = t.coordinates(("2018-01-01T00:00:00", "2018-01-01T00:10:00")) assert a[0] == Time("2018-01-01T00:00:00") assert a[1] == Time("2018-01-01T00:10:00") t = cf.TemporalFrame(reference_frame=Time([], scale='tai', format='isot')) assert t.coordinates("2018-01-01T00:00:00") == Time("2018-01-01T00:00:00", scale='tai') @pytest.mark.parametrize('inp', [ (10 * u.deg, 20 * u.deg), ((10 * u.deg, 20 * u.deg),), (u.Quantity([10, 20], u.deg),), (coord.SkyCoord(10 * u.deg, 20 * u.deg, frame=coord.ICRS),), # This is the same as 10,20 in ICRS (coord.SkyCoord(119.26936774, -42.79039286, unit=u.deg, frame='galactic'),) ]) def test_coordinate_to_quantity_celestial(inp): cel = cf.CelestialFrame(reference_frame=coord.ICRS(), axes_order=(0, 1)) lon, lat = cel.coordinate_to_quantity(*inp) assert_quantity_allclose(lon, 10 * u.deg) assert_quantity_allclose(lat, 20 * u.deg) with pytest.raises(ValueError): cel.coordinate_to_quantity(10 * u.deg, 2 * u.deg, 3 * u.deg) with pytest.raises(ValueError): cel.coordinate_to_quantity((1, 2)) @pytest.mark.parametrize('inp', [ (100,), (100 * u.nm,), (0.1 * u.um,), ]) def test_coordinate_to_quantity_spectral(inp): spec = cf.SpectralFrame(unit=u.nm, axes_order=(1, )) wav = spec.coordinate_to_quantity(*inp) assert_quantity_allclose(wav, 100 * u.nm) @pytest.mark.parametrize('inp', [ (Time("2011-01-01T00:00:10"),), (10 * u.s,) ]) @pytest.mark.skipif(astropy_version<"4", reason="Requires astropy 4.0 or higher.") def test_coordinate_to_quantity_temporal(inp): temp = cf.TemporalFrame(reference_frame=Time("2011-01-01T00:00:00"), unit=u.s) t = temp.coordinate_to_quantity(*inp) assert_quantity_allclose(t, 10 * u.s) temp2 = cf.TemporalFrame(reference_frame=Time([], format='isot'), unit=u.s) tt = Time("2011-01-01T00:00:00") t = temp2.coordinate_to_quantity(tt) assert t is tt @pytest.mark.parametrize('inp', [ (211 * u.AA, 0 * u.s, 0 * u.arcsec, 0 * u.arcsec), (211 * u.AA, 0 * u.s, (0 * u.arcsec, 0 * u.arcsec)), (211 * u.AA, 0 * u.s, (0, 0) * u.arcsec), (211 * u.AA, Time("2011-01-01T00:00:00"), (0, 0) * u.arcsec), (211 * u.AA, Time("2011-01-01T00:00:00"), coord.SkyCoord(0, 0, unit=u.arcsec)), ]) def test_coordinate_to_quantity_composite(inp): # Composite wave_frame = cf.SpectralFrame(axes_order=(0, ), unit=u.AA) time_frame = cf.TemporalFrame( axes_order=(1, ), unit=u.s, reference_frame=Time("2011-01-01T00:00:00")) sky_frame = cf.CelestialFrame(axes_order=(2, 3), reference_frame=coord.ICRS()) comp = cf.CompositeFrame([wave_frame, time_frame, sky_frame]) coords = comp.coordinate_to_quantity(*inp) expected = (211 * u.AA, 0 * u.s, 0 * u.arcsec, 0 * u.arcsec) for output, exp in zip(coords, expected): assert_quantity_allclose(output, exp) def test_stokes_frame(): sf = cf.StokesFrame() assert sf.coordinates(0) == 'I' assert sf.coordinates(0 * u.pix) == 'I' assert sf.coordinate_to_quantity('I') == 0 * u.one assert sf.coordinate_to_quantity(0) == 0 @pytest.mark.parametrize('inp', [ (211 * u.AA, 0 * u.s, 0 * u.one, 0 * u.one), (211 * u.AA, 0 * u.s, (0 * u.one, 0 * u.one)), (211 * u.AA, 0 * u.s, (0, 0) * u.one), (211 * u.AA, Time("2011-01-01T00:00:00"), (0, 0) * u.one) ]) def test_coordinate_to_quantity_frame2d_composite(inp): wave_frame = cf.SpectralFrame(axes_order=(0, ), unit=u.AA) time_frame = cf.TemporalFrame( axes_order=(1, ), unit=u.s, reference_frame=Time("2011-01-01T00:00:00")) frame2d = cf.Frame2D(name="intermediate", axes_order=(2, 3), unit=(u.one, u.one)) comp = cf.CompositeFrame([wave_frame, time_frame, frame2d]) coords = comp.coordinate_to_quantity(*inp) expected = (211 * u.AA, 0 * u.s, 0 * u.one, 0 * u.one) for output, exp in zip(coords, expected): assert_quantity_allclose(output, exp) def test_coordinate_to_quantity_frame_2d(): frame = cf.Frame2D(unit=(u.one, u.arcsec)) inp = (1, 2) expected = (1 * u.one, 2 * u.arcsec) result = frame.coordinate_to_quantity(*inp) for output, exp in zip(result, expected): assert_quantity_allclose(output, exp) inp = (1 * u.one, 2) expected = (1 * u.one, 2 * u.arcsec) result = frame.coordinate_to_quantity(*inp) for output, exp in zip(result, expected): assert_quantity_allclose(output, exp) @pytest.mark.skipif(astropy_version<"4", reason="Requires astropy 4.0 or higher.") def test_coordinate_to_quantity_error(): frame = cf.Frame2D(unit=(u.one, u.arcsec)) with pytest.raises(ValueError): frame.coordinate_to_quantity(1) with pytest.raises(ValueError): comp1.coordinate_to_quantity((1, 1), 2) frame = cf.TemporalFrame(reference_frame=Time([], format='isot'), unit=u.s) with pytest.raises(ValueError): frame.coordinate_to_quantity(1) def test_axis_physical_type(): assert icrs.axis_physical_types == ("pos.eq.ra", "pos.eq.dec") assert spec1.axis_physical_types == ("em.freq",) assert spec2.axis_physical_types == ("em.wl",) assert spec3.axis_physical_types == ("em.energy",) assert spec4.axis_physical_types == ("custom:unknown",) assert spec5.axis_physical_types == ("spect.dopplerVeloc",) assert comp1.axis_physical_types == ("pos.eq.ra", "pos.eq.dec", "em.freq") assert comp2.axis_physical_types == ("custom:x", "custom:y", "em.wl") assert comp3.axis_physical_types == ("pos.eq.ra", "pos.eq.dec", "em.energy") assert comp.axis_physical_types == ('pos.eq.ra', 'pos.eq.dec', 'em.freq', 'em.wl') spec6 = cf.SpectralFrame(name='waven', axes_order=(1,), axis_physical_types='em.wavenumber') assert spec6.axis_physical_types == ('em.wavenumber',) t = cf.TemporalFrame(reference_frame=Time("2018-01-01T00:00:00"), unit=u.s) assert t.axis_physical_types == ('time',) fr2d = cf.Frame2D(name='d', axes_names=("x", "y")) assert fr2d.axis_physical_types == ('custom:x', 'custom:y') fr2d = cf.Frame2D(name='d', axes_names=None) assert fr2d.axis_physical_types == ('custom:SPATIAL', 'custom:SPATIAL') fr2d = cf.Frame2D(name='d', axis_physical_types=("pos.x", "pos.y")) assert fr2d.axis_physical_types == ('custom:pos.x', 'custom:pos.y') with pytest.raises(ValueError): cf.CelestialFrame(reference_frame=coord.ICRS(), axis_physical_types=("pos.eq.ra",)) fr = cf.CelestialFrame(reference_frame=coord.ICRS(), axis_physical_types=("ra", "dec")) assert fr.axis_physical_types == ("custom:ra", "custom:dec") fr = cf.CelestialFrame(reference_frame=coord.BarycentricTrueEcliptic()) assert fr.axis_physical_types == ('pos.ecliptic.lon', 'pos.ecliptic.lat') frame = cf.CoordinateFrame(name='custom_frame', axes_type=("SPATIAL",), axes_order=(0,), axis_physical_types="length", axes_names="x", naxes=1) assert frame.axis_physical_types == ("custom:length",) frame = cf.CoordinateFrame(name='custom_frame', axes_type=("SPATIAL",), axes_order=(0,), axis_physical_types=("length",), axes_names="x", naxes=1) assert frame.axis_physical_types == ("custom:length",) with pytest.raises(ValueError): cf.CoordinateFrame(name='custom_frame', axes_type=("SPATIAL",), axes_order=(0,), axis_physical_types=("length", "length"), naxes=1) def test_base_frame(): with pytest.raises(ValueError): cf.CoordinateFrame(name='custom_frame', axes_type=("SPATIAL",), naxes=1, axes_order=(0,), axes_names=("x", "y")) frame = cf.CoordinateFrame(name='custom_frame', axes_type=("SPATIAL",), axes_order=(0,), axes_names="x", naxes=1) assert frame.naxes == 1 assert frame.axes_names == ("x",) frame.coordinate_to_quantity(1, 2) gwcs-0.12.0/gwcs/tests/test_region.py0000644000732200020070000002163113600426757021722 0ustar denchevaSTSCI\science00000000000000 # Licensed under a 3-clause BSD style license - see LICENSE.rst """ Test regions """ import warnings import numpy as np from numpy.testing import assert_equal, assert_allclose from astropy.modeling import models import pytest from .. import region, selector from .. import utils as gwutils def test_LabelMapperArray_from_vertices_int(): regions = {1: [[795, 970], [2047, 970], [2047, 999], [795, 999], [795, 970]], 2: [[844, 1067], [2047, 1067], [2047, 1113], [844, 1113], [844, 1067]], 3: [[654, 1029], [2047, 1029], [2047, 1078], [654, 1078], [654, 1029]], 4: [[772, 990], [2047, 990], [2047, 1042], [772, 1042], [772, 990]] } mask = selector.LabelMapperArray.from_vertices((2400, 2400), regions) labels = list(regions.keys()) labels.append(0) mask_labels = np.unique(mask.mapper).tolist() assert(np.sort(labels) == np.sort(mask_labels)).all() def test_LabelMapperArray_from_vertices_string(): regions = {'S1600A1': [[795, 970], [2047, 970], [2047, 999], [795, 999], [795, 970]], 'S200A1': [[844, 1067], [2047, 1067], [2047, 1113], [844, 1113], [844, 1067]], 'S200A2': [[654, 1029], [2047, 1029], [2047, 1078], [654, 1078], [654, 1029]], 'S400A1': [[772, 990], [2047, 990], [2047, 1042], [772, 1042], [772, 990]] } mask = selector.LabelMapperArray.from_vertices((1400, 1400), regions) labels = list(regions.keys()) labels.append('') mask_labels = np.unique(mask.mapper).tolist() assert(np.sort(labels) == np.sort(mask_labels)).all() # These tests below check the scanning algorithm for two shapes def polygon1(shape=(9, 9)): ar = np.zeros(shape) ar[1, 2] = 1 ar[2][2:4] = 1 ar[3][1:4] = 1 ar[4][:4] = 1 ar[5][1:4] = 1 ar[6][2:7] = 1 ar[7][3:6] = 1 # ar[8][3:4] =1 ##need to include this in the future if padding top and left return ar def two_polygons(): ar = np.zeros((301, 301)) ar[1, 2] = 1 ar[2][2:4] = 1 ar[3][1:4] = 1 ar[4][:4] = 1 ar[5][1:4] = 1 ar[6][2:7] = 1 ar[7][3:6] = 1 ar[:30, 10:31] = 2 return ar def test_polygon1(): vert = [(2, 1), (3, 5), (6, 6), (3, 8), (0, 4), (2, 1)] pol = region.Polygon('1', vert) mask = np.zeros((9, 9), dtype=np.int) mask = pol.scan(mask) pol1 = polygon1() assert_equal(mask, pol1) def test_create_mask_two_polygons(): vertices = {1: [[2, 1], [3, 5], [6, 6], [3, 8], [0, 4], [2, 1]], 2: [[10, 0], [30, 0], [30, 30], [10, 30], [10, 0]]} mask = selector.LabelMapperArray.from_vertices((301, 301), vertices) pol2 = two_polygons() assert_equal(mask.mapper, pol2) def create_range_mapper(): m = [] for i in np.arange(1, 10) * .1: c0_0, c1_0, c0_1, c1_1 = np.ones((4,)) * i m.append(models.Polynomial2D(2, c0_0=c0_0, c1_0=c1_0, c0_1=c0_1, c1_1=c1_1)) keys = np.array([[4.88, 5.64], [5.75, 6.5], [6.67, 7.47], [7.7, 8.63], [8.83, 9.96], [10.19, 11.49], [11.77, 13.28], [13.33, 15.34], [15.56, 18.09]]) rmapper = {} for k, v in zip(keys, m): rmapper[tuple(k)] = v sel = selector.LabelMapperRange(('x', 'y'), rmapper, inputs_mapping=models.Mapping((0,), n_inputs=2)) return sel def create_scalar_mapper(): m = [] for i in np.arange(5) * .1: c0_0, c1_0, c0_1, c1_1 = np.ones((4,)) * i m.append(models.Polynomial2D(2, c0_0=c0_0, c1_0=c1_0, c0_1=c0_1, c1_1=c1_1)) keys = [-1.95805483, -1.67833272, -1.39861060, -1.11888848, -8.39166358] dmapper = {} for k, v in zip(keys, m): dmapper[k] = v return dmapper def test_LabelMapperDict(): dmapper = create_scalar_mapper() sel = selector.LabelMapperDict(('x', 'y'), dmapper, atol=10**-3, inputs_mapping=models.Mapping((0,), n_inputs=2)) assert(sel(-1.9580, 2) == dmapper[-1.95805483](-1.95805483, 2)) with pytest.raises(TypeError): selector.LabelMapperDict(('x', 'y'), mapper={1: models.Rotation2D(23), 2: models.Shift(1) } ) def test_LabelMapperRange(): sel = create_range_mapper() assert(sel(6, 2) == 4.2) with pytest.raises(TypeError): selector.LabelMapperRange(('x', 'y'), mapper={(1, 5): models.Rotation2D(23), (7, 10): models.Shift(1) } ) def test_LabelMapper(): transform = models.Const1D(12.3) lm = selector.LabelMapper(inputs=('x', 'y'), mapper=transform, inputs_mapping=(1,)) x = np.linspace(3, 11, 20) assert_allclose(lm(x, x), transform(x)) def test_LabelMapperArray(): regions = np.arange(25).reshape(5, 5) array_mapper = selector.LabelMapperArray(mapper=regions) with pytest.raises(selector.LabelMapperArrayIndexingError): array_mapper(7, 1) # test the first and last element assert_equal(array_mapper(0, 0), 0) assert_equal(array_mapper(-1, -1), 24) @pytest.mark.filterwarnings("ignore:The input positions are not") def test_RegionsSelector(): labels = np.zeros((10, 10)) labels[1, 2] = 1 labels[2][2: 4] = 1 labels[3][1: 4] = 1 labels[4][: 4] = 1 labels[5][1: 4] = 1 labels[6][2: 7] = 1 labels[7][3: 6] = 1 labels[:, -2:] = 2 mapper = selector.LabelMapperArray(labels) sel = {1: models.Shift(1) & models.Scale(1), 2: models.Shift(2) & models.Scale(2) } with pytest.raises(ValueError): # 0 can't be a key in ``selector`` selector.RegionsSelector(inputs=('x', 'y'), outputs=('x', 'y'), label_mapper=mapper, selector={0: models.Shift(1) & models.Scale(1), 2: models.Shift(2) & models.Scale(2) } ) reg_selector = selector.RegionsSelector(inputs=('x', 'y'), outputs=('x', 'y'), label_mapper=mapper, selector=sel ) with pytest.raises(NotImplementedError): reg_selector.inverse mapper.inverse = mapper.copy() assert_allclose(reg_selector(2, 1), sel[1](2, 1)) assert_allclose(reg_selector(8, 2), sel[2](8, 2)) # test set_input with pytest.raises(gwutils.RegionError): reg_selector.set_input(3) transform = reg_selector.set_input(2) assert_equal(transform.parameters, [2, 2]) assert_allclose(transform(1, 1), sel[2](1, 1)) # test inverse rsinv = reg_selector.inverse # The label_mapper arays should be the same assert_equal(reg_selector.label_mapper.mapper, rsinv.label_mapper.mapper) # the transforms of the inverse ``RegionsSelector`` should be the inverse of the # transforms of the ``RegionsSelector`` model. x = np.linspace(-5, 5, 100) assert_allclose(reg_selector.selector[1].inverse(x, x), rsinv.selector[1](x, x)) assert_allclose(reg_selector.selector[2].inverse(x, x), rsinv.selector[2](x, x)) assert np.isnan(reg_selector(0, 0)).all() # Test setting ``undefined_transform_value`` to a non-default value. reg_selector.undefined_transform_value = -100 assert_equal(reg_selector(0, 0), [-100, -100]) def test_overalpping_ranges(): """ Initializing a ``LabelMapperRange`` with overlapping ranges should raise an error. """ keys = np.array([[4.88, 5.75], [5.64, 6.5], [6.67, 7.47]]) rmapper = {} for key in keys: rmapper[tuple(key)] = models.Const1D(4) with pytest.raises(ValueError): selector.LabelMapperRange(('x', 'y'), rmapper, inputs_mapping=((0,))) def test_outside_range(): """ Return ``_no_label`` value when keys are outside the range. """ warnings.filterwarnings("ignore", message="^All data is outside the valid range") lmr = create_range_mapper() assert lmr(1, 1) == 0 assert lmr(5, 1) == 1.2 def test_unique_labels(): labels = (np.arange(10) * np.ones((1023, 10))).T np.random.shuffle(labels) expected = np.arange(1, 10) result = selector.get_unique_regions(labels) assert_equal(expected, result) assert 0 not in result labels = ["S100A1", "S200A2", "S400A1", "S1600", "S200B1", "", ] * 1000 np.random.shuffle(labels) expected = ['S100A1', 'S1600', 'S200A2', 'S200B1', 'S400A1'] result = selector.get_unique_regions(labels) assert_equal(expected, result) gwcs-0.12.0/gwcs/tests/test_spectroscopy_models.py0000644000732200020070000000565213600426757024544 0ustar denchevaSTSCI\science00000000000000import pytest import astropy.units as u from astropy.modeling.models import Identity import numpy as np from numpy.testing import assert_allclose from .. import spectroscopy as sp# noqa from .. import geometry# noqa def test_angles_grating_equation(): """ Test agaibst the Nispec implementation.""" lam = np.array([2e-6] * 4) alpha_in = np.linspace(.01, .05, 4) model = sp.AnglesFromGratingEquation3D(20000, -1) # Eq. from Nirspec model. xout = -alpha_in - (model.groove_density * model.spectral_order * lam) alpha_out, beta_out, gamma_out = model(lam, -alpha_in, alpha_in) assert_allclose(alpha_out, xout) assert_allclose(beta_out, -alpha_in) assert_allclose(gamma_out, np.sqrt(1 - alpha_out**2 - beta_out**2)) # Now with units model = sp.AnglesFromGratingEquation3D(20000 * 1/u.m, -1) # Eq. from Nirspec model. xout = -alpha_in - (model.groove_density * model.spectral_order * lam * u.m) alpha_out, beta_out, gamma_out = model(lam * u.m, -u.Quantity(alpha_in), u.Quantity(alpha_in)) assert_allclose(alpha_out, xout) assert_allclose(beta_out, -alpha_in) assert_allclose(gamma_out, np.sqrt(1 - alpha_out**2 - beta_out**2)) def test_wavelength_grating_equation_units(): alpha_in = np.linspace(.01, .05, 4) model = sp.WavelengthFromGratingEquation(20000, -1) # Eq. from Nirspec model. wave = -(alpha_in + alpha_in) / (20000 * -1) result = model(-alpha_in, -alpha_in) assert_allclose(result, wave) # Now with units model = sp.WavelengthFromGratingEquation(20000 * 1/u.m, -1) # Eq. from Nirspec model. wave = -(u.Quantity(alpha_in) + u.Quantity(alpha_in)) / (20000 * 1/u.m * -1) result = model(-u.Quantity(alpha_in), -u.Quantity(alpha_in)) assert_allclose(result, wave) @pytest.mark.parametrize(('wavelength', 'n'), [(1, 1.43079543), (2, 1.42575377), (5, 1.40061966) ]) def test_SellmeierGlass(wavelength, n, sellmeier_glass): """ Test from Nirspec team. Wavelength is in microns. """ n_result = sellmeier_glass(wavelength) assert_allclose(n_result, n) def test_SellmeierZemax(sellmeier_zemax): """ The data for this test come from Nirspec.""" n = 1.4254647475849418 assert_allclose(sellmeier_zemax(2), n) def test_Snell3D(sellmeier_glass): """ Test from Nirspec.""" expected = (0.07015255913513296, 0.07015255913513296, 0.9950664484814988) model = sp.Snell3D() n = 1.4254647475849418 assert_allclose(model(n, .1, .1, .9), expected) def test_snell_sellmeier_combined(sellmeier_glass): fromdircos = geometry.FromDirectionCosines() todircos = geometry.ToDirectionCosines() model = sellmeier_glass & todircos | sp.Snell3D() & Identity(1) | fromdircos expected = (0.07013833805527926, 0.07013833805527926, 1.0050677723764139) assert_allclose(model(2, .1, .1, .9), expected) gwcs-0.12.0/gwcs/tests/test_utils.py0000644000732200020070000000777013600426757021607 0ustar denchevaSTSCI\science00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst import numpy as np from astropy.io import fits from astropy import wcs as fitswcs from astropy import units as u from astropy import coordinates as coord from astropy.modeling import models from astropy.utils.data import get_pkg_data_filename from astropy.tests.helper import assert_quantity_allclose import pytest from numpy.testing import assert_allclose from .. import utils as gwutils from ..utils import UnsupportedProjectionError def test_wrong_projcode(): with pytest.raises(UnsupportedProjectionError): ctype = {"CTYPE": ["RA---TAM", "DEC--TAM"]} gwutils.get_projcode(ctype) def test_wrong_projcode2(): with pytest.raises(UnsupportedProjectionError): gwutils.create_projection_transform("TAM") def test_fits_transform(): hdr = fits.Header.fromfile(get_pkg_data_filename('data/simple_wcs2.hdr')) gw1 = gwutils.make_fitswcs_transform(hdr) w1 = fitswcs.WCS(hdr) assert_allclose(gw1(1, 2), w1.wcs_pix2world(1, 2, 1), atol=10 ** -8) def test_lon_pole(): tan = models.Pix2Sky_TAN() car = models.Pix2Sky_CAR() sky_positive_lat = coord.SkyCoord(3 * u.deg, 1 * u.deg) sky_negative_lat = coord.SkyCoord(3 * u.deg, -1 * u.deg) assert_quantity_allclose(gwutils._compute_lon_pole(sky_positive_lat, tan), 180 * u.deg) assert_quantity_allclose(gwutils._compute_lon_pole(sky_negative_lat, tan), 180 * u.deg) assert_quantity_allclose(gwutils._compute_lon_pole(sky_positive_lat, car), 0 * u.deg) assert_quantity_allclose(gwutils._compute_lon_pole(sky_negative_lat, car), 180 * u.deg) assert_quantity_allclose(gwutils._compute_lon_pole((0, 34 * u.rad), tan), 180 * u.deg) assert_allclose(gwutils._compute_lon_pole((1, -34), tan), 180) def test_unknown_ctype(): wcsinfo = {'CDELT': np.array([3.61111098e-05, 3.61111098e-05, 2.49999994e-03]), 'CRPIX': np.array([17., 16., 1.]), 'CRVAL': np.array([4.49999564e+01, 1.72786731e-04, 4.84631542e+00]), 'CTYPE': np.array(['MRSAL1A', 'MRSBE1A', 'WAVE']), 'CUNIT': np.array([u.Unit("deg"), u.Unit("deg"), u.Unit("um")], dtype=object), 'PC': np.array([[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]]), 'WCSAXES': 3, 'has_cd': False } transform = gwutils.make_fitswcs_transform(wcsinfo) x = np.linspace(-5, 7, 10) y = np.linspace(-5, 7, 10) expected = (np.array([-0.00079444, -0.0007463, -0.00069815, -0.00065, -0.00060185, -0.0005537, -0.00050556, -0.00045741, -0.00040926, -0.00036111] ), np.array([-0.00075833, -0.00071019, -0.00066204, -0.00061389, -0.00056574, -0.00051759, -0.00046944, -0.0004213, -0.00037315, -0.000325] ) ) a, b = transform(x, y) assert_allclose(a, expected[0], atol=10**-8) assert_allclose(b, expected[1], atol=10**-8) def test_get_axes(): wcsinfo = {'CTYPE': np.array(['MRSAL1A', 'MRSBE1A', 'WAVE'])} cel, spec, other = gwutils.get_axes(wcsinfo) assert not cel assert spec == [2] assert other == [0, 1] wcsinfo = {'CTYPE': np.array(['RA---TAN', 'WAVE', 'DEC--TAN'])} cel, spec, other = gwutils.get_axes(wcsinfo) assert cel == [0, 2] assert spec == [1] assert not other def test_isnumerical(): sky = coord.SkyCoord(1 * u.deg, 2 * u.deg) assert not gwutils.isnumerical(sky) assert not gwutils.isnumerical(2 * u.m) assert gwutils.isnumerical(np.float(0)) assert gwutils.isnumerical(np.array(0)) assert not gwutils.isnumerical(np.array(['s200', '234'])) assert gwutils.isnumerical(np.array(0, dtype='>f8')) assert gwutils.isnumerical(np.array(0, dtype='>i4')) def test_get_values(): args = 2 * u.cm units=(u.m, ) res = gwutils.get_values(units, args) assert res == [.02] res = gwutils.get_values(None, args) assert res == [2] gwcs-0.12.0/gwcs/tests/test_wcs.py0000644000732200020070000004376713600426757021251 0ustar denchevaSTSCI\science00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst import warnings import numpy as np from numpy.testing import assert_allclose, assert_equal from astropy.modeling import models from astropy import coordinates as coord from astropy.io import fits from astropy import units as u import pytest from astropy.utils.data import get_pkg_data_filename from astropy import wcs as astwcs from .. import wcs from ..wcstools import (wcs_from_fiducial, grid_from_bounding_box, wcs_from_points) from .. import coordinate_frames as cf from .. import utils from ..utils import CoordinateFrameError m1 = models.Shift(12.4) & models.Shift(-2) m2 = models.Scale(2) & models.Scale(-2) m = m1 | m2 icrs = cf.CelestialFrame(reference_frame=coord.ICRS(), name='icrs') detector = cf.Frame2D(name='detector', axes_order=(0, 1)) focal = cf.Frame2D(name='focal', axes_order=(0, 1), unit=(u.m, u.m)) spec = cf.SpectralFrame(name='wave', unit=[u.m, ], axes_order=(2, ), axes_names=('lambda', )) pipe = [(detector, m1), (focal, m2), (icrs, None) ] # Create some data. nx, ny = (5, 2) x = np.linspace(0, 1, nx) y = np.linspace(0, 1, ny) xv, yv = np.meshgrid(x, y) # Test initializing a WCS def test_create_wcs(): """ Test initializing a WCS object. """ # use only frame names gw1 = wcs.WCS(output_frame='icrs', input_frame='detector', forward_transform=m) # omit input_frame gw2 = wcs.WCS(output_frame='icrs', forward_transform=m) # use CoordinateFrame objects gw3 = wcs.WCS(output_frame=icrs, input_frame=detector, forward_transform=m) # use a pipeline to initialize pipe = [(detector, m1), (icrs, None)] gw4 = wcs.WCS(forward_transform=pipe) assert(gw1.available_frames == gw2.available_frames == \ gw3.available_frames == gw4.available_frames == ['detector', 'icrs']) res = m(1, 2) assert_allclose(gw1(1, 2), res) assert_allclose(gw2(1, 2), res) assert_allclose(gw3(1, 2), res) assert_allclose(gw3(1, 2), res) def test_init_no_transform(): """ Test initializing a WCS object without a forward_transform. """ gw = wcs.WCS(output_frame='icrs') assert gw._pipeline == [('detector', None), ('icrs', None)] assert np.in1d(gw.available_frames, ['detector', 'icrs']).all() gw = wcs.WCS(output_frame=icrs, input_frame=detector) assert gw._pipeline == [('detector', None), ('icrs', None)] assert np.in1d(gw.available_frames, ['detector', 'icrs']).all() with pytest.raises(NotImplementedError): gw(1, 2) def test_init_no_output_frame(): """ Test initializing a WCS without an output_frame raises an error. """ with pytest.raises(CoordinateFrameError): wcs.WCS(forward_transform=m1) def test_insert_transform(): """ Test inserting a transform.""" gw = wcs.WCS(output_frame='icrs', forward_transform=m1) assert_allclose(gw.forward_transform(1, 2), m1(1, 2)) gw.insert_transform(frame='icrs', transform=m2) assert_allclose(gw.forward_transform(1, 2), (m1 | m2)(1, 2)) def test_set_transform(): """ Test setting a transform between two frames in the pipeline.""" w = wcs.WCS(forward_transform=pipe[:]) w.set_transform('detector', 'focal', models.Identity(2)) assert_allclose(w(1, 1), (2, -2)) with pytest.raises(CoordinateFrameError): w.set_transform('detector1', 'focal', models.Identity(2)) with pytest.raises(CoordinateFrameError): w.set_transform('detector', 'focal1', models.Identity(2)) def test_get_transform(): """ Test getting a transform between two frames in the pipeline.""" w = wcs.WCS(pipe[:]) tr_forward = w.get_transform('detector', 'focal') tr_back = w.get_transform('icrs', 'detector') x, y = 1, 2 fx, fy = tr_forward(1, 2) assert_allclose(w.pipeline[0][1](x, y), (fx, fy)) assert_allclose((x, y), tr_back(*w(x, y))) assert(w.get_transform('detector', 'detector') is None) def test_backward_transform(): """ Test backward transform raises an error when an analytical inverse is not available. """ # Test that an error is raised when one of the models has not inverse. poly = models.Polynomial1D(1, c0=4) w = wcs.WCS(forward_transform=poly & models.Scale(2), output_frame='sky') with pytest.raises(NotImplementedError): w.backward_transform # test backward transform poly.inverse = models.Shift(-4) w = wcs.WCS(forward_transform=poly & models.Scale(2), output_frame='sky') assert_allclose(w.backward_transform(1, 2), (-3, 1)) def test_return_coordinates(): """Test converting to coordinate objects or quantities.""" w = wcs.WCS(pipe[:]) x = 1 y = 2.3 numerical_result = (26.8, -0.6) # Celestial frame num_plus_output = w(x, y, with_units=True) output_quant = w.output_frame.coordinate_to_quantity(num_plus_output) assert_allclose(w(x, y), numerical_result) assert_allclose(utils.get_values(w.unit, *output_quant), numerical_result) assert_allclose(w.invert(num_plus_output), (x, y)) assert isinstance(num_plus_output, coord.SkyCoord) # Spectral frame poly = models.Polynomial1D(1, c0=1, c1=2) w = wcs.WCS(forward_transform=poly, output_frame=spec) numerical_result = poly(y) num_plus_output = w(y, with_units=True) output_quant = w.output_frame.coordinate_to_quantity(num_plus_output) assert_allclose(utils.get_values(w.unit, output_quant), numerical_result) assert isinstance(num_plus_output, u.Quantity) # CompositeFrame - [celestial, spectral] output_frame = cf.CompositeFrame(frames=[icrs, spec]) transform = m1 & poly w = wcs.WCS(forward_transform=transform, output_frame=output_frame) numerical_result = transform(x, y, y) num_plus_output = w(x, y, y, with_units=True) output_quant = w.output_frame.coordinate_to_quantity(*num_plus_output) assert_allclose(utils.get_values(w.unit, *output_quant), numerical_result) def test_from_fiducial_sky(): sky = coord.SkyCoord(1.63 * u.radian, -72.4 * u.deg, frame='fk5') tan = models.Pix2Sky_TAN() w = wcs_from_fiducial(sky, projection=tan) assert isinstance(w.CelestialFrame.reference_frame, coord.FK5) assert_allclose(w(.1, .1), (93.7210280925364, -72.29972666307474)) def test_from_fiducial_composite(): sky = coord.SkyCoord(1.63 * u.radian, -72.4 * u.deg, frame='fk5') tan = models.Pix2Sky_TAN() spec = cf.SpectralFrame(unit=(u.micron,), axes_order=(0,)) celestial = cf.CelestialFrame(reference_frame=sky.frame, unit=(sky.spherical.lon.unit, sky.spherical.lat.unit), axes_order=(1, 2)) coord_frame = cf.CompositeFrame([spec, celestial], name='cube_frame') w = wcs_from_fiducial([.5, sky], coord_frame, projection=tan) assert isinstance(w.cube_frame.frames[1].reference_frame, coord.FK5) assert_allclose(w(1, 1, 1), (1.5, 96.52373368309931, -71.37420187296995)) # test returning coordinate objects with composite output_frame res = w(1, 2, 2, with_units=True) assert_allclose(res[0], u.Quantity(1.5 * u.micron)) assert isinstance(res[1], coord.SkyCoord) assert_allclose(res[1].ra.value, 99.329496642319) assert_allclose(res[1].dec.value, -70.30322020351122) trans = models.Shift(10) & models.Scale(2) & models.Shift(-1) w = wcs_from_fiducial([.5, sky], coord_frame, projection=tan, transform=trans) assert_allclose(w(1, 1, 1), (11.5, 99.97738475762152, -72.29039139739766)) # test coordinate object output coord_result = w(1, 1, 1, with_units=True) assert_allclose(coord_result[0], u.Quantity(11.5 * u.micron)) def test_from_fiducial_frame2d(): fiducial = (34.5, 12.3) w = wcs_from_fiducial(fiducial, coordinate_frame=cf.Frame2D()) assert (w.output_frame.name == 'Frame2D') assert_allclose(w(1, 1), (35.5, 13.3)) def test_bounding_box(): trans3 = models.Shift(10) & models.Scale(2) & models.Shift(-1) pipeline = [('detector', trans3), ('sky', None)] w = wcs.WCS(pipeline) bb = ((-1, 10), (6, 15)) with pytest.raises(ValueError): w.bounding_box = bb trans2 = models.Shift(10) & models.Scale(2) pipeline = [('detector', trans2), ('sky', None)] w = wcs.WCS(pipeline) w.bounding_box = bb assert w.bounding_box == w.forward_transform.bounding_box[::-1] pipeline = [("detector", models.Shift(2)), ("sky", None)] w = wcs.WCS(pipeline) w.bounding_box = (1, 5) assert w.bounding_box == w.forward_transform.bounding_box with pytest.raises(ValueError): w.bounding_box = ((1, 5), (2, 6)) def test_grid_from_bounding_box(): bb = ((-1, 9.9), (6.5, 15)) x, y = grid_from_bounding_box(bb, step=[.1, .5], center=False) assert_allclose(x[:, 0], -1) assert_allclose(x[:, -1], 9.9) assert_allclose(y[0], 6.5) assert_allclose(y[-1], 15) def test_grid_from_bounding_box_1d(): # Test 1D case x = grid_from_bounding_box((-.5, 4.5)) assert_allclose(x, [0., 1., 2., 3., 4.]) def test_grid_from_bounding_box_step(): bb = ((-0.5, 5.5), (-0.5, 4.5)) x, y = grid_from_bounding_box(bb) x1, y1 = grid_from_bounding_box(bb, step=(1, 1)) assert_allclose(x, x1) assert_allclose(y, y1) with pytest.raises(ValueError): grid_from_bounding_box(bb, step=(1, 2, 1)) def test_wcs_from_points(): np.random.seed(0) hdr = fits.Header.fromtextfile(get_pkg_data_filename("data/acs.hdr"), endcard=False) with warnings.catch_warnings() as w: warnings.simplefilter("ignore") w = astwcs.WCS(hdr) y, x = np.mgrid[:2046:20j, :4023:10j] ra, dec = w.wcs_pix2world(x, y, 1) fiducial = coord.SkyCoord(ra.mean()*u.deg, dec.mean()*u.deg, frame="icrs") w = wcs_from_points(xy=(x, y), world_coordinates=(ra, dec), fiducial=fiducial) newra, newdec = w(x, y) assert_allclose(newra, ra) assert_allclose(newdec, dec) n = np.random.randn(ra.size) n.shape = ra.shape nra = n * 10 ** -2 ndec = n * 10 ** -2 w = wcs_from_points(xy=(x + nra, y + ndec), world_coordinates=(ra, dec), fiducial=fiducial) newra, newdec = w(x, y) assert_allclose(newra, ra, atol=10**-6) assert_allclose(newdec, dec, atol=10**-6) def test_grid_from_bounding_box_2(): bb = ((-0.5, 5.5), (-0.5, 4.5)) x, y = grid_from_bounding_box(bb) assert_allclose(x, np.repeat([np.arange(6)], 5, axis=0)) assert_allclose(y, np.repeat(np.array([np.arange(5)]), 6, 0).T) bb = ((-0.5, 5.5), (-0.5, 4.6)) x, y = grid_from_bounding_box(bb, center=True) assert_allclose(x, np.repeat([np.arange(6)], 6, axis=0)) assert_allclose(y, np.repeat(np.array([np.arange(6)]), 6, 0).T) def test_bounding_box_eval(): """ Tests evaluation with and without respecting the bounding_box. """ trans3 = models.Shift(10) & models.Scale(2) & models.Shift(-1) pipeline = [('detector', trans3), ('sky', None)] w = wcs.WCS(pipeline) w.bounding_box = ((-1, 10), (6, 15), (4.3, 6.9)) # test scalar outside bbox assert_allclose(w(1, 7, 3), [np.nan, np.nan, np.nan]) assert_allclose(w(1, 7, 3, with_bounding_box=False), [11, 14, 2]) assert_allclose(w(1, 7, 3, fill_value=100.3), [100.3, 100.3, 100.3]) assert_allclose(w(1, 7, 3, fill_value=np.inf), [np.inf, np.inf, np.inf]) # test scalar inside bbox assert_allclose(w(1, 7, 5), [11, 14, 4]) # test arrays assert_allclose(w([1, 1], [7, 7], [3, 5]), [[np.nan, 11], [np.nan, 14], [np.nan, 4]]) # test ``transform`` method assert_allclose(w.transform('detector', 'sky', 1, 7, 3), [np.nan, np.nan, np.nan]) def test_format_output(): points = np.arange(5) values = np.array([1.5, 3.4, 6.7, 7, 32]) t = models.Tabular1D(points, values) pipe = [('detector', t), ('world', None)] w = wcs.WCS(pipe) assert_allclose(w(1), 3.4) assert_allclose(w([1, 2]), [3.4, 6.7]) assert np.isscalar(w(1)) def test_available_frames(): w = wcs.WCS(pipe) assert w.available_frames == ['detector', 'focal', 'icrs'] def test_footprint(): icrs = cf.CelestialFrame(name='icrs', reference_frame=coord.ICRS(), axes_order=(0, 1)) spec = cf.SpectralFrame(name='freq', unit=[u.Hz, ], axes_order=(2, )) world = cf.CompositeFrame([icrs, spec]) transform = (models.Shift(10) & models.Shift(-1)) & models.Scale(2) pipe = [('det', transform), (world, None)] w = wcs.WCS(pipe) with pytest.raises(TypeError): w.footprint() w.bounding_box = ((1,5), (1,3), (1, 6)) assert_equal(w.footprint(), np.array([[11, 0, 2], [11, 0, 12], [11, 2, 2], [11, 2, 12], [15, 0, 2], [15, 0, 12], [15, 2, 2], [15, 2, 12]])) assert_equal(w.footprint(axis_type='spatial'), np.array([[11., 0.], [11., 2.], [15., 2.], [15., 0.]])) assert_equal(w.footprint(axis_type='spectral'), np.array([2, 12])) def test_high_level_api(): """ Test WCS high level API. """ output_frame = cf.CompositeFrame(frames=[icrs, spec]) transform = m1 & models.Scale(1.5) det = cf.CoordinateFrame(naxes=3, unit=(u.pix, u.pix, u.pix), axes_order=(0, 1, 2), axes_type=('length', 'length', 'length')) w = wcs.WCS(forward_transform=transform, output_frame=output_frame, input_frame=det) r, d, lam = w(xv, yv, xv) world_coord = w.pixel_to_world(xv, yv, xv) assert isinstance(world_coord[0], coord.SkyCoord) assert isinstance(world_coord[1], u.Quantity) assert_allclose(world_coord[0].data.lon.value, r) assert_allclose(world_coord[0].data.lat.value, d) assert_allclose(world_coord[1].value, lam) x1, y1, z1 = w.world_to_pixel(*world_coord) assert_allclose(x1, xv) assert_allclose(y1, yv) assert_allclose(z1, xv) class TestImaging(object): def setup_class(self): hdr = fits.Header.fromtextfile(get_pkg_data_filename("data/acs.hdr"), endcard=False) self.fitsw = astwcs.WCS(hdr) a_coeff = hdr['A_*'] a_order = a_coeff.pop('A_ORDER') b_coeff = hdr['B_*'] b_order = b_coeff.pop('B_ORDER') crpix = [hdr['CRPIX1'], hdr['CRPIX2']] distortion = models.SIP( crpix, a_order, b_order, a_coeff, b_coeff, name='sip_distorion') + models.Identity(2) cdmat = np.array([[hdr['CD1_1'], hdr['CD1_2']], [hdr['CD2_1'], hdr['CD2_2']]]) aff = models.AffineTransformation2D(matrix=cdmat, name='rotation') offx = models.Shift(-hdr['CRPIX1'], name='x_translation') offy = models.Shift(-hdr['CRPIX2'], name='y_translation') wcslin = (offx & offy) | aff phi = hdr['CRVAL1'] lon = hdr['CRVAL2'] theta = 180 n2c = models.RotateNative2Celestial(phi, lon, theta, name='sky_rotation') tan = models.Pix2Sky_TAN(name='tangent_projection') sky_cs = cf.CelestialFrame(reference_frame=coord.ICRS(), name='sky') det = cf.Frame2D(name='detector') wcs_forward = wcslin | tan | n2c pipeline = [('detector', distortion), ('focal', wcs_forward), (sky_cs, None) ] self.wcs = wcs.WCS(input_frame=det, output_frame=sky_cs, forward_transform=pipeline) self.xv, self.yv = xv, yv @pytest.mark.filterwarnings('ignore') def test_distortion(self): sipx, sipy = self.fitsw.sip_pix2foc(self.xv, self.yv, 1) sipx = np.array(sipx) + 2048 sipy = np.array(sipy) + 1024 sip_coord = self.wcs.get_transform('detector', 'focal')(self.xv, self.yv) assert_allclose(sipx, sip_coord[0]) assert_allclose(sipy, sip_coord[1]) def test_wcslinear(self): ra, dec = self.fitsw.wcs_pix2world(self.xv, self.yv, 1) sky = self.wcs.get_transform('focal', 'sky')(self.xv, self.yv) assert_allclose(ra, sky[0]) assert_allclose(dec, sky[1]) def test_forward(self): sky_coord = self.wcs(self.xv, self.yv) ra, dec = self.fitsw.all_pix2world(self.xv, self.yv, 1) assert_allclose(sky_coord[0], ra) assert_allclose(sky_coord[1], dec) def test_backward(self): transform = self.wcs.get_transform(from_frame='focal', to_frame=self.wcs.output_frame) sky_coord = self.wcs.transform('focal', self.wcs.output_frame, self.xv, self.yv) px_coord = transform.inverse(*sky_coord) assert_allclose(px_coord[0], self.xv, atol=10**-6) assert_allclose(px_coord[1], self.yv, atol=10**-6) def test_footprint(self): bb = ((1, 4096), (1, 2048)) footprint = (self.wcs.footprint(bb)) fits_footprint = self.fitsw.calc_footprint(axes=(4096, 2048)) assert_allclose(footprint, fits_footprint) def test_inverse(self): sky_coord = self.wcs(1, 2, with_units=True) with pytest.raises(NotImplementedError): self.wcs.invert(sky_coord) def test_back_coordinates(self): sky_coord = self.wcs(1, 2, with_units=True) res = self.wcs.transform('sky', 'focal', sky_coord) assert_allclose(res, self.wcs.get_transform('detector', 'focal')(1, 2)) def test_units(self): assert(self.wcs.unit == (u.degree, u.degree)) def test_get_transform(self): with pytest.raises(wcs.CoordinateFrameError): assert(self.wcs.get_transform('x_translation', 'sky_rotation').submodel_names == \ self.wcs.forward_transform[1:].submodel_names) def test_pixel_to_world(self): sky_coord = self.wcs.pixel_to_world(self.xv, self.yv) ra, dec = self.fitsw.all_pix2world(self.xv, self.yv, 1) assert isinstance(sky_coord, coord.SkyCoord) assert_allclose(sky_coord.data.lon.value, ra) assert_allclose(sky_coord.data.lat.value, dec) gwcs-0.12.0/gwcs/utils.py0000644000732200020070000003350613600426757017402 0ustar denchevaSTSCI\science00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Utility function for WCS """ import re import functools import numpy as np from astropy.modeling import models as astmodels from astropy.modeling import core, projections from astropy.io import fits from astropy import coordinates as coords from astropy import units as u # these ctype values do not include yzLN and yzLT pairs sky_pairs = {"equatorial": ["RA", "DEC"], "ecliptic": ["ELON", "ELAT"], "galactic": ["GLON", "GLAT"], "helioecliptic": ["HLON", "HLAT"], "supergalactic": ["SLON", "SLAT"], # "spec": specsystems } radesys = ['ICRS', 'FK5', 'FK4', 'FK4-NO-E', 'GAPPT', 'GALACTIC'] class UnsupportedTransformError(Exception): def __init__(self, message): super(UnsupportedTransformError, self).__init__(message) class UnsupportedProjectionError(Exception): def __init__(self, code): message = "Unsupported projection: {0}".format(code) super(UnsupportedProjectionError, self).__init__(message) class RegionError(Exception): def __init__(self, message): super(RegionError, self).__init__(message) class CoordinateFrameError(Exception): def __init__(self, message): super(CoordinateFrameError, self).__init__(message) def _toindex(value): """ Convert value to an int or an int array. Input coordinates converted to integers corresponding to the center of the pixel. The convention is that the center of the pixel is (0, 0), while the lower left corner is (-0.5, -0.5). The outputs are used to index the mask. Examples -------- >>> _toindex(np.array([-0.5, 0.49999])) array([0, 0]) >>> _toindex(np.array([0.5, 1.49999])) array([1, 1]) >>> _toindex(np.array([1.5, 2.49999])) array([2, 2]) """ indx = np.asarray(np.floor(np.asarray(value) + 0.5), dtype=np.int) return indx def get_values(units, *args): """ Return the values of Quantity objects after optionally converting to units. Parameters ---------- units : str or `~astropy.units.Unit` or None Units to convert to. The input values are converted to ``units`` before the values are returned. args : `~astropy.units.Quantity` Quantity inputs. """ if units is not None: result = [a.to_value(unit) for a, unit in zip(args, units)] else: result = [a.value for a in args] return result def _compute_lon_pole(skycoord, projection): """ Compute the longitude of the celestial pole of a standard frame in the native frame. This angle then can be used as one of the Euler angles (the other two being skyccord) to rotate the native frame into the standard frame ``skycoord.frame``. Parameters ---------- skycoord : `astropy.coordinates.SkyCoord`, or sequence of floats or `~astropy.units.Quantity` of length 2 The fiducial point of the native coordinate system. If tuple, its length is 2 projection : `astropy.modeling.projections.Projection` A Projection instance. Returns ------- lon_pole : float or `~astropy/units.Quantity` Native longitude of the celestial pole [deg]. TODO: Implement all projections Currently this only supports Zenithal and Cylindrical. """ if isinstance(skycoord, coords.SkyCoord): lat = skycoord.spherical.lat unit = u.deg else: lon, lat = skycoord if isinstance(lat, u.Quantity): unit = u.deg else: unit = None if isinstance(projection, projections.Zenithal): lon_pole = 180 elif isinstance(projection, projections.Cylindrical): if lat >= 0: lon_pole = 0 else: lon_pole = 180 else: raise UnsupportedProjectionError("Projection {0} is not supported.".format(projection)) if unit is not None: lon_pole = lon_pole * unit return lon_pole def get_projcode(wcs_info): # CTYPE here is only the imaging CTYPE keywords sky_axes, _, _ = get_axes(wcs_info) if not sky_axes: return None projcode = wcs_info['CTYPE'][sky_axes[0]][5:8].upper() if projcode not in projections.projcodes: raise UnsupportedProjectionError('Projection code %s, not recognized' % projcode) return projcode def read_wcs_from_header(header): """ Extract basic FITS WCS keywords from a FITS Header. Parameters ---------- header : astropy.io.fits.Header FITS Header with WCS information. Returns ------- wcs_info : dict A dictionary with WCS keywords. """ wcs_info = {} try: wcs_info['WCSAXES'] = header['WCSAXES'] except KeyError: p = re.compile(r'ctype[\d]*', re.IGNORECASE) ctypes = header['CTYPE*'] keys = list(ctypes.keys()) for key in keys[::-1]: if p.split(key)[-1] != "": keys.remove(key) wcs_info['WCSAXES'] = len(keys) wcsaxes = wcs_info['WCSAXES'] # if not present call get_csystem wcs_info['RADESYS'] = header.get('RADESYS', 'ICRS') wcs_info['VAFACTOR'] = header.get('VAFACTOR', 1) wcs_info['NAXIS'] = header.get('NAXIS', 0) # date keyword? # wcs_info['DATEOBS'] = header.get('DATE-OBS', 'DATEOBS') wcs_info['EQUINOX'] = header.get("EQUINOX", None) wcs_info['EPOCH'] = header.get("EPOCH", None) wcs_info['DATEOBS'] = header.get("MJD-OBS", header.get("DATE-OBS", None)) ctype = [] cunit = [] crpix = [] crval = [] cdelt = [] for i in range(1, wcsaxes + 1): ctype.append(header['CTYPE{0}'.format(i)]) cunit.append(header.get('CUNIT{0}'.format(i), None)) crpix.append(header.get('CRPIX{0}'.format(i), 0.0)) crval.append(header.get('CRVAL{0}'.format(i), 0.0)) cdelt.append(header.get('CDELT{0}'.format(i), 1.0)) if 'CD1_1' in header: wcs_info['has_cd'] = True else: wcs_info['has_cd'] = False pc = np.zeros((wcsaxes, wcsaxes)) for i in range(1, wcsaxes + 1): for j in range(1, wcsaxes + 1): try: if wcs_info['has_cd']: pc[i - 1, j - 1] = header['CD{0}_{1}'.format(i, j)] else: pc[i - 1, j - 1] = header['PC{0}_{1}'.format(i, j)] except KeyError: if i == j: pc[i - 1, j - 1] = 1. else: pc[i - 1, j - 1] = 0. wcs_info['CTYPE'] = ctype wcs_info['CUNIT'] = cunit wcs_info['CRPIX'] = crpix wcs_info['CRVAL'] = crval wcs_info['CDELT'] = cdelt wcs_info['PC'] = pc return wcs_info def get_axes(header): """ Matches input with spectral and sky coordinate axes. Parameters ---------- header : astropy.io.fits.Header or dict FITS Header (or dict) with basic WCS information. Returns ------- sky_inmap, spectral_inmap, unknown : lists indices in the input representing sky and spectral cordinates. """ if isinstance(header, fits.Header): wcs_info = read_wcs_from_header(header) elif isinstance(header, dict): wcs_info = header else: raise TypeError("Expected a FITS Header or a dict.") # Split each CTYPE value at "-" and take the first part. # This should represent the coordinate system. ctype = [ax.split('-')[0].upper() for ax in wcs_info['CTYPE']] sky_inmap = [] spec_inmap = [] unknown = [] skysystems = np.array(list(sky_pairs.values())).flatten() for ax in ctype: ind = ctype.index(ax) if ax in specsystems: spec_inmap.append(ind) elif ax in skysystems: sky_inmap.append(ind) else: unknown.append(ind) if sky_inmap: _is_skysys_consistent(ctype, sky_inmap) return sky_inmap, spec_inmap, unknown def _is_skysys_consistent(ctype, sky_inmap): """ Determine if the sky axes in CTYPE mathch to form a standard celestial system.""" for item in sky_pairs.values(): if ctype[sky_inmap[0]] == item[0]: if ctype[sky_inmap[1]] != item[1]: raise ValueError( "Inconsistent ctype for sky coordinates {0} and {1}".format(*ctype)) break elif ctype[sky_inmap[1]] == item[0]: if ctype[sky_inmap[0]] != item[1]: raise ValueError( "Inconsistent ctype for sky coordinates {0} and {1}".format(*ctype)) sky_inmap = sky_inmap[::-1] break specsystems = ["WAVE", "FREQ", "ENER", "WAVEN", "AWAV", "VRAD", "VOPT", "ZOPT", "BETA", "VELO"] sky_systems_map = {'ICRS': coords.ICRS, 'FK5': coords.FK5, 'FK4': coords.FK4, 'FK4NOE': coords.FK4NoETerms, 'GAL': coords.Galactic, 'HOR': coords.AltAz } def make_fitswcs_transform(header): """ Create a basic FITS WCS transform. It does not include distortions. Parameters ---------- header : astropy.io.fits.Header or dict FITS Header (or dict) with basic WCS information """ if isinstance(header, fits.Header): wcs_info = read_wcs_from_header(header) elif isinstance(header, dict): wcs_info = header else: raise TypeError("Expected a FITS Header or a dict.") transforms = [] wcs_linear = fitswcs_linear(wcs_info) transforms.append(wcs_linear) wcs_nonlinear = fitswcs_nonlinear(wcs_info) if wcs_nonlinear is not None: transforms.append(wcs_nonlinear) return functools.reduce(core._model_oper('|'), transforms) def fitswcs_linear(header): """ Create a WCS linear transform from a FITS header. Parameters ---------- header : astropy.io.fits.Header or dict FITS Header or dict with basic FITS WCS keywords. """ if isinstance(header, fits.Header): wcs_info = read_wcs_from_header(header) elif isinstance(header, dict): wcs_info = header else: raise TypeError("Expected a FITS Header or a dict.") pc = wcs_info['PC'] # get the part of the PC matrix corresponding to the imaging axes sky_axes, spec_axes, unknown = get_axes(wcs_info) if pc.shape != (2, 2): if sky_axes: i, j = sky_axes elif unknown and len(unknown) == 2: i, j = unknown sky_pc = np.zeros((2, 2)) sky_pc[0, 0] = pc[i, i] sky_pc[0, 1] = pc[i, j] sky_pc[1, 0] = pc[j, i] sky_pc[1, 1] = pc[j, j] pc = sky_pc.copy() sky_axes.extend(unknown) if sky_axes: crpix = [] cdelt = [] for i in sky_axes: crpix.append(wcs_info['CRPIX'][i]) cdelt.append(wcs_info['CDELT'][i]) else: cdelt = wcs_info['CDELT'] crpix = wcs_info['CRPIX'] # if wcsaxes == 2: rotation = astmodels.AffineTransformation2D(matrix=pc, name='pc_matrix') # elif wcsaxes == 3 : # rotation = AffineTransformation3D(matrix=matrix) # else: # raise DimensionsError("WCSLinearTransform supports only 2 or 3 dimensions, " # "{0} given".format(wcsaxes)) translation_models = [astmodels.Shift(-shift, name='crpix' + str(i + 1)) for i, shift in enumerate(crpix)] translation = functools.reduce(lambda x, y: x & y, translation_models) if not wcs_info['has_cd']: # Do not compute scaling since CDELT* = 1 if CD is present. scaling_models = [astmodels.Scale(scale, name='cdelt' + str(i + 1)) for i, scale in enumerate(cdelt)] scaling = functools.reduce(lambda x, y: x & y, scaling_models) wcs_linear = translation | rotation | scaling else: wcs_linear = translation | rotation return wcs_linear def fitswcs_nonlinear(header): """ Create a WCS linear transform from a FITS header. Parameters ---------- header : astropy.io.fits.Header or dict FITS Header or dict with basic FITS WCS keywords. """ if isinstance(header, fits.Header): wcs_info = read_wcs_from_header(header) elif isinstance(header, dict): wcs_info = header else: raise TypeError("Expected a FITS Header or a dict.") transforms = [] projcode = get_projcode(wcs_info) if projcode is not None: projection = create_projection_transform(projcode).rename(projcode) transforms.append(projection) # Create the sky rotation transform sky_axes, _, _ = get_axes(wcs_info) if sky_axes: phip, lonp = [wcs_info['CRVAL'][i] for i in sky_axes] # TODO: write "def compute_lonpole(projcode, l)" # Set a defaul tvalue for now thetap = 180 n2c = astmodels.RotateNative2Celestial(phip, lonp, thetap, name="crval") transforms.append(n2c) if transforms: return functools.reduce(core._model_oper('|'), transforms) return None def create_projection_transform(projcode): """ Create the non-linear projection transform. Parameters ---------- projcode : str FITS WCS projection code. Returns ------- transform : astropy.modeling.Model Projection transform. """ projklassname = 'Pix2Sky_' + projcode try: projklass = getattr(projections, projklassname) except AttributeError: raise UnsupportedProjectionError(projcode) projparams = {} return projklass(**projparams) def isnumerical(val): """ Determine if a value is numerical (number or np.array of numbers). """ isnum = True if isinstance(val, coords.SkyCoord): isnum = False elif isinstance(val, u.Quantity): isnum = False elif (isinstance(val, np.ndarray) and not np.issubdtype(val.dtype, np.floating) and not np.issubdtype(val.dtype, np.integer)): isnum = False return isnum gwcs-0.12.0/gwcs/wcs.py0000644000732200020070000005643413600426757017043 0ustar denchevaSTSCI\science00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst import functools import itertools import numpy as np from astropy.modeling.core import Model # , fix_inputs from astropy.modeling import utils as mutils from .api import GWCSAPIMixin from . import coordinate_frames from .utils import CoordinateFrameError from .utils import _toindex from . import utils HAS_FIX_INPUTS = True try: from astropy.modeling.core import fix_inputs except ImportError: HAS_FIX_INPUTS = False __all__ = ['WCS'] class WCS(GWCSAPIMixin): """ Basic WCS class. Parameters ---------- forward_transform : `~astropy.modeling.Model` or a list The transform between ``input_frame`` and ``output_frame``. A list of (frame, transform) tuples where ``frame`` is the starting frame and ``transform`` is the transform from this frame to the next one or ``output_frame``. The last tuple is (transform, None), where None indicates the end of the pipeline. input_frame : str, `~gwcs.coordinate_frames.CoordinateFrame` A coordinates object or a string name. output_frame : str, `~gwcs.coordinate_frames.CoordinateFrame` A coordinates object or a string name. name : str a name for this WCS """ def __init__(self, forward_transform=None, input_frame='detector', output_frame=None, name=""): #self.low_level_wcs = self self._available_frames = [] self._pipeline = [] self._name = name self._array_shape = None self._initialize_wcs(forward_transform, input_frame, output_frame) self._pixel_shape = None def _initialize_wcs(self, forward_transform, input_frame, output_frame): if forward_transform is not None: if isinstance(forward_transform, Model): if output_frame is None: raise CoordinateFrameError("An output_frame must be specified" "if forward_transform is a model.") _input_frame, inp_frame_obj = self._get_frame_name(input_frame) _output_frame, outp_frame_obj = self._get_frame_name(output_frame) super(WCS, self).__setattr__(_input_frame, inp_frame_obj) super(WCS, self).__setattr__(_output_frame, outp_frame_obj) self._pipeline = [(input_frame, forward_transform.copy()), (output_frame, None)] elif isinstance(forward_transform, list): for item in forward_transform: name, frame_obj = self._get_frame_name(item[0]) super(WCS, self).__setattr__(name, frame_obj) #self._pipeline.append((name, item[1])) self._pipeline = forward_transform else: raise TypeError("Expected forward_transform to be a model or a " "(frame, transform) list, got {0}".format( type(forward_transform))) else: # Initialize a WCS without a forward_transform - allows building a WCS programmatically. if output_frame is None: raise CoordinateFrameError("An output_frame must be specified" "if forward_transform is None.") _input_frame, inp_frame_obj = self._get_frame_name(input_frame) _output_frame, outp_frame_obj = self._get_frame_name(output_frame) super(WCS, self).__setattr__(_input_frame, inp_frame_obj) super(WCS, self).__setattr__(_output_frame, outp_frame_obj) self._pipeline = [(_input_frame, None), (_output_frame, None)] def get_transform(self, from_frame, to_frame): """ Return a transform between two coordinate frames. Parameters ---------- from_frame : str or `~gwcs.coordinate_frame.CoordinateFrame` Initial coordinate frame name of object. to_frame : str, or instance of `~gwcs.cordinate_frames.CoordinateFrame` End coordinate frame name or object. Returns ------- transform : `~astropy.modeling.Model` Transform between two frames. """ if not self._pipeline: return None try: from_ind = self._get_frame_index(from_frame) except ValueError: raise CoordinateFrameError("Frame {0} is not in the available " "frames".format(from_frame)) try: to_ind = self._get_frame_index(to_frame) except ValueError: raise CoordinateFrameError("Frame {0} is not in the available frames".format(to_frame)) if to_ind < from_ind: transforms = np.array(self._pipeline[to_ind: from_ind])[:, 1].tolist() transforms = [tr.inverse for tr in transforms[::-1]] elif to_ind == from_ind: return None else: transforms = np.array(self._pipeline[from_ind: to_ind])[:, 1].copy() return functools.reduce(lambda x, y: x | y, transforms) def set_transform(self, from_frame, to_frame, transform): """ Set/replace the transform between two coordinate frames. Parameters ---------- from_frame : str or `~gwcs.coordinate_frame.CoordinateFrame` Initial coordinate frame. to_frame : str, or instance of `~gwcs.cordinate_frames.CoordinateFrame` End coordinate frame. transform : `~astropy.modeling.Model` Transform between ``from_frame`` and ``to_frame``. """ from_name, from_obj = self._get_frame_name(from_frame) to_name, to_obj = self._get_frame_name(to_frame) if not self._pipeline: if from_name != self._input_frame: raise CoordinateFrameError( "Expected 'from_frame' to be {0}".format(self._input_frame)) if to_frame != self._output_frame: raise CoordinateFrameError( "Expected 'to_frame' to be {0}".format(self._output_frame)) try: from_ind = self._get_frame_index(from_name) except ValueError: raise CoordinateFrameError("Frame {0} is not in the available frames".format(from_name)) try: to_ind = self._get_frame_index(to_name) except ValueError: raise CoordinateFrameError("Frame {0} is not in the available frames".format(to_name)) if from_ind + 1 != to_ind: raise ValueError("Frames {0} and {1} are not in sequence".format(from_name, to_name)) self._pipeline[from_ind] = (self._pipeline[from_ind][0], transform) @property def forward_transform(self): """ Return the total forward transform - from input to output coordinate frame. """ if self._pipeline: return functools.reduce(lambda x, y: x | y, [step[1] for step in self._pipeline[: -1]]) else: return None @property def backward_transform(self): """ Return the total backward transform if available - from output to input coordinate system. Raises ------ NotImplementedError : An analytical inverse does not exist. """ try: backward = self.forward_transform.inverse except NotImplementedError as err: raise NotImplementedError("Could not construct backward transform. \n{0}".format(err)) return backward def _get_frame_index(self, frame): """ Return the index in the pipeline where this frame is locate. """ if isinstance(frame, coordinate_frames.CoordinateFrame): frame = frame.name frame_names = [getattr(item[0], "name", item[0]) for item in self._pipeline] return frame_names.index(frame) def _get_frame_name(self, frame): """ Return the name of the frame and a ``CoordinateFrame`` object. Parameters ---------- frame : str, `~gwcs.coordinate_frames.CoordinateFrame` Coordinate frame. Returns ------- name : str The name of the frame frame_obj : `~gwcs.coordinate_frames.CoordinateFrame` Frame instance or None (if `frame` is str) """ if isinstance(frame, str): name = frame frame_obj = None else: name = frame.name frame_obj = frame return name, frame_obj def __call__(self, *args, **kwargs): """ Executes the forward transform. args : float or array-like Inputs in the input coordinate system, separate inputs for each dimension. with_units : bool If ``True`` returns a `~astropy.coordinates.SkyCoord` or `~astropy.units.Quantity` object, by using the units of the output cooridnate frame. Optional, default=False. with_bounding_box : bool, optional If True(default) values in the result which correspond to any of the inputs being outside the bounding_box are set to ``fill_value``. fill_value : float, optional Output value for inputs outside the bounding_box (default is np.nan). """ transform = self.forward_transform if transform is None: raise NotImplementedError("WCS.forward_transform is not implemented.") with_units = kwargs.pop("with_units", False) if 'with_bounding_box' not in kwargs: kwargs['with_bounding_box'] = True if 'fill_value' not in kwargs: kwargs['fill_value'] = np.nan if self.bounding_box is not None: # Currently compound models do not attempt to combine individual model # bounding boxes. Get the forward transform and assign the ounding_box to it # before evaluating it. The order Model.bounding_box is reversed. axes_ind = self._get_axes_indices() if transform.n_inputs > 1: transform.bounding_box = np.array(self.bounding_box)[axes_ind][::-1] else: transform.bounding_box = self.bounding_box result = transform(*args, **kwargs) if with_units: if self.output_frame.naxes == 1: result = self.output_frame.coordinates(result) else: result = self.output_frame.coordinates(*result) return result def invert(self, *args, **kwargs): """ Invert coordinates. The analytical inverse of the forward transform is used, if available. If not an iterative method is used. Parameters ---------- args : float, array like, `~astropy.coordinates.SkyCoord` or `~astropy.units.Unit` coordinates to be inverted kwargs : dict keyword arguments to be passed to the iterative invert method. with_bounding_box : bool, optional If True(default) values in the result which correspond to any of the inputs being outside the bounding_box are set to ``fill_value``. fill_value : float, optional Output value for inputs outside the bounding_box (default is np.nan). """ if not utils.isnumerical(args[0]): args = self.output_frame.coordinate_to_quantity(*args) if self.output_frame.naxes == 1: args = [args] if not self.backward_transform.uses_quantity: args = utils.get_values(self.output_frame.unit, *args) with_units = kwargs.pop('with_units', False) if 'with_bounding_box' not in kwargs: kwargs['with_bounding_box'] = True if 'fill_value' not in kwargs: kwargs['fill_value'] = np.nan try: result = self.backward_transform(*args, **kwargs) except (NotImplementedError, KeyError): result = self._invert(*args, **kwargs) if with_units and self.input_frame: if self.input_frame.naxes == 1: return self.input_frame.coordinates(result) else: return self.input_frame.coordinates(*result) else: return result def _invert(self, *args, **kwargs): """ Implement iterative inverse here. """ raise NotImplementedError def transform(self, from_frame, to_frame, *args, **kwargs): """ Transform positions between two frames. Parameters ---------- from_frame : str or `~gwcs.coordinate_frames.CoordinateFrame` Initial coordinate frame. to_frame : str, or instance of `~gwcs.cordinate_frames.CoordinateFrame` Coordinate frame into which to transform. args : float or array-like Inputs in ``from_frame``, separate inputs for each dimension. output_with_units : bool If ``True`` - returns a `~astropy.coordinates.SkyCoord` or `~astropy.units.Quantity` object. with_bounding_box : bool, optional If True(default) values in the result which correspond to any of the inputs being outside the bounding_box are set to ``fill_value``. fill_value : float, optional Output value for inputs outside the bounding_box (default is np.nan). """ transform = self.get_transform(from_frame, to_frame) if not utils.isnumerical(args[0]): inp_frame = getattr(self, from_frame) args = inp_frame.coordinate_to_quantity(*args) if not transform.uses_quantity: args = utils.get_values(inp_frame.unit, *args) with_units = kwargs.pop("with_units", False) if 'with_bounding_box' not in kwargs: kwargs['with_bounding_box'] = True if 'fill_value' not in kwargs: kwargs['fill_value'] = np.nan result = transform(*args, **kwargs) if with_units: to_frame_name, to_frame_obj = self._get_frame_name(to_frame) if to_frame_obj is not None: if to_frame_obj.naxes == 1: result = to_frame_obj.coordinates(result) else: result = to_frame_obj.coordinates(*result) else: raise TypeError("Coordinate objects could not be created because" "frame {0} is not defined.".format(to_frame_name)) return result @property def available_frames(self): """ List all frames in this WCS object. Returns ------- available_frames : dict {frame_name: frame_object or None} """ if self._pipeline: return [getattr(frame[0], "name", frame[0]) for frame in self._pipeline] else: return None def insert_transform(self, frame, transform, after=False): """ Insert a transform before (default) or after a coordinate frame. Append (or prepend) a transform to the transform connected to frame. Parameters ---------- frame : str or `~gwcs.coordinate_frame.CoordinateFrame` Coordinate frame which sets the point of insertion. transform : `~astropy.modeling.Model` New transform to be inserted in the pipeline after : bool If True, the new transform is inserted in the pipeline immediately after ``frame``. """ name, _ = self._get_frame_name(frame) frame_ind = self._get_frame_index(name) if not after: fr, current_transform = self._pipeline[frame_ind - 1] self._pipeline[frame_ind - 1] = (fr, current_transform | transform) else: fr, current_transform = self._pipeline[frame_ind] self._pipeline[frame_ind] = (fr, transform | current_transform) @property def unit(self): """The unit of the coordinates in the output coordinate system.""" if self._pipeline: try: return getattr(self, self._pipeline[-1][0].name).unit except AttributeError: return None else: return None @property def output_frame(self): """Return the output coordinate frame.""" if self._pipeline: frame = self._pipeline[-1][0] if not isinstance(frame, str): frame = frame.name return getattr(self, frame) else: return None @property def input_frame(self): """Return the input coordinate frame.""" if self._pipeline: frame = self._pipeline[0][0] if not isinstance(frame, str): frame = frame.name return getattr(self, frame) else: return None @property def name(self): """Return the name for this WCS.""" return self._name @name.setter def name(self, value): """Set the name for the WCS.""" self._name = value @property def pipeline(self): """Return the pipeline structure.""" return self._pipeline @property def bounding_box(self): """ Return the range of acceptable values for each input axis. The order of the axes is `~gwcs.coordinate_frames.CoordinateFrame.axes_order`. """ frames = self.available_frames transform_0 = self.get_transform(frames[0], frames[1]) try: bb = transform_0.bounding_box except NotImplementedError: return None if transform_0.n_inputs == 1: return bb try: axes_order = self.input_frame.axes_order except AttributeError: axes_order = np.arange(transform_0.n_inputs) # Model.bounding_box is in python order, need to reverse it first. bb = np.array(bb[::-1])[np.array(axes_order)] return tuple(tuple(item) for item in bb) @bounding_box.setter def bounding_box(self, value): """ Set the range of acceptable values for each input axis. The order of the axes is `~gwcs.coordinate_frames.CoordinateFrame.axes_order`. For two inputs and axes_order(0, 1) the bounding box is ((xlow, xhigh), (ylow, yhigh)). Parameters ---------- value : tuple or None Tuple of tuples with ("low", high") values for the range. """ frames = self.available_frames transform_0 = self.get_transform(frames[0], frames[1]) if value is None: transform_0.bounding_box = value else: try: # Make sure the dimensions of the new bbox are correct. mutils._BoundingBox.validate(transform_0, value) except: raise # get the sorted order of axes' indices axes_ind = self._get_axes_indices() if transform_0.n_inputs == 1: transform_0.bounding_box = value else: # The axes in bounding_box in modeling follow python order transform_0.bounding_box = np.array(value)[axes_ind][::-1] self.set_transform(frames[0], frames[1], transform_0) def _get_axes_indices(self): try: axes_ind = np.argsort(self.input_frame.axes_order) except AttributeError: # the case of a frame being a string axes_ind = np.arange(self.forward_transform.n_inputs) return axes_ind def __str__(self): from astropy.table import Table col1 = [item[0] for item in self._pipeline] col2 = [] for item in self._pipeline[: -1]: model = item[1] if model.name is not None: col2.append(model.name) else: col2.append(model.__class__.__name__) col2.append(None) t = Table([col1, col2], names=['From', 'Transform']) return str(t) def __repr__(self): fmt = "".format( self.output_frame, self.input_frame, self.forward_transform) return fmt def footprint(self, bounding_box=None, center=False, axis_type="all"): """ Return the footprint in world coordinates. Parameters ---------- bounding_box : tuple of floats: (start, stop) `prop: bounding_box` center : bool If `True` use the center of the pixel, otherwise use the corner. axis_type : str A supported ``output_frame.axes_type`` or "all" (default). One of ['spatial', 'spectral', 'temporal'] or a custom type. Returns ------- coord : ndarray Array of coordinates in the output_frame mapping corners to the output frame. For spatial coordinates the order is clockwise, starting from the bottom left corner. """ def _order_clockwise(v): return np.asarray([[v[0][0], v[1][0]], [v[0][0], v[1][1]], [v[0][1], v[1][1]], [v[0][1], v[1][0]]]).T if bounding_box is None: if self.bounding_box is None: raise TypeError("Need a valid bounding_box to compute the footprint.") bb = self.bounding_box else: bb = bounding_box all_spatial = all([t.lower() == "spatial" for t in self.output_frame.axes_type]) if all_spatial: vertices = _order_clockwise(bb) else: vertices = np.array(list(itertools.product(*bb))).T if center: vertices = _toindex(vertices) result = np.asarray(self.__call__(*vertices, **{'with_bounding_box': False})) axis_type = axis_type.lower() if axis_type == 'spatial' and all_spatial: return result.T if axis_type != "all": axtyp_ind = np.array([t.lower() for t in self.output_frame.axes_type]) == axis_type if not axtyp_ind.any(): raise ValueError('This WCS does not have axis of type "{}".'.format(axis_type)) result = np.asarray([(r.min(), r.max()) for r in result[axtyp_ind]]) if axis_type == "spatial": result = _order_clockwise(result) else: result.sort() result = np.squeeze(result) return result.T def fix_inputs(self, fixed): """ Return a new unique WCS by fixing inputs to constant values. Parameters ---------- fixed : dict Keyword arguments with fixed values corresponding to `self.selector`. Returns ------- new_wcs : `WCS` A new unique WCS corresponding to the values in `fixed`. Examples -------- >>> w = WCS(pipeline, selector={"spectral_order": [1, 2]}) # doctest: +SKIP >>> new_wcs = w.set_inputs(spectral_order=2) # doctest: +SKIP >>> new_wcs.inputs # doctest: +SKIP ("x", "y") """ if not HAS_FIX_INPUTS: raise ImportError('"fix_inputs" needs astropy version >= 4.0.') new_pipeline = [] step0 = self.pipeline[0] new_transform = fix_inputs(step0[1], fixed) new_pipeline.append((step0[0], new_transform)) new_pipeline.extend(self.pipeline[1:]) return self.__class__(new_pipeline) gwcs-0.12.0/gwcs/wcstools.py0000644000732200020070000002522713600426757020120 0ustar denchevaSTSCI\science00000000000000# Licensed under a 3-clause BSD style license - see LICENSE.rst import functools import warnings import numpy as np from astropy.modeling.core import Model from astropy.modeling import projections from astropy.modeling import models, fitting from astropy import coordinates as coord from astropy import units as u from .wcs import WCS from .coordinate_frames import * # noqa from .utils import UnsupportedTransformError, UnsupportedProjectionError from .utils import _compute_lon_pole __all__ = ['wcs_from_fiducial', 'grid_from_bounding_box', 'wcs_from_points'] def wcs_from_fiducial(fiducial, coordinate_frame=None, projection=None, transform=None, name='', bounding_box=None): """ Create a WCS object from a fiducial point in a coordinate frame. If an additional transform is supplied it is prepended to the projection. Parameters ---------- fiducial : `~astropy.coordinates.SkyCoord` or tuple of float One of: A location on the sky in some standard coordinate system. A Quantity with spectral units. A list of the above. coordinate_frame : ~gwcs.coordinate_frames.CoordinateFrame` The output coordinate frame. If fiducial is not an instance of `~astropy.coordinates.SkyCoord`, ``coordinate_frame`` is required. projection : `~astropy.modeling.projections.Projection` Projection instance - required if there is a celestial component in the fiducial. transform : `~astropy.modeling.Model` (optional) An optional tranform to be prepended to the transform constructed by the fiducial point. The number of outputs of this transform must equal the number of axes in the coordinate frame. name : str Name of this WCS. bounding_box : tuple Domain of this WCS. The format is a list of dictionaries for each axis in the input frame [{'lower': float, 'upper': float, 'includes_lower': bool, 'includes_upper': bool, 'step': float}] """ if transform is not None: if not isinstance(transform, Model): raise UnsupportedTransformError("Expected transform to be an instance" "of astropy.modeling.Model") # transform_outputs = transform.n_outputs if isinstance(fiducial, coord.SkyCoord): coordinate_frame = CelestialFrame(reference_frame=fiducial.frame, unit=(fiducial.spherical.lon.unit, fiducial.spherical.lat.unit)) fiducial_transform = _sky_transform(fiducial, projection) elif isinstance(coordinate_frame, CompositeFrame): trans_from_fiducial = [] for item in coordinate_frame.frames: ind = coordinate_frame.frames.index(item) try: model = frame2transform[item.__class__](fiducial[ind], projection=projection) except KeyError: raise TypeError("Coordinate frame {0} is not supported".format(item)) trans_from_fiducial.append(model) fiducial_transform = functools.reduce(lambda x, y: x & y, [tr for tr in trans_from_fiducial]) else: # The case of one coordinate frame with more than 1 axes. try: fiducial_transform = frame2transform[coordinate_frame.__class__](fiducial, projection=projection) except KeyError: raise TypeError("Coordinate frame {0} is not supported".format(coordinate_frame)) if transform is not None: forward_transform = transform | fiducial_transform else: forward_transform = fiducial_transform if bounding_box is not None: if len(bounding_box) != forward_transform.n_outputs: raise ValueError("Expected the number of items in 'bounding_box' to be equal to the " "number of outputs of the forawrd transform.") forward_transform.bounding_box = bounding_box[::-1] return WCS(output_frame=coordinate_frame, forward_transform=forward_transform, name=name) def _verify_projection(projection): if projection is None: raise ValueError("Celestial coordinate frame requires a projection to be specified.") if not isinstance(projection, projections.Projection): raise UnsupportedProjectionError(projection) def _sky_transform(skycoord, projection): """ A sky transform is a projection, followed by a rotation on the sky. """ _verify_projection(projection) lon_pole = _compute_lon_pole(skycoord, projection) if isinstance(skycoord, coord.SkyCoord): lon, lat = skycoord.spherical.lon, skycoord.spherical.lat else: lon, lat = skycoord sky_rotation = models.RotateNative2Celestial(lon, lat, lon_pole) return projection | sky_rotation def _spectral_transform(fiducial, **kwargs): """ A spectral transform is a shift by the fiducial. """ return models.Shift(fiducial) def _frame2D_transform(fiducial, **kwargs): fiducial_transform = functools.reduce(lambda x, y: x & y, [models.Shift(val) for val in fiducial]) return fiducial_transform frame2transform = {CelestialFrame: _sky_transform, SpectralFrame: _spectral_transform, Frame2D: _frame2D_transform } def grid_from_bounding_box(bounding_box, step=1, center=True): """ Create a grid of input points from the WCS bounding_box. Note: If ``bbox`` is a tuple describing the range of an axis in ``bounding_box``, ``x.5`` is considered part of the next pixel in ``bbox[0]`` and part of the previous pixel in ``bbox[1]``. In this way if ``bbox`` describes the edges of an image the indexing includes only pixels within the image. Parameters ---------- bounding_box : tuple The bounding_box of a WCS object, `~gwcs.wcs.WCS.bounding_box`. step : scalar or tuple Step size for grid in each dimension. Scalar applies to all dimensions. center : bool The bounding_box is in order of X, Y [, Z] and the output will be in the same order. Examples -------- >>> bb = ((-1, 2.9), (6, 7.5)) >>> grid_from_bounding_box(bb, step=(1, .5), center=False) array([[[-1. , 0. , 1. , 2. , 3. ], [-1. , 0. , 1. , 2. , 3. ], [-1. , 0. , 1. , 2. , 3. ], [-1. , 0. , 1. , 2. , 3. ]], [[ 6. , 6. , 6. , 6. , 6. ], [ 6.5, 6.5, 6.5, 6.5, 6.5], [ 7. , 7. , 7. , 7. , 7. ], [ 7.5, 7.5, 7.5, 7.5, 7.5]]]) >>> bb = ((-1, 2.9), (6, 7.5)) >>> grid_from_bounding_box(bb) array([[[-1., 0., 1., 2., 3.], [-1., 0., 1., 2., 3.]], [[ 6., 6., 6., 6., 6.], [ 7., 7., 7., 7., 7.]]]) Returns ------- x, y [, z]: ndarray Grid of points. """ def _bbox_to_pixel(bbox): return (np.floor(bbox[0] + 0.5), np.ceil(bbox[1] - 0.5)) # 1D case if np.isscalar(bounding_box[0]): nd = 1 bounding_box = (bounding_box, ) else: nd = len(bounding_box) if center: bb = tuple([_bbox_to_pixel(bb) for bb in bounding_box]) else: bb = bounding_box step = np.atleast_1d(step) if nd > 1 and len(step) == 1: step = np.repeat(step, nd) if len(step) != len(bb): raise ValueError('`step` must be a scalar, or tuple with length ' 'matching `bounding_box`') slices = [] for d, s in zip(bb, step): slices.append(slice(d[0], d[1] + s, s)) grid = np.mgrid[slices[::-1]][::-1] if nd == 1: return grid[0] return grid def wcs_from_points(xy, world_coordinates, fiducial, projection=projections.Sky2Pix_TAN(), degree=4, polynomial_type="polynomial"): """ Given two matching sets of coordinates on detector and sky, compute the WCS. Notes ----- This function implements the following algorithm: ``world_coordinates`` are transformed to a projection plane using the specified projection. A polynomial fits ``xy`` and the projected coordinates. The fitted polynomials and the projection transforms are combined into a tranform from detector to sky. The input coordinate frame is set to ``detector``. The output coordinate frame is initialized based on the frame in the fiducial. Parameters ---------- xy : tuple of 2 ndarrays Points in the input cooridnate frame - x, y inputs. world_coordinates : tuple of 2 ndarrays Points in the output coordinate frame. The order matches the order of ``xy``. fiducial_point : `~astropy.coordinates.SkyCoord` A fiducial point in the output coordinate frame. projection : `~astropy.modeling.projections.Projection` A projection type. One of the projections in `~astropy.modeling.projections.projcode`. degree : int Degree of Polynpomial model to be fit to data. polynomial_type : str one of "polynomial", "chebyshev", "legendre" Returns ------- wcsobj : `~gwcs.wcs.WCS` a WCS object for this observation. """ supported_poly_types = {"polynomial": models.Polynomial2D, "chebyshev": models.Chebyshev2D, "legendre": models.Legendre2D } x, y = xy lon, lat = world_coordinates if not isinstance(projection, projections.Projection): raise UnsupportedProjectionError("Unsupported projection code {0}".format(projection)) if polynomial_type not in supported_poly_types.keys(): raise ValueError("Unsupported polynomial_type: {}. " "Only one of {} is supported.".format(polynomial_type, supported_poly_types.keys())) skyrot = models.RotateCelestial2Native(fiducial.data.lon, fiducial.data.lat, 180*u.deg) trans = (skyrot | projection) projection_x, projection_y = trans(lon, lat) poly = supported_poly_types[polynomial_type](degree) fitter = fitting.LevMarLSQFitter() with warnings.catch_warnings(): warnings.simplefilter("ignore") poly_x = fitter(poly, x, y, projection_x) poly_y = fitter(poly, x, y, projection_y) transform = models.Mapping((0, 1, 0, 1)) | poly_x & poly_y | projection.inverse | skyrot.inverse skyframe = CelestialFrame(reference_frame=fiducial.frame) detector = Frame2D(name="detector") pipeline = [(detector, transform), (skyframe, None) ] return WCS(pipeline) gwcs-0.12.0/gwcs.egg-info/0000755000732200020070000000000013600427270017342 5ustar denchevaSTSCI\science00000000000000gwcs-0.12.0/gwcs.egg-info/PKG-INFO0000644000732200020070000000055713600427267020454 0ustar denchevaSTSCI\science00000000000000Metadata-Version: 2.1 Name: gwcs Version: 0.12.0 Summary: Generalized World Coordinate System Home-page: https://gwcs.readthedocs.io/en/latest/ Author: gwcs developers Author-email: help@stsci.edu License: BSD Description: Tools for managing the WCS of astronomical observations in a general (non-FITS) way Platform: UNKNOWN Provides-Extra: test Provides-Extra: docs gwcs-0.12.0/gwcs.egg-info/SOURCES.txt0000644000732200020070000000475213600427270021236 0ustar denchevaSTSCI\science00000000000000.bandit.yaml .gitignore .gitmodules .rtd-environment.yml .travis.yml CHANGES.rst CODE_OF_CONDUCT.md CONTRIBUTING.md MANIFEST.in README.rst conftest.py convert_schemas.py readthedocs.yml setup.cfg setup.py docs/Makefile docs/conf.py docs/index.rst docs/make.bat docs/_templates/autosummary/base.rst docs/_templates/autosummary/class.rst docs/_templates/autosummary/module.rst docs/gwcs/ifu-regions.png docs/gwcs/ifu.rst docs/gwcs/imaging_with_distortion.rst docs/gwcs/points_to_wcs.rst docs/gwcs/pure_asdf.rst docs/gwcs/using_wcs.rst docs/gwcs/wcs_ape.rst docs/gwcs/wcs_validation.rst docs/gwcs/wcstools.rst docs/gwcs/schemas/index.rst gwcs/__init__.py gwcs/api.py gwcs/coordinate_frames.py gwcs/extension.py gwcs/geometry.py gwcs/gwcs_types.py gwcs/region.py gwcs/selector.py gwcs/spectroscopy.py gwcs/utils.py gwcs/wcs.py gwcs/wcstools.py gwcs.egg-info/PKG-INFO gwcs.egg-info/SOURCES.txt gwcs.egg-info/dependency_links.txt gwcs.egg-info/entry_points.txt gwcs.egg-info/requires.txt gwcs.egg-info/top_level.txt gwcs/schemas/__init__.py gwcs/schemas/stsci.edu/gwcs/celestial_frame-1.0.0.yaml gwcs/schemas/stsci.edu/gwcs/composite_frame-1.0.0.yaml gwcs/schemas/stsci.edu/gwcs/direction_cosines-1.0.0.yaml gwcs/schemas/stsci.edu/gwcs/frame-1.0.0.yaml gwcs/schemas/stsci.edu/gwcs/frame2d-1.0.0.yaml gwcs/schemas/stsci.edu/gwcs/grating_equation-1.0.0.yaml gwcs/schemas/stsci.edu/gwcs/label_mapper-1.0.0.yaml gwcs/schemas/stsci.edu/gwcs/regions_selector-1.0.0.yaml gwcs/schemas/stsci.edu/gwcs/sellmeier_glass-1.0.0.yaml gwcs/schemas/stsci.edu/gwcs/sellmeier_zemax-1.0.0.yaml gwcs/schemas/stsci.edu/gwcs/snell3d-1.0.0.yaml gwcs/schemas/stsci.edu/gwcs/spectral_frame-1.0.0.yaml gwcs/schemas/stsci.edu/gwcs/step-1.0.0.yaml gwcs/schemas/stsci.edu/gwcs/stokes_frame-1.0.0.yaml gwcs/schemas/stsci.edu/gwcs/temporal_frame-1.0.0.yaml gwcs/schemas/stsci.edu/gwcs/wcs-1.0.0.yaml gwcs/tags/__init__.py gwcs/tags/geometry_models.py gwcs/tags/selectortags.py gwcs/tags/spectroscopy_models.py gwcs/tags/wcs.py gwcs/tags/tests/__init__.py gwcs/tags/tests/test_selector.py gwcs/tags/tests/test_transforms.py gwcs/tags/tests/test_wcs.py gwcs/tests/__init__.py gwcs/tests/conftest.py gwcs/tests/coveragerc gwcs/tests/setup_package.py gwcs/tests/test_api.py gwcs/tests/test_api_slicing.py gwcs/tests/test_coordinate_systems.py gwcs/tests/test_region.py gwcs/tests/test_spectroscopy_models.py gwcs/tests/test_utils.py gwcs/tests/test_wcs.py gwcs/tests/data/acs.hdr gwcs/tests/data/acs_wfc.hdr gwcs/tests/data/simple_wcs2.hdr licenses/LICENSE.rst licenses/README.rstgwcs-0.12.0/gwcs.egg-info/dependency_links.txt0000644000732200020070000000000113600427267023416 0ustar denchevaSTSCI\science00000000000000 gwcs-0.12.0/gwcs.egg-info/entry_points.txt0000644000732200020070000000015113600427267022643 0ustar denchevaSTSCI\science00000000000000[asdf_extensions] gwcs = gwcs.extension:GWCSExtension [bandit.formatters] bson = bandit_bson:formatter gwcs-0.12.0/gwcs.egg-info/requires.txt0000644000732200020070000000023513600427267021750 0ustar denchevaSTSCI\science00000000000000astropy>=4.0 numpy asdf [docs] sphinx sphinx-automodapi sphinx-rtd-theme stsci-rtd-theme sphinx-astropy sphinx-asdf [test] pytest pytest-doctestplus scipy gwcs-0.12.0/gwcs.egg-info/top_level.txt0000644000732200020070000000000513600427267022075 0ustar denchevaSTSCI\science00000000000000gwcs gwcs-0.12.0/licenses/0000755000732200020070000000000013600427270016512 5ustar denchevaSTSCI\science00000000000000gwcs-0.12.0/licenses/LICENSE.rst0000644000732200020070000000274113600426757020343 0ustar denchevaSTSCI\science00000000000000Copyright (c) 2015, Space Telescope Science Institute All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the Astropy Team nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. gwcs-0.12.0/licenses/README.rst0000644000732200020070000000024213600426757020210 0ustar denchevaSTSCI\science00000000000000Licenses ======== This directory holds license and credit information for the affiliated package, works the affiliated package is derived from, and/or datasets. gwcs-0.12.0/readthedocs.yml0000644000732200020070000000013213600426757017722 0ustar denchevaSTSCI\science00000000000000conda: file: .rtd-environment.yml python: version: 3.5 setup_py_install: true gwcs-0.12.0/setup.cfg0000644000732200020070000000144013600427270016525 0ustar denchevaSTSCI\science00000000000000[metadata] name = gwcs description = Generalized World Coordinate System long_description = Tools for managing the WCS of astronomical observations in a general (non-FITS) way author = gwcs developers author_email = help@stsci.edu license = BSD url = https://gwcs.readthedocs.io/en/latest/ edit_on_github = False github_project = spacetelescope/gwcs [build_sphinx] source-dir = docs build-dir = docs/_build all_files = 1 [upload_docs] upload-dir = docs/_build/html show-response = 1 [tool:pytest] minversion = 3.1 testpaths = gwcs docs norecursedirs = build docs/_build doctest_plus = enabled asdf_schema_tests_enabled = true asdf_schema_root = gwcs/schemas addopts = --doctest-rst [flake8] ignore = E265,E501,F403,F405 exclude = conftest.py, schemas, tags [egg_info] tag_build = tag_date = 0 gwcs-0.12.0/setup.py0000755000732200020070000000416213600426757016436 0ustar denchevaSTSCI\science00000000000000#!/usr/bin/env python # Licensed under a 3-clause BSD style license - see LICENSE.rst import glob import os import sys from setuptools import setup, find_packages from configparser import ConfigParser if sys.version_info < (3, 6): error = """ GWCS supports Python versions 3.6 and above. """ sys.exit(error) conf = ConfigParser() conf.read(['setup.cfg']) metadata = dict(conf.items('metadata')) PACKAGENAME = metadata.get('name', 'packagename') DESCRIPTION = metadata.get('description', 'Astropy affiliated package') AUTHOR = metadata.get('author', '') AUTHOR_EMAIL = metadata.get('author_email', '') LICENSE = metadata.get('license', 'unknown') URL = metadata.get('url', 'http://astropy.org') def get_package_data(): # Installs the schema files schemas = [] root = os.path.join(PACKAGENAME, 'schemas') for node, dirs, files in os.walk(root): for fname in files: if fname.endswith('.yaml'): schemas.append( os.path.relpath(os.path.join(node, fname), root)) # In the package directory, install to the subdirectory 'schemas' schemas = [os.path.join('schemas', s) for s in schemas] return schemas schemas = get_package_data() PACKAGE_DATA ={'gwcs':schemas} entry_points = {'asdf_extensions': 'gwcs = gwcs.extension:GWCSExtension', 'bandit.formatters': 'bson = bandit_bson:formatter'} DOCS_REQUIRE = [ 'sphinx', 'sphinx-automodapi', 'sphinx-rtd-theme', 'stsci-rtd-theme', 'sphinx-astropy', 'sphinx-asdf', ] TESTS_REQUIRE = [ 'pytest', 'pytest-doctestplus', 'scipy', ] setup(name=PACKAGENAME, use_scm_version=True, setup_requires=['setuptools_scm'], description=DESCRIPTION, install_requires=[ 'astropy>=4.0', 'numpy', 'asdf'], packages=find_packages(), extras_require={ 'test': TESTS_REQUIRE, 'docs': DOCS_REQUIRE, }, tests_require=TESTS_REQUIRE, author=AUTHOR, author_email=AUTHOR_EMAIL, license=LICENSE, url=URL, package_data=PACKAGE_DATA, entry_points=entry_points, )