././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1710091850.359165 gwcs-0.21.0/0000755000175100001770000000000014573367112012153 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/.bandit.yaml0000644000175100001770000000006114573367100014350 0ustar00runnerdockerexclude_dirs: - gwcs/tests - gwcs/tags/tests ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/.flake80000644000175100001770000000067314573367100013331 0ustar00runnerdocker# flake8 does not support pyproject.toml (https://github.com/PyCQA/flake8/issues/234) [flake8] select = F, W, E101, E111, E112, E113, E401, E402, E501, E711, E722 max-line-length = 110 exclude = conftest.py, schemas, tags, .git, __pycache__, docs, build, dist, .tox, .eggs # E265: # has no space after # E501: line too long # F403: unable to detect undefined names # F405: may be defined from * imports ignore = E265,E501,F403,F405,W503,W504 ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1710091850.339165 gwcs-0.21.0/.github/0000755000175100001770000000000014573367112013513 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/.github/CODEOWNERS0000644000175100001770000000024014573367100015077 0ustar00runnerdocker# automatically requests pull request reviews for files matching the given pattern; the last match takes precendence * @spacetelescope/gwcs-maintainers ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1710091850.339165 gwcs-0.21.0/.github/workflows/0000755000175100001770000000000014573367112015550 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/.github/workflows/build.yml0000644000175100001770000000060314573367100017366 0ustar00runnerdockername: build on: release: types: [ released ] pull_request: workflow_dispatch: jobs: build: uses: OpenAstronomy/github-actions-workflows/.github/workflows/publish_pure_python.yml@v1 with: upload_to_pypi: ${{ (github.event_name == 'release') && (github.event.action == 'released') }} secrets: pypi_token: ${{ secrets.PYPI_PASSWORD_STSCI_MAINTAINER }} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/.github/workflows/changelog.yml0000644000175100001770000000075114573367100020222 0ustar00runnerdockername: Ensure changelog on: pull_request: types: [labeled, unlabeled, opened, synchronize, reopened] jobs: ensure_changelog: name: Verify that a changelog entry exists for this pull request runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 with: submodules: true - run: grep -P '\[[^\]]*#${{github.event.number}}[,\]]' CHANGES.rst if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-changelog-entry-needed') }} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/.github/workflows/ci.yml0000644000175100001770000000547614573367100016677 0ustar00runnerdockername: 'CI' on: push: branches: - 'master' tags: - '*' pull_request: schedule: # Weekly Monday 9AM build # * is a special character in YAML so you have to quote this string - cron: '0 9 * * 1' concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: check: name: uses: OpenAstronomy/github-actions-workflows/.github/workflows/tox.yml@v1 with: envs: | - linux: check-style - linux: check-security test: uses: OpenAstronomy/github-actions-workflows/.github/workflows/tox.yml@main with: envs: | - linux: test python-version: 3.9 - linux: test-numpy125 python-version: 3.9 - linux: test-numpy123 python-version: 3.9 - linux: test python-version: 3.10 - linux: test-numpy125 python-version: 3.10 - linux: test-numpy123 python-version: 3.10 - linux: test python-version: 3.11 pytest-results-summary: true - macos: test python-version: 3.11 pytest-results-summary: true - linux: test-dev - linux: test-pyargs python-version: 3.11 - linux: test-cov python-version: 3.11 coverage: codecov pytest-results-summary: true crds: name: retrieve current CRDS context runs-on: ubuntu-latest env: OBSERVATORY: jwst CRDS_PATH: /tmp/crds_cache CRDS_SERVER_URL: https://jwst-crds.stsci.edu steps: - id: context run: > echo "pmap=$( curl -s -X POST -d '{"jsonrpc": "1.0", "method": "get_default_context", "params": ["${{ env.OBSERVATORY }}"], "id": 1}' ${{ env.CRDS_SERVER_URL }}/json/ | python -c "import sys, json; print(json.load(sys.stdin)['result'])" )" >> $GITHUB_OUTPUT # Get default CRDS_CONTEXT without installing crds client # See https://hst-crds.stsci.edu/static/users_guide/web_services.html#generic-request - id: path run: echo "path=${{ env.CRDS_PATH }}" >> $GITHUB_OUTPUT - id: server run: echo "url=${{ env.CRDS_SERVER_URL }}" >> $GITHUB_OUTPUT outputs: context: ${{ steps.context.outputs.pmap }} path: ${{ steps.path.outputs.path }} server: ${{ steps.server.outputs.url }} test_downstream: uses: OpenAstronomy/github-actions-workflows/.github/workflows/tox.yml@main needs: [ crds ] with: setenv: | CRDS_PATH: ${{ needs.crds.outputs.path }} CRDS_SERVER_URL: ${{ needs.crds.outputs.server }} CRDS_CLIENT_RETRY_COUNT: 3 CRDS_CLIENT_RETRY_DELAY_SECONDS: 20 cache-path: ${{ needs.crds.outputs.path }} cache-key: crds-${{ needs.crds.outputs.context }} envs: | - linux: py310-test-jwst-cov-xdist ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/.gitignore0000644000175100001770000000112414573367100014136 0ustar00runnerdocker# 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 .tox # 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/.readthedocs.yaml0000644000175100001770000000101514573367100015374 0ustar00runnerdocker# Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details version: 2 sphinx: builder: html configuration: docs/conf.py fail_on_warning: true build: os: ubuntu-22.04 tools: python: mambaforge-4.10 conda: environment: docs/rtd_environment.yaml # Set the version of Python and requirements required to build your docs python: install: - method: pip path: . extra_requirements: - docs # Don't build any extra formats formats: [] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/CHANGES.rst0000644000175100001770000002641214573367100013757 0ustar00runnerdocker0.21.0 (2024-03-10) ------------------- - Improve documentation. [#483] - Add a minimum version requirement for ``asdf-wcs-schemas``. [#491] - Fix ``WCS.__str__`` for instances without transforms. [#489] 0.20.0 (2023-11-29) ------------------- - Replace ``pkg_resources`` with ``importlib.metadata``. [#478] - Serialize and deserialize ``pixel_shape`` with asdf. [#480] 0.19.0 (2023-09-15) ------------------- Bug Fixes ^^^^^^^^^ - Synchronize ``array_shape`` and ``pixel_shape`` attributes of WCS objects. [#439] - Fix failures and warnings with numpy 2.0. [#472] other ^^^^^ - Remove deprecated old ``bounding_box``. The new implementation is released with astropy v 5.3. [#458] - Refactor ``CoordinateFrame.axis_physical_types``. [#459] - ``StokesFrame`` uses now ``astropy.coordinates.StokesCoord``. [#452] - Dropped support for Python 3.8. [#451] - Fixed a call to ``astropy.coordinates`` in ``wcstools.wcs_from_points``. [#448] - Code and docstrings clean up. [#460] - Register all available asdf extension manifests from ``asdf-wcs-schemas`` except 1.0.0 (which contains duplicate tag versions). [#469] - Register empty extension for 1.0.0 to avoid warning about a missing extension when opening old files. [#475] 0.18.3 (2022-12-23) ------------------- Bug Fixes ^^^^^^^^^ - Fixed a bug in the estimate of pixel scale in the iterative inverse code. [#423] - Fixed constant term in the polynomial used for SIP fitting. Improved stability and accuracy of the SIP fitting code. [#427] 0.18.2 (2022-09-07) ------------------- Bug Fixes ^^^^^^^^^ - Corrected the reported requested forward SIP accuracy and reported fit residuals by ``to_fits_sip()`` and ``to_fits()``. [#413, #419] - Fixed a bug due to which the check for divergence in ``_fit_2D_poly()`` and hence in ``to_fits()`` and ``to_fits_sip()`` was ignored. [#414] New Features ^^^^^^^^^^^^ 0.18.1 (2022-03-15) ------------------- Bug Fixes ^^^^^^^^^ - Remove references to the ``six`` package. [#402] 0.18.0 (2021-12-22) ------------------- Bug Fixes ^^^^^^^^^ - Updated code in ``region.py`` with latest improvements and bug fixes from ``stsci.skypac.regions.py`` [#382] - Added support to ``_compute_lon_pole()`` for computation of ``lonpole`` for all projections from ``astropy.modeling.projections``. This also extends support for different projections in ``wcs_from_fiducial()``. [#389] New Features ^^^^^^^^^^^^ - Enabled ``CompoundBoundingBox`` support for wcs. [#375] - Moved schemas to standalone package ``asdf-wcs-schemas``. Reworked the serialization code to use ASDF converters. [#388] 0.17.1 (2021-11-27) ------------------- Bug Fixes ^^^^^^^^^ - Fixed a bug with StokesProfile and array types. [#384] 0.17.0 (2021-11-17) ------------------- Bug Fixes ^^^^^^^^^ - `world_axis_object_components` and `world_axis_object_classes` now ensure unique keys in `CompositeFrame` and `CoordinateFrame`. [#356] - Fix issue where RuntimeWarning is raised when there are NaNs in coordinates in angle wrapping code [#367] - Fix deprecation warning when wcs is initialized with a pipeline [#368] - Use ``CD`` formalism in ``WCS.to_fits_sip()``. [#380] New Features ^^^^^^^^^^^^ - ``wcs_from_points`` now includes fitting for the inverse transform. [#349] - Generalized ``WCS.to_fits_sip`` to be able to create a 2D celestial FITS WCS from celestial subspace of the ``WCS``. Also, now `WCS.to_fits_sip`` supports arbitrary order of output axes. [#357] API Changes ^^^^^^^^^^^ - Modified interface to ``wcs_from_points`` function to better match analogous function in astropy. [#349] - ``Model._BoundingBox`` was renamed to ``Model.ModelBoundingBox``. [#376, #377] 0.16.1 (2020-12-20) ------------------- Bug Fixes ^^^^^^^^^ - Fix a regression with ``pixel_to_world`` for output frames with one axis. [#342] 0.16.0 (2020-12-18) ------------------- New Features ^^^^^^^^^^^^ - Added an option to `to_fits_sip()` to be able to specify the reference point (``crpix``) of the FITS WCS. [#337] - Added support for providing custom range of degrees in ``to_fits_sip``. [#339] Bug Fixes ^^^^^^^^^ - ``bounding_box`` now works with tuple of ``Quantities``. [#331] - Fix a formula for estimating ``crpix`` in ``to_fits_sip()`` so that ``crpix`` is near the center of the bounding box. [#337] - Allow sub-pixel sampling of the WCS model when computing SIP approximation in ``to_fits_sip()``. [#338] - Fixed a bug in ``to_fits_sip`` due to which ``inv_degree`` was ignored. [#339] 0.15.0 (2020-11-13) ------------------- New Features ^^^^^^^^^^^^ - Added ``insert_frame`` method to modify the pipeline of a ``WCS`` object. [#299] - Added ``to_fits_tab`` method to generate FITS header and binary table extension following FITS WCS ``-TAB`` convension. [#295] - Added ``in_image`` function for testing whether a point in world coordinates maps back to the domain of definition of the forward transformation. [#322] - Implemented iterative inverse for some imaging WCS. [#324] 0.14.0 (2020-08-19) ------------------- New Features ^^^^^^^^^^^^ - Updated versions of schemas for gwcs objects based on latest versions of transform schemas in asdf-standard. [#307] - Added a ``wcs.Step`` class to allow serialization to ASDF to use references. [#317] - ``wcs.pipeline`` now is a list of ``Step`` instances instead of a (frame, transform) tuple. Use ``WCS.pipeline.transform`` and ``WCS.pipeline.frame`` to access them. [#319] Bug Fixes ^^^^^^^^^ - Fix a bug in polygon fill for zero-width bounding boxes. [#293] - Add an optional parameter ``input_frame`` to ``wcstools.wcs_from_fiducial`. [#312] 0.13.0 (2020-03-26) ------------------- New Features ^^^^^^^^^^^^ - Added two new transforms - ``SphericalToCartesian`` and ``CartesianToSpherical``. [#275, #284, #285] - Added ``to_fits_sip`` method to generate FITS header with SIP keywords [#286] - Added ``get_ctype_from_ucd`` function. [#288] Bug Fixes ^^^^^^^^^ - Fixed an off by one issue in ``utils.make_fitswcs_transform``. [#290] 0.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.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. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/CODE_OF_CONDUCT.md0000644000175100001770000000620714573367100014754 0ustar00runnerdocker# 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/ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/CONTRIBUTING.md0000644000175100001770000000132114573367100014376 0ustar00runnerdockerPlease 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. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/MANIFEST.in0000644000175100001770000000057614573367100013716 0ustar00runnerdockerinclude 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 ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1710091850.355165 gwcs-0.21.0/PKG-INFO0000644000175100001770000001376714573367112013266 0ustar00runnerdockerMetadata-Version: 2.1 Name: gwcs Version: 0.21.0 Summary: Generalized World Coordinate System Author-email: gwcs developers License: Copyright (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. Project-URL: Homepage, https://github.com/spacetelescope/gwcs Project-URL: Tracker, https://github.com/spacetelescope/gwcs/issues Project-URL: Documentation, https://gwcs.readthedocs.io/en/stable/ Project-URL: Source Code, https://github.com/spacetelescope/jwst Requires-Python: >=3.9 Description-Content-Type: text/x-rst Requires-Dist: asdf>=2.8.1 Requires-Dist: astropy>=5.3 Requires-Dist: numpy Requires-Dist: scipy Requires-Dist: asdf_wcs_schemas>=0.4.0 Requires-Dist: asdf-astropy>=0.2.0 Provides-Extra: docs Requires-Dist: sphinx; extra == "docs" Requires-Dist: sphinx-automodapi; extra == "docs" Requires-Dist: sphinx-rtd-theme; extra == "docs" Requires-Dist: stsci-rtd-theme; extra == "docs" Requires-Dist: sphinx-astropy; extra == "docs" Requires-Dist: sphinx-asdf; extra == "docs" Requires-Dist: tomli; python_version < "3.11" and extra == "docs" Provides-Extra: test Requires-Dist: ci-watson>=0.3.0; extra == "test" Requires-Dist: pytest>=4.6.0; extra == "test" Requires-Dist: pytest-astropy; extra == "test" GWCS - Generalized World Coordinate System ========================================== .. image:: https://github.com/spacetelescope/gwcs/actions/workflows/ci.yml/badge.svg :target: https://github.com/spacetelescope/gwcs/actions :alt: CI Status .. image:: https://readthedocs.org/projects/docs/badge/?version=latest :target: https://gwcs.readthedocs.io/en/latest/ :alt: Documentation Status .. image:: https://codecov.io/gh/spacetelescope/gwcs/branch/master/graph/badge.svg?token=JtHal6Jbta :target: https://codecov.io/gh/spacetelescope/gwcs :alt: Code coverage .. image:: http://img.shields.io/badge/powered%20by-AstroPy-orange.svg?style=flat :target: http://www.astropy.org :alt: Powered by Astropy Badge .. image:: https://img.shields.io/badge/powered%20by-STScI-blue.svg?colorA=707170&colorB=3e8ddd&style=flat :target: http://www.stsci.edu :alt: Powered by STScI Badge 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. 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 --------------------------------------------- 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/ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/README.rst0000644000175100001770000000627214573367100013646 0ustar00runnerdockerGWCS - Generalized World Coordinate System ========================================== .. image:: https://github.com/spacetelescope/gwcs/actions/workflows/ci.yml/badge.svg :target: https://github.com/spacetelescope/gwcs/actions :alt: CI Status .. image:: https://readthedocs.org/projects/docs/badge/?version=latest :target: https://gwcs.readthedocs.io/en/latest/ :alt: Documentation Status .. image:: https://codecov.io/gh/spacetelescope/gwcs/branch/master/graph/badge.svg?token=JtHal6Jbta :target: https://codecov.io/gh/spacetelescope/gwcs :alt: Code coverage .. image:: http://img.shields.io/badge/powered%20by-AstroPy-orange.svg?style=flat :target: http://www.astropy.org :alt: Powered by Astropy Badge .. image:: https://img.shields.io/badge/powered%20by-STScI-blue.svg?colorA=707170&colorB=3e8ddd&style=flat :target: http://www.stsci.edu :alt: Powered by STScI Badge 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. 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 --------------------------------------------- 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/ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/convert_schemas.py0000644000175100001770000002772314573367100015720 0ustar00runnerdocker# 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)) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1710091850.343165 gwcs-0.21.0/docs/0000755000175100001770000000000014573367112013103 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/docs/Makefile0000644000175100001770000001116414573367100014543 0ustar00runnerdocker# 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." ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1710091850.3351648 gwcs-0.21.0/docs/_templates/0000755000175100001770000000000014573367112015240 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1710091850.343165 gwcs-0.21.0/docs/_templates/autosummary/0000755000175100001770000000000014573367112017626 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/docs/_templates/autosummary/base.rst0000644000175100001770000000037214573367100021271 0ustar00runnerdocker{% 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. #}././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/docs/_templates/autosummary/class.rst0000644000175100001770000000037314573367100021465 0ustar00runnerdocker{% 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. #}././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/docs/_templates/autosummary/module.rst0000644000175100001770000000037414573367100021646 0ustar00runnerdocker{% 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. #}././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/docs/conf.py0000644000175100001770000001441114573367100014400 0ustar00runnerdocker# -*- 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 sys from datetime import datetime from pathlib import Path try: import tomllib except ImportError: import tomli as tomllib import importlib.metadata 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) # -- 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 with open(Path(__file__).parent.parent / "pyproject.toml", "rb") as metadata_file: configuration = tomllib.load(metadata_file) metadata = configuration['project'] project = metadata['name'] author = metadata["authors"][0]["name"] copyright = f'{datetime.now().year}, {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. release = importlib.metadata.version(project) # 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)] sys.path.insert(0, str(Path(__file__).parent / 'sphinxext')) extensions += ['sphinx_asdf'] # Enable nitpicky mode - which ensures that all references in the docs resolve. nitpicky = True nitpick_ignore = [ ('py:class', 'gwcs.api.GWCSAPIMixin'), ('py:obj', 'astropy.modeling.projections.projcodes'), ('py:attr', 'gwcs.WCS.bounding_box'), ('py:meth', 'gwcs.WCS.footprint') ] ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1710091850.343165 gwcs-0.21.0/docs/gwcs/0000755000175100001770000000000014573367112014046 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/docs/gwcs/fits_analog.rst0000644000175100001770000001170514573367100017067 0ustar00runnerdocker.. _fits_equivalent_example: FITS Equivalent WCS Example =========================== 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 For this example the following imports are needed: >>> 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 Now we have a complete WCS object. The next example will use it to convert pixel coordinates(1, 2) to sky coordinates: >>> 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` to provide a mapping from sky coordinates to pixel coordinates if available, otherwise it applies an iterative method to calculate the pixel coordinates. >>> wcsobj.invert(sky) (, ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/docs/gwcs/ifu-regions.png0000644000175100001770000004427314573367100017012 0ustar00runnerdockerPNG  IHDR ]#$9tEXtSoftwareMatplotlib version3.7.2, https://matplotlib.org/)] pHYsaa?iH(IDATx{x}d&I8QAA1mwvu˶k綮ZVǮRFS$3]=6Gr-=TZ 9m`" T-DC^"+#"XN8l{ob _®L0`@`ڎ8 ? 'Ɣ$R0^P l#˧g 9`@V5T)an1Km UڞٞYA0OА2"3ٓ W$)uPhR,8J{#8nY>p($Iᱣ!3|vy} o<HLiU؞ B]1%9gIv7۞H"LnW@J*m{ ` `T&}TTr~3!~zvI'Juvڞ  Ȫ:mO͸GR@xY4)/UڞY夂q?9 (y`rCJJHU%g  "WOQ]Q0N0'H~jBN0~U$m@ %cf=D0@rRv3|c{lO I8* QnRJnj{F0~GEO>o{HP1DOo=@#~no=nGɶW 3d _mܿ p ȚHKXGJ@>"9Q'(`f0' QیWYG0&LӸqTRR;N7p1cxb 6L%%%5klimmռyTYYj]z:p@hT'D1o C0榛nҝwީztM7owܑ>7߬o]wu֭[2͞=[̛7O7nԊ+l2=3?o~=zN>d⋺[/|A8 }Є 4n8]wu>|."II'?_]v.q-\P_|1ϐ2hز,a]wu >\/ŋ|E]]]?}H˗/Wqqq0?ϴpB͜9Sjܹm%3YҦ`pO,҄B'0tm0_Bض`w#Ӻy0 +wf3D0 +ړ%Jk=O(("\ *T7TbʆcC0@(k 29ern2xV-'' Oj|v|ą,S9y =NQX?3 7n39`d{ DS9d_Jfo0^1@ D2=g"ҙT (`TpTr3 ι>+}Jɶv3,g"} צڞq?98[xq((T"Ǒ)^! q؞| u:|~1`*` Tk{JweQbǟl`3Y,7͎Ri˶gwozE3 dkپ _8IG2`@7W*a{`(@N(d{ (1m{ 8N(?߂W@Ƹ"3`rLx(5؞ BxX n{/Lij`(`nEC&1N?nXC{ɺv<@02W 9 ȌpsL"n{|¥䐲dRgR@ Cq$IoB9`Fr @ @']3 M@#c7Q/bcoq#QR*:؞?۞V ȨK؞ #  P`ܲ2P 8rd`c?kQ+{oO ʹL@n]+(h9{ rA @8T_}g~rOpeQb}ug@ *3|$TW@A Qrg3p s*}c` J9]ݶgUJlf{XC0 c<%j{D0@ =y` |*=y`&(8j= :ܲ2h >"SԤIFl/'y-\_`փPݘV3γh)l# P :t馛4j(s=ύ ?cn6]{ %I?OUWWGyD_|^{5-_\ׯ%Iwq>[nó{,pIF|- 裏jԧ>Zvi֭[ԤYf?WUU3fh͚55k֨: 4k,u`8[o;S&LOwI'i풤zIRsss477V__^_O$jmmMphT>|E ;Zr6 Hs9ڼysϽ3fwY__+Zn$I jkkSccc0O=Rf̘cؓOӏ~#G?$9+B74a7N]w.H;H2.\/ !sʤPWoa8 =Zhz7Nv͛>WuuuijkkӇ>!-_\g?… 5sLsoq㩪c{p_wG_F&];8]F=kra(0p܀^|pdRN (C9S/+7\r|Λ,r P-`=NGJڞ !.dy'n{Pxygc;mO d w4TG0 e$m=@#rV'~gCF>= Rx`VwMPm`8("38'8!Jٞ_NQ8V X; =uV:GUQ{<}NeT ;/@  D*OjTE ((Cr .AƃwqtXƃ4faX&g_=8f$7alO2i{pyUɣg`.tt<ŵr7kTsgsp{oTڸ I&@0`+b{ r=|Zdo AD0O[]e{`3CklO@@ p衶'9`tWb{sj? Y 7Dc{/HD'^ ъc{y-.)Nٞ=y+lC0РA=7!q`(.?I { @#nnl@ %]AG0_I$llO`7F?y` F` @%S&W`7QնWU88̾X*~ )|;HڞTsg2``r+*dBg (L&J0Ox%3C00w`\F(Ud{{c#e D0GT۞nj`,ys^8fJH.dy)h?h{$!J؞sCF]j{  ~sF&=Y@0+g,@ e %=@^(t$ WRJe{P x; aR 4KHmpn*rx?P so8+ҟւ 4x`kܹjnn}۷oל9sTZZZ]}J$~+aD0Pzj_yzCiժUڵk>OL&5gb1^Zw^-^8G!mOdpмyX J]w}nV{6m^Zk׮$=ڴi[SN\nAK.U,v{zz\A0 4g͚5{}OѣfIҚ5k4ydե3{lutthƍGK,QUUUcԨQ>+,0<^x-Y_kjjR$Quuuթ)}{_kGh"?vecSQr`3 yhvء/ZbѨhV&#( kFOW8V8֪Ut+NXLmmmY:Sz}LNK/UW]UVVK_tYgI;{ g{3ÀD؞dr@ 7\|/m> wCH Ir !w('v 7ACq 迷^,! dpcI%dϼj{gg$7@WdM;9/F3F0}F2@A#YݶgĮRn=@@ n *i:h{x /y M&( @ ?9S޶=AD0O[]e{` C0۞,6 g c\`onqTNulD0YV؞}B0VRڡW@ a>2cPXuDWl= `/iu#~m{ H n鐉l@ %cl@ EL Ȣ{e 3>#!  rިlO TQgE @%S[gx"w&T&@7]e勶g@N"0 =1կitOP/ٞ0x"'x" `BC*U=C0@8LL?Eb.d!@RHsDݩJy. -VH܊Ȳj ˶|?a yNQDo= < Ҹ[mPNU~9Q3A0̉'j=YD0UW9N]CPe饡G/?`*| #!x)` q T<(HNHJ3@CP9Ѩa3F0(xAE0 < %4asP(ZXU YB0$g?EmԐg@ @~*ݶg@A#ڞ`z߇+ jo*.Q}g{`=YF0@I^`,pRWm`,pd' g1Ge;yO  rV#l 权4=@ @]!%v E0ړ%rcM6鷿4uTp }k_S$G'玎<蔲]g@풤IRccf͚>̉'ѣGk͚55khɪKfƍs,YǨQ94dj` R s9:S$IMMMD"uغ:555Xx}H-Zǎ;{$ W_ճ>FF3sna8 jٲe~#bjkkufקs&{  1h…zSOiܸq>m4iʕm޼Y۷oWCC$AZZZ҇Yb*++5iҤȐpsR{6 wIf_JTUUDUUUKuUWFҗuY;O&M>9|jjjҵ^ pOH^,#swJ?$\ܹsӣٳg?APH˖-_K._ DŽdRgF0C5kҥZtQ3f7s<6q+a9f7`^l> '>Y,@q`D?m{6oT^=r< 藽.4s|;&iMmd < < EdB("4F҈(x"'@a;sx!T$$C/}&ùPP'Ϛg˄` KLn`@lb o%Ҕ oD0 w!I#渎fMd{@8C: @  @ "pCq%v= cg 0(x" JZS3! SJvt؞ < rRt+A3`d{w HOe{,#70` ܊ )dl~#,n~3`P}n W&3"1 '!`c*2| SdN!'x"̉'mO%;lO%D0Odc.`LJIm``s@.!䜢= < Ğ}~ E0aGzu38U6_0Q%"{9V^N;d0'tO;tj-Э+۫H%=OƜZ$Վ+UVFǣ3T\_ޤl֤]b{o&_~3:_lm=k@G?kNqW~R!Y1 ),I:mrT7) ҤI [tMϞmo5[ˍ[[5RE4 ?A||]:]=?>_YqTZYcKgAS&%j{ɔQ;X?zPsOMjKN秞o\;o^  g7c>1ٷ>2+}{dUNTkOp$(gܩ[]:l! OIKתOm{G?ZW7o3DGis꼿9J*i"+J>|qҪjgg#uQ_ڞg>s.8D]yipe$zyʒ"Ysj7N(‘ 䌩O}kn~֭ѼY[s~^UW[\5?JW|k"ߌ\Z̾@CK{t#Գ۷ٞgTI{*X|r=fE8/˿|$_oQ[-=c.d2ěٞ0 I|sڿj{BF6}l@  ``whpA@^0`+UW} l)U'y`ȠKjر*..֌3sٞ@ ꪫWU 2efϞ3!CnV]ve?I&鮻Rii~؞@بE?纮f͚5k{zzzӓs{{$)dgǻbLo1Juw+aC9.X}q1nd/ɢ')%L\&${?IkbJeͱJ-NeڵK#FիАWZJ֭|k_׿lرcFi{Fq CXh4fm߾]UUH5J;vPee9VqZ0ƨSÇ=% 2dB{}YGh4h4WUU{*++9iNwp:Tez̀H$iӦiʕϥR)\] naȐJ\rO3|#<-^XÆ SIIf͚-[:Lkk͛JUWWK/Ձx,nɒ%:3TQQZ]tEڼystwwkkf$5kco~c\5MMMy睦dhРA?? 44&L0+V0G̗ecLa>>na!XL5kVsj֬YZfeٵuV555:4cƌfUWWk̚5KE{{$Fبx<85z^ɓ{ٳёOɤxuuu O hΜ9T4tٻw^>NU$IG<ZSSjkk{}=&}|JtWs)"DT]]밇G:ZxWРn֤IaÆ9 $ /kC09bzWڞbĉat%hժUgeՎ;/Y+VPqq9@/%C P(G<777Ҫ{ӡM$jmmj…Zl~iȑ+?-tZ|DtkڴiZdL}{u466E~VZo]pXuuusZ  9$hڴiZresTJ+WTCCe5n8::::nݺР6566SO)JiƌY_-\P?z)7קM^͛}^++VXJM4);G$Rzzz 49s^ymذ!1}t͛7/߅rZ ~%z{L45{ٴi? 4/y$s뭷_|l۶c̍7hͯ~+/ /Ќ7:t(wN3֭3>0ag>c(_nO?mvޝ8x`0jFmz)ϛАz"0r9̆ СC͢El~k̪U֭[/l8y'1q%aLa`Awq=zD"34k׮=w|\r%ƘwZyuי:F̙3͛{3gLyy4MggcG: ${'}C/~fРAٽ{w6\p)))1C 1nxM} _0cƌ1H :̜93 ip4C!1;m|c'x"'x"'x"'x"'x"'x"'x"'x"'x"'x"'x"'x"'x"'x"'x"'x"'x"'x"'x"'x"'x"'x"'x"'x"'x"'x"'x"'x"'x"'x"'x"'x"'x"'x"'x"'x84svIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/docs/gwcs/ifu.rst0000644000175100001770000001174614573367100015371 0ustar00runnerdockerAn 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 Next, create the appropriate mapper object corresponding to the figure above: >>> # Ignore the details of how this mask is constructed; they are using >>> # array operations to generate the mask displayed for this example. >>> y, x = np.mgrid[:1000, :500] >>> fmask = (((-x + 0.01 * y + 0.00002 * y**2)/ 500) * 13 - 0.5) + 14 >>> mask = fmask.astype(np.int8) >>> mask[(mask % 2) == 1] = 0 >>> mask[mask > 13] = 0 >>> mask = mask // 2 >>> labelmapper = selector.LabelMapperArray(mask) 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. 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=labelmapper, ... 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 = labelmapper.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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/docs/gwcs/imaging_with_distortion.rst0000644000175100001770000000726014573367100021526 0ustar00runnerdocker.. _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 >>> crpix = (2048, 1024) >>> shift_by_crpix = models.Shift(-crpix[0]) & models.Shift(-crpix[1]) >>> 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 = [0, 1, 8.55e-06, -4.73e-10, 2.37e-14, 0, -5.20e-06, ... -3.98e-11, 1.97e-15, 2.17e-06, -5.23e-10, 3.47e-14, ... 1.08e-11, -2.46e-14, 1.49e-14] >>> poly_y = models.Polynomial2D(4) >>> poly_y.parameters = [0, 0, -1.75e-06, 8.57e-11, -1.77e-14, 1, 6.18e-06, ... -5.09e-10, -3.78e-15, -7.22e-06, -6.17e-11, ... -3.66e-14, -4.18e-10, 1.22e-14, -9.96e-15] >>> distortion = ((models.Shift(-crpix[0]) & models.Shift(-crpix[1])) | ... models.Mapping((0, 1, 0, 1)) | (poly_x & poly_y) | ... (models.Shift(crpix[0]) & models.Shift(crpix[1]))) >>> 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 Finally, save this WCS to an ``ASDF`` file: .. doctest-skip:: >>> from asdf import AsdfFile >>> tree = {"wcs": wcsobj} >>> wcs_file = AsdfFile(tree) >>> wcs_file.write_to("imaging_wcs_wdist.asdf") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/docs/gwcs/points_to_wcs.rst0000644000175100001770000001104114573367100017464 0ustar00runnerdocker .. _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.projcodes`. 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 will be used to fit the WCS information. The function requires tuples of arrays for x & y, and a `~astropy.coordinates.SkyCoord` object for sky coordinates. >>> 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])) >>> ra, dec = (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])) >>> radec = SkyCoord(ra, dec, unit=(u.deg, u.deg)) We can now choose the reference point on the sky for the projection. This can either be an `~astropy.coordinates.SkyCoord` object, or the string 'center' to use the geometric center of input points. In this example, we will specify exact coordinates for the fiducial. >>> 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/docs/gwcs/pure_asdf.rst0000644000175100001770000001235614573367100016554 0ustar00runnerdocker.. _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] ... ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/docs/gwcs/using_wcs.rst0000644000175100001770000001507614573367100016607 0ustar00runnerdocker.. _using_wcs_examples: Using the WCS object ==================== This section uses the ``imaging_wcs_wdist.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_wdist.asdf") >>> wcsobj = asdf_file.tree["wcs"] >>> print(wcsobj) # doctest: +SKIP From Transform ----------------- ---------------- detector distortion undistorted_frame linear_transform icrs None Inspecting Available Coordinate Frames -------------------------------------- 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")) Using Bounding Box ------------------ 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, 5.54527989]), array([ nan, -72.06454341])] 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() Manipulating Transforms ----------------------- 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 (-292.4150238489997, -616.8680129899999) 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', 'undistorted_frame', new_transform) >>> wcsobj(1, 2) # doctest: +FLOAT_CMP (5.501064280097802, -72.04557376712566) 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.002128560195604, -72.04557376712566) Inverse Transformations ----------------------- Often, it is useful to be able to compute inverse transformation that converts coordinates from the output frame back to the coordinates in the input frame. Note. the ``backward_transform`` attribute is equivalent to ``forward_transform.inverse``. In this section, for illustration purpose, we will be using the same 2D imaging WCS from ``imaging_wcs_wdist.asdf`` created in :ref:`imaging_example` whose forward transformation converts image coordinates to world coordinates and inverse transformation converts world coordinates back to image coordinates. .. doctest-skip:: >>> wcsobj = asdf.open(get_pkg_data_filename('imaging_wcs_wdist.asdf')).tree['wcs'] The most general method available for computing inverse coordinate transformation is the `WCS.invert() ` method. This method uses automatic or user-supplied analytical inverses whenever available to convert coordinates from the output frame to the input frame. When analytical inverse is not available as is the case for the ``wcsobj`` above, a numerical solution will be attempted using `WCS.numerical_inverse() `. Default parameters used by `WCS.numerical_inverse() ` or `WCS.invert() ` methods should be acceptable in most situations: .. doctest-skip:: >>> world = wcsobj(350, 200) >>> print(wcsobj.invert(*world)) # convert a single point (349.9999994163172, 200.00000017679295) >>> world = wcsobj([2, 350, -5000], [2, 200, 6000]) >>> print(wcsobj.invert(*world)) # convert multiple points at once (array([ 2.00000000e+00, 3.49999999e+02, -5.00000000e+03]), array([1.99999972e+00, 2.00000002e+02, 6.00000000e+03]) By default, parameter ``quiet`` is set to `True` in `WCS.numerical_inverse() ` and so it will return results "as is" without warning us about possible loss of accuracy or about divergence of the iterative process. In order to catch these kind of errors that can occur during numerical inversion, we need to turn off ``quiet`` mode and be prepared to catch `gwcs.wcs.NoConvergence` exceptions. In the next example, let's also add a point far away from the image for which numerical inverse fails. .. doctest-skip:: >>> from gwcs import NoConvergence >>> world = wcsobj([-85000, 2, 350, 3333, -5000], [-55000, 2, 200, 1111, 6000], ... with_bounding_box=False) >>> try: ... x, y = wcsobj.invert(*world, quiet=False, maxiter=40, ... detect_divergence=True, with_bounding_box=False) ... except NoConvergence as e: ... print(f"Indices of diverging points: {e.divergent}") ... print(f"Indices of poorly converging points: {e.slow_conv}") ... print(f"Best solution:\n{e.best_solution}") ... print(f"Achieved accuracy:\n{e.accuracy}") Indices of diverging points: [0] Indices of poorly converging points: [4] Best solution: [[ 1.38600585e+11 6.77595594e+11] [ 2.00000000e+00 1.99999972e+00] [ 3.49999999e+02 2.00000002e+02] [ 3.33300000e+03 1.11100000e+03] [-4.99999985e+03 5.99999985e+03]] Achieved accuracy: [[8.56497375e+02 5.09216089e+03] [6.57962988e-06 3.70445289e-07] [5.31656943e-06 2.72052603e-10] [6.81557583e-06 1.06560533e-06] [3.96365344e-04 6.41822468e-05]] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/docs/gwcs/wcs_ape.rst0000644000175100001770000000161514573367100016221 0ustar00runnerdocker.. _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. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/docs/gwcs/wcs_validation.rst0000644000175100001770000000220114573367100017576 0ustar00runnerdocker.. _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 serialize and deserialize GWCS objects (including transformations and frames) and to provide validation that the serialization is correct. ASDF makes use of abstract data type definitions called ``schemas``. The serialization and deserialization happens in classes, referred to as ``converters`` defined in ``gwcs.converters.*`` modules. Most of the schemas available for the WCS object, coordinate frames and some WCS specific transforms live in the `asdf-wcs-schemas package `_. 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 `asdf-astropy `_. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/docs/gwcs/wcstools.rst0000644000175100001770000000236714573367100016462 0ustar00runnerdockerWCS 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/docs/index.rst0000644000175100001770000003316514573367100014751 0ustar00runnerdocker.. _gwcs: GWCS 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: Basic Structure of a GWCS Object -------------------------------- The key concept to be aware of is that a GWCS Object consists of a pipeline of steps; each step contains a transform (i.e., an Astropy model) that converts the input coordinates of the step to the output coordinates of the step. Furthermore, each step has an optional coordinate frame associated with the step. The coordinate frame represents the input coordinate frame, not the output coordinates. Most typically, the first step coordinate frame is the detector pixel coordinates (the default). Since no step has a coordinate frame for the output coordinates, it is necessary to append a step with no transform to the end of the pipeline to represent the output coordinate frame. For imaging, this frame typically references one of the Astropy standard Sky Coordinate Frames of Reference. The GWCS frames also serve to hold the units on the axes, the names of the axes and the physical type of the axis (e.g., wavelength). Since it is often useful to obtain coordinates in an intermediate frame of reference, GWCS allows the pipeline to consist of more than one transform. For example, for spectrographs, it is useful to have access to coordinates in the slit plane, and in such a case, the first step would transform from the detector to the slit plane, and the second step from the slit plane to sky coordinates and a wavelength. Constructed this way, it is possible to extract from the GWCS the needed transforms between identified frames of reference. The GWCS object can be saved to the ASDF format using the `asdf `__ package and validated using `ASDF Standard `__ There are two ways to save the GWCS object to a files: - `Save a WCS object as a pure ASDF file`_ - `Save a WCS object as a pure ASDF file`_ A step-by-step example of constructing an imaging GWCS object. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The following example shows how to construct a GWCS object that maps input pixel coordinates to sky coordinates. This example involves 4 sequential transformations: - Adjusting pixel coordinates such that the center of the array has (0, 0) value (typical of most WCS definitions, but any pixel may be the reference that is tied to the sky reference, even the (0, 0) pixel, or even pixels outside of the detector). - Scaling pixels such that the center pixel of the array has the expected angular scale. (I.e., applying the plate scale) - Projecting the resultant coordinates onto the sky using the tangent projection. If the field of view is small, the inaccuracies resulting leaving this out will be small; however, this is generally applied. - Transforming the center pixel to the appropriate celestial coordinate with the approprate orientation on the sky. For simplicity's sake, we assume the detector array is already oriented with north up, and that the array has the appropriate parity as the sky coordinates. The detector has a 1000 pixel by 1000 pixel array. For simplicity, no units will be used, but instead will be implicit. The following imports are generally useful: .. doctest-skip:: >>> 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 In the following transformation definitions, angular units are in degrees by default. .. doctest-skip:: >>> pixelshift = models.Shift(-500) & models.Shift(-500) >>> pixelscale = models.Scale(0.1 / 3600.) & models.Scale(0.1 / 3600.) # 0.1 arcsec/pixel >>> tangent_projection = models.Pix2Sky_TAN() >>> celestial_rotation = models.RotateNative2Celestial(30., 45., 180.) For the last transformation, the three arguments are, respectively: - Celestial longitude (i.e., RA) of the fiducial point (e.g., (0, 0) in the input spherical coordinates). In this case we put the detector center at 30 degrees (RA = 2 hours) - Celestial latitude (i.e., Dec) of the fiducial point. Here Dec = 45 degrees. - Longitude of celestial pole in input coordinate system. With north up, this always corresponds to a value of 180. The more general case where the detector is not aligned with north, would have a rotation transform after the pixelshift and pixelscale transformations to align the detector coordinates with north up. The net transformation from pixel coordinates to celestial coordinates then becomes: .. doctest-skip:: >>> det2sky = pixelshift | pixelscale | tangent_projection | celestial_rotation The remaining elements to defining the WCS are he input and output frames of reference. While the GWCS scheme allows intermediate frames of reference, this example doesn't have any. The output frame is expressed with no associated transform .. doctest-skip:: >>> 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)) >>> wcsobj = wcs.WCS([(detector_frame, det2sky), ... (sky_frame, None) ... ] >>> 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: .. doctest-skip:: >>> sky = wcsobj(1, 2) >>> 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. .. doctest-skip:: >>> 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` 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'] 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/points_to_wcs.rst gwcs/fits_analog.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 .. automodapi:: gwcs.geometry ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/docs/make.bat0000644000175100001770000001064114573367100014507 0ustar00runnerdocker@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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/docs/rtd_environment.yaml0000644000175100001770000000014214573367100017176 0ustar00runnerdockername: rtd311 channels: - conda-forge - defaults dependencies: - python=3.11 - pip - graphviz ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1710091850.3471649 gwcs-0.21.0/gwcs/0000755000175100001770000000000014573367112013116 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/gwcs/__init__.py0000644000175100001770000000431014573367100015222 0ustar00runnerdocker# 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 importlib.metadata try: __version__ = importlib.metadata.version(__name__) except importlib.metadata.PackageNotFoundError: # 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/gwcs/api.py0000644000175100001770000003245214573367100014244 0ustar00runnerdocker# 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`). """ pixel_arrays = index_arrays[::-1] return self.pixel_to_world_values(*pixel_arrays) 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. """ result = self.world_to_pixel_values(*world_arrays) if self.pixel_n_dim != 1: result = result[::-1] return result @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). """ if self._pixel_shape is None: return None else: return self._pixel_shape[::-1] @array_shape.setter def array_shape(self, value): if value is None: self._pixel_shape = None else: self._pixel_shape = value[::-1] @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 self.input_frame.naxes > 1: first_res = result[0] if not utils.isnumerical(first_res): result = [i.value for i in result] else: if not utils.isnumerical(result): result = result.value 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) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1710091850.3471649 gwcs-0.21.0/gwcs/converters/0000755000175100001770000000000014573367112015310 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/gwcs/converters/__init__.py0000644000175100001770000000013014573367100017410 0ustar00runnerdocker# Licensed under a 3-clause BSD style license - see LICENSE.rst # -*- coding: utf-8 -*- ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/gwcs/converters/geometry.py0000644000175100001770000000522214573367100017513 0ustar00runnerdocker""" ASDF tags for geometry related models. """ from asdf_astropy.converters.transform.core import TransformConverterBase __all__ = ['DirectionCosinesConverter', 'SphericalCartesianConverter'] class DirectionCosinesConverter(TransformConverterBase): tags = ["tag:stsci.edu:gwcs/direction_cosines-*"] types = ["gwcs.geometry.ToDirectionCosines", "gwcs.geometry.FromDirectionCosines"] def from_yaml_tree_transform(self, node, tag, ctx): from ..geometry import ToDirectionCosines, FromDirectionCosines 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}") def to_yaml_tree_transform(self, model, tag, ctx): from ..geometry import ToDirectionCosines, FromDirectionCosines 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 node class SphericalCartesianConverter(TransformConverterBase): tags = ["tag:stsci.edu:gwcs/spherical_cartesian-*"] types = ["gwcs.geometry.SphericalToCartesian", "gwcs.geometry.CartesianToSpherical"] def from_yaml_tree_transform(self, node, tag, ctx): from ..geometry import SphericalToCartesian, CartesianToSpherical transform_type = node['transform_type'] wrap_lon_at = node['wrap_lon_at'] if transform_type == 'spherical_to_cartesian': return SphericalToCartesian(wrap_lon_at=wrap_lon_at) elif transform_type == 'cartesian_to_spherical': return CartesianToSpherical(wrap_lon_at=wrap_lon_at) else: raise TypeError(f"Unknown model_type {transform_type}") def to_yaml_tree_transform(self, model, tag, ctx): from ..geometry import SphericalToCartesian, CartesianToSpherical if isinstance(model, SphericalToCartesian): transform_type = 'spherical_to_cartesian' elif isinstance(model, CartesianToSpherical): transform_type = 'cartesian_to_spherical' else: raise TypeError(f"Model of type {model.__class__} is not supported.") node = { 'transform_type': transform_type, 'wrap_lon_at': model.wrap_lon_at } return node ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/gwcs/converters/selector.py0000644000175100001770000001135714573367100017506 0ustar00runnerdocker# Licensed under a 3-clause BSD style license - see LICENSE.rst # -*- coding: utf-8 -*- from collections import OrderedDict import numpy as np from astropy.modeling import models from astropy.modeling.core import Model from astropy.utils.misc import isiterable from asdf.tags.core.ndarray import NDArrayType from asdf_astropy.converters.transform.core import TransformConverterBase __all__ = ['LabelMapperConverter', 'RegionsSelectorConverter'] class LabelMapperConverter(TransformConverterBase): tags = ["tag:stsci.edu:gwcs/label_mapper-*"] types = ["gwcs.selector.LabelMapperArray", "gwcs.selector.LabelMapperDict", "gwcs.selector.LabelMapperRange", "gwcs.selector.LabelMapper"] def from_yaml_tree_transform(self, node, tag, ctx): from ..selector import (LabelMapperArray, LabelMapperDict, LabelMapperRange, LabelMapper) 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', 1e-8) no_label = node.get('no_label', np.nan) if isinstance(mapper, NDArrayType): if mapper.ndim != 2: raise NotImplementedError("GWCS currently only supports 2D 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) def to_yaml_tree_transform(self, model, tag, ctx): from ..selector import (LabelMapperArray, LabelMapperDict, LabelMapperRange, LabelMapper) 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 node class RegionsSelectorConverter(TransformConverterBase): tags = ["tag:stsci.edu:gwcs/regions_selector-*"] types = ["gwcs.selector.RegionsSelector"] def from_yaml_tree_transform(self, node, tag, ctx): from ..selector import RegionsSelector 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) def to_yaml_tree_transform(self, model, tag, 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 node ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/gwcs/converters/spectroscopy.py0000644000175100001770000001032314573367100020413 0ustar00runnerdocker""" ASDF tags for spectroscopy related models. """ from astropy import units as u from asdf_astropy.converters.transform.core import ( TransformConverterBase, parameter_to_value ) __all__ = ['GratingEquationConverter', 'SellmeierGlassConverter', 'SellmeierZemaxConverter', 'Snell3DConverter'] class SellmeierGlassConverter(TransformConverterBase): tags = ["tag:stsci.edu:gwcs/sellmeier_glass-*"] types = ["gwcs.spectroscopy.SellmeierGlass"] def from_yaml_tree_transform(self, node, tag, ctx): from ..spectroscopy import SellmeierGlass return SellmeierGlass(node['B_coef'], node['C_coef']) def to_yaml_tree_transform(self, model, tag, ctx): node = {'B_coef': parameter_to_value(model.B_coef), 'C_coef': parameter_to_value(model.C_coef)} return node class SellmeierZemaxConverter(TransformConverterBase): tags = ["tag:stsci.edu:gwcs/sellmeier_zemax-*"] types = ["gwcs.spectroscopy.SellmeierZemax"] def from_yaml_tree_transform(self, node, tag, ctx): from ..spectroscopy import SellmeierZemax return SellmeierZemax(node['temperature'], node['ref_temperature'], node['ref_pressure'], node['pressure'], node['B_coef'], node['C_coef'], node['D_coef'], node['E_coef']) def to_yaml_tree_transform(self, model, tag, 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 node class Snell3DConverter(TransformConverterBase): tags = ["tag:stsci.edu:gwcs/snell3d-*"] types = ["gwcs.spectroscopy.Snell3D"] def from_yaml_tree_transform(self, node, tag, ctx): from ..spectroscopy import Snell3D return Snell3D() def to_yaml_tree_transform(self, model, tag, ctx): return {} class GratingEquationConverter(TransformConverterBase): tags = ["tag:stsci.edu:gwcs/grating_equation-*"] types = ["gwcs.spectroscopy.AnglesFromGratingEquation3D", "gwcs.spectroscopy.WavelengthFromGratingEquation"] def from_yaml_tree_transform(self, node, tag, ctx): from ..spectroscopy import (AnglesFromGratingEquation3D, WavelengthFromGratingEquation) 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 def to_yaml_tree_transform(self, model, tag, ctx): from ..spectroscopy import (AnglesFromGratingEquation3D, WavelengthFromGratingEquation) 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 node ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1710091850.3511648 gwcs-0.21.0/gwcs/converters/tests/0000755000175100001770000000000014573367112016452 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/gwcs/converters/tests/__init__.py0000644000175100001770000000000014573367100020546 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/gwcs/converters/tests/test_selector.py0000644000175100001770000001022414573367100021677 0ustar00runnerdocker# Licensed under a 3-clause BSD style license - see LICENSE.rst # -*- coding: utf-8 -*- import numpy as np from numpy.testing import assert_array_equal from astropy.modeling.models import Mapping, Shift, Scale, Polynomial2D import asdf from ...tests.test_region import create_scalar_mapper from ... import selector def _assert_mapper_equal(a, b): __tracebackhide__ = True if a is None: return assert type(a) is type(b) if isinstance(a.mapper, dict): assert(a.mapper.__class__ == b.mapper.__class__) # nosec assert(np.isin(list(a.mapper), list(b.mapper)).all()) # 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) def _assert_selector_equal(a, b): __tracebackhide__ = True if a is None: return if isinstance(a, selector.RegionsSelector): assert type(a) is type(b) _assert_mapper_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) def assert_selector_roundtrip(s, tmpdir, version=None): """ Assert that a selector can be written to an ASDF file and read back in without losing any of its essential properties. """ path = str(tmpdir / "test.asdf") with asdf.AsdfFile({"selector": s}, version=version) as af: af.write_to(path) with asdf.open(path) as af: rs = af["selector"] if isinstance(s, selector.RegionsSelector): _assert_selector_equal(s, rs) elif isinstance(s, selector._LabelMapper): _assert_mapper_equal(s, rs) else: assert False 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) assert_selector_roundtrip(rs, tmpdir) def test_LabelMapperArray_str(tmpdir): a = np.array([["label1", "", "label2"], ["label1", "", ""], ["label1", "label2", "label2"]]) mask = selector.LabelMapperArray(a) assert_selector_roundtrip(mask, tmpdir) def test_labelMapperArray_int(tmpdir): a = np.array([[1, 0, 2], [1, 0, 0], [1, 2, 2]]) mask = selector.LabelMapperArray(a) assert_selector_roundtrip(mask, tmpdir) def test_LabelMapperDict(tmpdir): dmapper = create_scalar_mapper() sel = selector.LabelMapperDict(('x', 'y'), dmapper, inputs_mapping=Mapping((0,), n_inputs=2), atol=1e-3) assert_selector_roundtrip(sel, tmpdir) 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)) assert_selector_roundtrip(sel, tmpdir) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/gwcs/converters/tests/test_transforms.py0000644000175100001770000000310114573367100022251 0ustar00runnerdocker# Licensed under a 3-clause BSD style license - see LICENSE.rst # -*- coding: utf-8 -*- import pytest from astropy.modeling.models import Identity from astropy import units as u try: from asdf_astropy.testing.helpers import assert_model_roundtrip except ImportError: from asdf_astropy.converters.transform.tests.test_transform import assert_model_roundtrip 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() tocart = geometry.SphericalToCartesian() tospher = geometry.CartesianToSpherical() transforms = [todircos, fromdircos, tospher, tocart, 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): assert_model_roundtrip(model, tmpdir) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/gwcs/converters/tests/test_wcs.py0000644000175100001770000001373314573367100020663 0ustar00runnerdocker# Licensed under a 3-clause BSD style license - see LICENSE.rst # -*- coding: utf-8 -*- import os.path import pytest astropy = pytest.importorskip('astropy', minversion='3.0') from astropy.modeling import models # noqa: E402 from astropy import coordinates as coord # noqa: E402 from astropy import units as u # noqa: E402 from astropy import time # noqa: E402 import asdf # noqa: E402 from asdf_astropy.testing.helpers import ( # noqa: E402 assert_model_equal) from ... import coordinate_frames as cf # noqa: E402 from ... import wcs # noqa: E402 def _assert_frame_equal(a, b): __tracebackhide__ = True assert type(a) is type(b) if a is None: return if not isinstance(a, cf.CoordinateFrame): return a == b assert a.name == b.name # nosec assert a.axes_order == b.axes_order # nosec assert a.axes_names == b.axes_names # nosec assert a.unit == b.unit # nosec assert a.reference_frame == b.reference_frame # nosec def assert_frame_roundtrip(frame, tmpdir, version=None): """ Assert that a frame can be written to an ASDF file and read back in without losing any of its essential properties. """ path = str(tmpdir / "test.asdf") with asdf.AsdfFile({"frame": frame}, version=version) as af: af.write_to(path) with asdf.open(path) as af: _assert_frame_equal(frame, af["frame"]) def _assert_wcs_equal(a, b): assert a.name == b.name # nosec assert a.pixel_shape == b.pixel_shape assert len(a.available_frames) == len(b.available_frames) # nosec for a_step, b_step in zip(a.pipeline, b.pipeline): _assert_frame_equal(a_step.frame, b_step.frame) assert_model_equal(a_step.transform, b_step.transform) def assert_wcs_roundtrip(wcs, tmpdir, version=None): path = str(tmpdir / "test.asdf") with asdf.AsdfFile({"wcs": wcs}, version=version) as af: af.write_to(path) with asdf.open(path) as af: _assert_wcs_equal(wcs, af["wcs"]) def test_create_wcs(tmpdir): m1 = models.Shift(12.4) & models.Shift(-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) gw4 = wcs.WCS(output_frame=icrs, input_frame=det, forward_transform=m1) gw4.pixel_shape = (100, 200) assert_wcs_roundtrip(gw1, tmpdir) assert_wcs_roundtrip(gw2, tmpdir) assert_wcs_roundtrip(gw3, tmpdir) assert_wcs_roundtrip(gw4, 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, ))]) assert_frame_roundtrip(comp, tmpdir) assert_frame_roundtrip(comp1, tmpdir) assert_frame_roundtrip(comp2, tmpdir) def create_test_frames(): """Creates an array of frames to be used for testing.""" 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): frames = create_test_frames() for f in frames: assert_frame_roundtrip(f, tmpdir) def test_references(tmpdir): m1 = models.Shift(12.4) & models.Shift(-2) icrs = cf.CelestialFrame(name='icrs', reference_frame=coord.ICRS()) det = cf.Frame2D(name='detector', axes_order=(0, 1)) focal = cf.Frame2D(name='focal', axes_order=(0, 1)) pipe1 = [(det, m1), (focal, m1), (icrs, None)] gw1 = wcs.WCS(pipe1) pipe2 = [(det, m1), (det, m1), (icrs, None)] gw2 = wcs.WCS(pipe2) tree = {'wcs1': gw1, 'wcs2': gw2} af = asdf.AsdfFile(tree) output_path = os.path.join(str(tmpdir), "test.asdf") af.write_to(output_path) with asdf.open(output_path) as af: gw1 = af.tree['wcs1'] gw2 = af.tree['wcs2'] assert gw1.pipeline[0].transform is gw1.pipeline[1].transform assert gw2.pipeline[0].transform is gw2.pipeline[1].transform assert gw2.pipeline[0].frame is gw2.pipeline[1].frame ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/gwcs/converters/wcs.py0000644000175100001770000001410414573367100016453 0ustar00runnerdocker# Licensed under a 3-clause BSD style license - see LICENSE.rst # -*- coding: utf-8 -*- from asdf.extension import Converter __all__ = ["WCSConverter", "CelestialFrameConverter", "CompositeFrameConverter", "FrameConverter", "SpectralFrameConverter", "StepConverter", "TemporalFrameConverter", "StokesFrameConverter"] class WCSConverter(Converter): tags = ["tag:stsci.edu:gwcs/wcs-*"] types = ["gwcs.wcs.WCS"] def from_yaml_tree(self, node, tag, ctx): from ..wcs import WCS gwcsobj = WCS(node['steps'], name=node['name']) if 'pixel_shape' in node: gwcsobj.pixel_shape = node['pixel_shape'] return gwcsobj def to_yaml_tree(self, gwcsobj, tag, ctx): return { 'name': gwcsobj.name, 'steps': gwcsobj.pipeline, 'pixel_shape': gwcsobj.pixel_shape, } class StepConverter(Converter): tags = ["tag:stsci.edu:gwcs/step-*"] types = ["gwcs.wcs.Step"] def from_yaml_tree(self, node, tag, ctx): from ..wcs import Step return Step(frame=node['frame'], transform=node.get('transform', None)) def to_yaml_tree(self, step, tag, ctx): return { 'frame': step.frame, 'transform': step.transform } class FrameConverter(Converter): tags = ["tag:stsci.edu:gwcs/frame-*"] types = ["gwcs.coordinate_frames.CoordinateFrame"] def _from_yaml_tree(self, node, tag, 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'] = node['reference_frame'] if 'axes_order' in node: kwargs['axes_order'] = tuple(node['axes_order']) if 'unit' in node: kwargs['unit'] = tuple(node['unit']) if 'axis_physical_types' in node: kwargs['axis_physical_types'] = tuple(node['axis_physical_types']) return kwargs def _to_yaml_tree(self, frame, tag, ctx): from ..coordinate_frames import CoordinateFrame 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'] = frame.reference_frame if frame.unit is not None: node['unit'] = list(frame.unit) if frame.axis_physical_types is not None: node['axis_physical_types'] = list(frame.axis_physical_types) return node def from_yaml_tree(self, node, tag, ctx): from ..coordinate_frames import CoordinateFrame node = self._from_yaml_tree(node, tag, ctx) return CoordinateFrame(**node) def to_yaml_tree(self, frame, tag, ctx): return self._to_yaml_tree(frame, tag, ctx) class Frame2DConverter(FrameConverter): tags = ["tag:stsci.edu:gwcs/frame2d-*"] types = ["gwcs.coordinate_frames.Frame2D"] def from_yaml_tree(self, node, tag, ctx): from ..coordinate_frames import Frame2D node = self._from_yaml_tree(node, tag, ctx) return Frame2D(**node) class CelestialFrameConverter(FrameConverter): tags = ["tag:stsci.edu:gwcs/celestial_frame-*"] types = ["gwcs.coordinate_frames.CelestialFrame"] def from_yaml_tree(self, node, tag, ctx): from ..coordinate_frames import CelestialFrame node = self._from_yaml_tree(node, tag, ctx) return CelestialFrame(**node) class SpectralFrameConverter(FrameConverter): tags = ["tag:stsci.edu:gwcs/spectral_frame-*"] types = ["gwcs.coordinate_frames.SpectralFrame"] def from_yaml_tree(self, node, tag, ctx): from ..coordinate_frames import SpectralFrame node = self._from_yaml_tree(node, tag, ctx) if 'reference_position' in node: node['reference_position'] = node['reference_position'].upper() return SpectralFrame(**node) def to_yaml_tree(self, frame, tag, ctx): node = self._to_yaml_tree(frame, tag, ctx) if frame.reference_position is not None: node['reference_position'] = frame.reference_position.lower() return node class CompositeFrameConverter(FrameConverter): tags = ["tag:stsci.edu:gwcs/composite_frame-*"] types = ["gwcs.coordinate_frames.CompositeFrame"] def from_yaml_tree(self, node, tag, ctx): from ..coordinate_frames import CompositeFrame if len(node) != 2: raise ValueError("CompositeFrame has extra properties") name = node['name'] frames = node['frames'] return CompositeFrame(frames, name) def to_yaml_tree(self, frame, tag, ctx): return { 'name': frame.name, 'frames': frame.frames } class TemporalFrameConverter(FrameConverter): tags = ["tag:stsci.edu:gwcs/temporal_frame-*"] types = ["gwcs.coordinate_frames.TemporalFrame"] def from_yaml_tree(self, node, tag, ctx): from ..coordinate_frames import TemporalFrame node = self._from_yaml_tree(node, tag, ctx) return TemporalFrame(**node) class StokesFrameConverter(FrameConverter): tags = ["tag:stsci.edu:gwcs/stokes_frame-*"] types = ["gwcs.coordinate_frames.StokesFrame"] def from_yaml_tree(self, node, tag, ctx): from ..coordinate_frames import StokesFrame node = self._from_yaml_tree(node, tag, ctx) return StokesFrame(**node) def to_yaml_tree(self, frame, tag, ctx): node = {} node['name'] = frame.name if frame.axes_order: node['axes_order'] = list(frame.axes_order) return node ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/gwcs/coordinate_frames.py0000644000175100001770000006733114573367100017163 0ustar00runnerdocker# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Defines coordinate frames and ties them to data axes. """ from collections import defaultdict 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) from astropy.wcs.wcsapi.fitswcs import CTYPE_TO_UCD1 from astropy.coordinates import StokesCoord __all__ = ['Frame2D', 'CelestialFrame', 'SpectralFrame', 'CompositeFrame', 'CoordinateFrame', 'TemporalFrame', 'StokesFrame'] def _ucd1_to_ctype_name_mapping(ctype_to_ucd, allowed_ucd_duplicates): inv_map = {} new_ucd = set() for kwd, ucd in ctype_to_ucd.items(): if ucd in inv_map: if ucd not in allowed_ucd_duplicates: new_ucd.add(ucd) continue elif ucd in allowed_ucd_duplicates: inv_map[ucd] = allowed_ucd_duplicates[ucd] else: inv_map[ucd] = kwd if new_ucd: logging.warning( "Found unsupported duplicate physical type in 'astropy' mapping to CTYPE.\n" "Update 'gwcs' to the latest version or notify 'gwcs' developer.\n" "Duplicate physical types will be mapped to the following CTYPEs:\n" + '\n'.join([f'{repr(ucd):s} --> {repr(inv_map[ucd]):s}' for ucd in new_ucd]) ) return inv_map # List below allowed physical type duplicates and a corresponding CTYPE # to which all duplicates will be mapped to: _ALLOWED_UCD_DUPLICATES = { 'time': 'TIME', 'em.wl': 'WAVE', } UCD1_TO_CTYPE = _ucd1_to_ctype_name_mapping( ctype_to_ucd=CTYPE_TO_UCD1, allowed_ucd_duplicates=_ALLOWED_UCD_DUPLICATES ) 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"] def get_ctype_from_ucd(ucd): """ Return the FITS ``CTYPE`` corresponding to a UCD1 value. Parameters ---------- ucd : str UCD string, for example one of ```WCS.world_axis_physical_types``. Returns ------- CTYPE : str The corresponding FITS ``CTYPE`` value or an empty string. """ return UCD1_TO_CTYPE.get(ucd, "") 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__() # _axis_physical_types holds any user supplied physical types self._axis_physical_types = self._set_axis_physical_types(axis_physical_types) def _set_axis_physical_types(self, pht): """ 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) 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""" coo = tuple([arg * un if not hasattr(arg, "to") else arg.to(un) for arg, un in zip(args, self.unit)]) if self.naxes == 1: return coo[0] 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 # If coords is a 1-tuple of quantity then return the element of the tuple # This aligns the behavior with the other implementations if not hasattr(coords, 'unit') and len(coords) == 1: return coords[0] return coords @property def _default_axis_physical_types(self): """ The default physical types to use for this frame if none are specified by the user. """ return tuple("custom:{}".format(t) for t in self.axes_type) @property def axis_physical_types(self): """ The axis physical types for this frame. These physical types are the types in frame order, not transform order. """ return self._axis_physical_types or self._default_axis_physical_types @property def _world_axis_object_classes(self): return {f"{at}{i}" if i != 0 else at: (u.Quantity, (), {'unit': unit}) for i, (at, unit) in enumerate(zip(self._axes_type, self.unit))} @property def _world_axis_object_components(self): return [(f"{at}{i}" if i != 0 else at, 0, 'value') for i, at in enumerate(self._axes_type)] 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 _default_axis_physical_types(self): if isinstance(self.reference_frame, coord.Galactic): return "pos.galactic.lon", "pos.galactic.lat" elif isinstance(self.reference_frame, (coord.GeocentricTrueEcliptic, coord.GCRS, coord.PrecessedGeocentric)): return "pos.bodyrc.lon", "pos.bodyrc.lat" elif isinstance(self.reference_frame, coord.builtin_frames.BaseRADecFrame): return "pos.eq.ra", "pos.eq.dec" elif isinstance(self.reference_frame, coord.builtin_frames.BaseEclipticFrame): return "pos.ecliptic.lon", "pos.ecliptic.lat" else: return tuple("custom:{}".format(t) for t in self.axes_names) @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 _default_axis_physical_types(self): if self.unit[0].physical_type == "frequency": return ("em.freq",) elif self.unit[0].physical_type == "length": return ("em.wl",) elif self.unit[0].physical_type == "energy": return ("em.energy",) elif self.unit[0].physical_type == "speed": return ("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: return ("custom:{}".format(self.unit[0].physical_type),) @property def _world_axis_object_classes(self): return {'spectral': ( coord.SpectralCoord, (), {'unit': self.unit[0]})} @property def _world_axis_object_components(self): return [('spectral', 0, 'value')] def coordinates(self, *args): # using SpectralCoord if isinstance(args[0], coord.SpectralCoord): return args[0].to(self.unit[0]) else: if hasattr(args[0], 'unit'): return coord.SpectralCoord(*args).to(self.unit[0]) else: return coord.SpectralCoord(*args, 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 _default_axis_physical_types(self): return ("time",) @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): if isinstance(self.reference_frame.value, np.ndarray): return [('temporal', 0, 'value')] def offset_from_time_and_reference(time): return (time - self.reference_frame).sec return [('temporal', 0, offset_from_time_and_reference)] 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(dt, time.TimeDelta) and isinstance(dt, time.Time) or isinstance(self.reference_frame.value, np.ndarray)): return time.Time(dt, **kwargs) if not hasattr(dt, 'unit'): dt = dt * unit return self.reference_frame + dt 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] if isinstance(coords[0], np.ndarray): return coords[0] * self.unit[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 _wao_classes_rename_map(self): mapper = defaultdict(dict) seen_names = [] for frame in self.frames: # ensure the frame is in the mapper mapper[frame] for key in frame._world_axis_object_classes.keys(): if key in seen_names: new_key = f"{key}{seen_names.count(key)}" mapper[frame][key] = new_key seen_names.append(key) return mapper @property def _wao_renamed_components_iter(self): mapper = self._wao_classes_rename_map for frame in self.frames: renamed_components = [] for comp in frame._world_axis_object_components: comp = list(comp) rename = mapper[frame].get(comp[0]) if rename: comp[0] = rename renamed_components.append(tuple(comp)) yield frame, renamed_components @property def _wao_renamed_classes_iter(self): mapper = self._wao_classes_rename_map for frame in self.frames: for key, value in frame._world_axis_object_classes.items(): rename = mapper[frame].get(key) if rename: key = rename yield key, value @property def _world_axis_object_components(self): """ We need to generate the components respecting the axes_order. """ out = [None] * self.naxes for frame, components in self._wao_renamed_components_iter: for i, ao in enumerate(frame.axes_order): out[ao] = 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): return dict(self._wao_renamed_classes_iter) class StokesFrame(CoordinateFrame): """ A coordinate frame for representing Stokes polarisation states. Parameters ---------- name : str Name of this frame. axes_order : tuple A dimension in the data that corresponds to this axis. """ def __init__(self, axes_order=(0,), axes_names=("stokes",), name=None, axis_physical_types=None): super(StokesFrame, self).__init__(1, ["STOKES"], axes_order, name=name, axes_names=axes_names, unit=u.one, axis_physical_types=axis_physical_types) @property def _default_axis_physical_types(self): return ("phys.polarization.stokes",) @property def _world_axis_object_classes(self): return {'stokes': ( StokesCoord, (), {}, )} @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 StokesCoord(arg) def coordinate_to_quantity(self, *coords): if isinstance(coords[0], StokesCoord): return coords[0].value << u.one 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) @property def _default_axis_physical_types(self): if all(self.axes_names): ph_type = self.axes_names else: ph_type = self.axes_type return tuple("custom:{}".format(t) for t in ph_type) 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/gwcs/extension.py0000644000175100001770000000550114573367100015502 0ustar00runnerdocker# Licensed under a 3-clause BSD style license - see LICENSE.rst import importlib.resources from asdf.extension import Extension, ManifestExtension from .converters.wcs import ( CelestialFrameConverter, CompositeFrameConverter, FrameConverter, Frame2DConverter, SpectralFrameConverter, StepConverter, StokesFrameConverter, TemporalFrameConverter, WCSConverter, ) from .converters.selector import ( LabelMapperConverter, RegionsSelectorConverter ) from .converters.spectroscopy import ( GratingEquationConverter, SellmeierGlassConverter, SellmeierZemaxConverter, Snell3DConverter ) from .converters.geometry import ( DirectionCosinesConverter, SphericalCartesianConverter ) WCS_MODEL_CONVERTERS = [ CelestialFrameConverter(), CompositeFrameConverter(), FrameConverter(), Frame2DConverter(), SpectralFrameConverter(), StepConverter(), StokesFrameConverter(), TemporalFrameConverter(), WCSConverter(), LabelMapperConverter(), RegionsSelectorConverter(), GratingEquationConverter(), SellmeierGlassConverter(), SellmeierZemaxConverter(), Snell3DConverter(), DirectionCosinesConverter(), SphericalCartesianConverter(), ] # The order here is important; asdf will prefer to use extensions # that occur earlier in the list. WCS_MANIFEST_URIS = [ f"asdf://asdf-format.org/astronomy/gwcs/manifests/{path.stem}" for path in sorted((importlib.resources.files("asdf_wcs_schemas.resources") / "manifests").iterdir(), reverse=True) ] # 1.0.0 contains multiple versions of the same tag, a bug fixed in # 1.0.1 so only register 1.0.0 if it's the only available manifest TRANSFORM_EXTENSIONS = [ ManifestExtension.from_uri( uri, legacy_class_names=["gwcs.extension.GWCSExtension"], converters=WCS_MODEL_CONVERTERS, ) for uri in WCS_MANIFEST_URIS if len(WCS_MANIFEST_URIS) == 1 or '1.0.0' not in uri ] # if we don't register something for the 1.0.0 extension/manifest # opening old files will issue AsdfWarning messages stating that # the file was produced with an extension that is not installed # As the 1.0.1 and 1.1.0 extensions support all the required tags # it's not a helpful warning so here we register an 'empty' # extension for 1.0.0 which doesn't support any tags or types # but will be installed into asdf preventing the warning if len(TRANSFORM_EXTENSIONS) > 1: class _EmptyExtension(Extension): extension_uri = 'asdf://asdf-format.org/astronomy/gwcs/extensions/gwcs-1.0.0' legacy_class_names=["gwcs.extension.GWCSExtension"] TRANSFORM_EXTENSIONS.append(_EmptyExtension()) def get_extensions(): """ Get the gwcs.converters extension. This method is registered with the asdf.extensions entry point. Returns ------- list of asdf.extension.Extension """ return TRANSFORM_EXTENSIONS ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/gwcs/geometry.py0000644000175100001770000001453614573367100015331 0ustar00runnerdocker# Licensed under a 3-clause BSD style license - see LICENSE.rst """ Models for general analytical geometry transformations. """ import numbers import numpy as np from astropy.modeling.core import Model from astropy import units as u __all__ = ['ToDirectionCosines', 'FromDirectionCosines', 'SphericalToCartesian', 'CartesianToSpherical'] 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() class SphericalToCartesian(Model): """ Convert spherical coordinates on a unit sphere to cartesian coordinates. Spherical coordinates when not provided as ``Quantity`` are assumed to be in degrees with ``lon`` being the *longitude (or azimuthal) angle* ``[0, 360)`` (or ``[-180, 180)``) and angle ``lat`` is the *latitude* (or *elevation angle*) in range ``[-90, 90]``. """ _separable = False _input_units_allow_dimensionless = True n_inputs = 2 n_outputs = 3 def __init__(self, wrap_lon_at=360, **kwargs): """ Parameters ---------- wrap_lon_at : {360, 180}, optional An **integer number** that specifies the range of the longitude (azimuthal) angle. When ``wrap_lon_at`` is 180, the longitude angle will have a range of ``[-180, 180)`` and when ``wrap_lon_at`` is 360 (default), the longitude angle will have a range of ``[0, 360)``. """ super().__init__(**kwargs) self.inputs = ('lon', 'lat') self.outputs = ('x', 'y', 'z') self.wrap_lon_at = wrap_lon_at @property def wrap_lon_at(self): """ An **integer number** that specifies the range of the longitude (azimuthal) angle. Allowed values are 180 and 360. When ``wrap_lon_at`` is 180, the longitude angle will have a range of ``[-180, 180)`` and when ``wrap_lon_at`` is 360 (default), the longitude angle will have a range of ``[0, 360)``. """ return self._wrap_lon_at @wrap_lon_at.setter def wrap_lon_at(self, wrap_angle): if not (isinstance(wrap_angle, numbers.Integral) and wrap_angle in [180, 360]): raise ValueError("'wrap_lon_at' must be an integer number: 180 or 360") self._wrap_lon_at = wrap_angle def evaluate(self, lon, lat): if isinstance(lon, u.Quantity) != isinstance(lat, u.Quantity): raise TypeError("All arguments must be of the same type " "(i.e., quantity or not).") lon = np.deg2rad(lon) lat = np.deg2rad(lat) cs = np.cos(lat) x = cs * np.cos(lon) y = cs * np.sin(lon) z = np.sin(lat) return x, y, z def inverse(self): return CartesianToSpherical(wrap_lon_at=self._wrap_lon_at) @property def input_units(self): return {'lon': u.deg, 'lat': u.deg} class CartesianToSpherical(Model): """ Convert cartesian coordinates to spherical coordinates on a unit sphere. Output spherical coordinates are in degrees. When input cartesian coordinates are quantities (``Quantity`` objects), output angles will also be quantities in degrees. Angle ``lon`` is the *longitude* (or *azimuthal angle*) in range ``[0, 360)`` (or ``[-180, 180)``) and angle ``lat`` is the *latitude* (or *elevation angle*) in the range ``[-90, 90]``. """ _separable = False _input_units_allow_dimensionless = True n_inputs = 3 n_outputs = 2 def __init__(self, wrap_lon_at=360, **kwargs): """ Parameters ---------- wrap_lon_at : {360, 180}, optional An **integer number** that specifies the range of the longitude (azimuthal) angle. When ``wrap_lon_at`` is 180, the longitude angle will have a range of ``[-180, 180)`` and when ``wrap_lon_at`` is 360 (default), the longitude angle will have a range of ``[0, 360)``. """ super().__init__(**kwargs) self.inputs = ('x', 'y', 'z') self.outputs = ('lon', 'lat') self.wrap_lon_at = wrap_lon_at @property def wrap_lon_at(self): """ An **integer number** that specifies the range of the longitude (azimuthal) angle. Allowed values are 180 and 360. When ``wrap_lon_at`` is 180, the longitude angle will have a range of ``[-180, 180)`` and when ``wrap_lon_at`` is 360 (default), the longitude angle will have a range of ``[0, 360)``. """ return self._wrap_lon_at @wrap_lon_at.setter def wrap_lon_at(self, wrap_angle): if not (isinstance(wrap_angle, numbers.Integral) and wrap_angle in [180, 360]): raise ValueError("'wrap_lon_at' must be an integer number: 180 or 360") self._wrap_lon_at = wrap_angle def evaluate(self, x, y, z): nquant = [isinstance(i, u.Quantity) for i in (x, y, z)].count(True) if nquant in [1, 2]: raise TypeError("All arguments must be of the same type " "(i.e., quantity or not).") h = np.hypot(x, y) lat = np.rad2deg(np.arctan2(z, h)) lon = np.rad2deg(np.arctan2(y, x)) lon[h == 0] *= 0 if self._wrap_lon_at != 180: lon = np.mod(lon, 360.0 * u.deg if nquant else 360.0, where=np.isfinite(lon), out=lon) return lon, lat def inverse(self): return SphericalToCartesian(wrap_lon_at=self._wrap_lon_at) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/gwcs/region.py0000644000175100001770000003001514573367100014747 0ustar00runnerdocker""" Polygon filling algorithm. """ # Licensed under a 3-clause BSD style license - see LICENSE.rst # NOTE: Algorithm description can be found, e.g., here: # http://www.cs.rit.edu/~icss571/filling/how_to.html # http://www.cs.uic.edu/~jbell/CourseNotes/ComputerGraphics/PolygonFilling.html import abc from collections import OrderedDict import numpy as np __all__ = ['Region', 'Edge', 'Polygon'] 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._shiftx & self._shifty are introduced to shift the bottom-left # corner of the polygon's bounding box to (0,0) as a (hopefully # temporary) workaround to a limitation of the original code that the # polygon must be completely contained in the image. It seems that the # code works fine if we make sure that the bottom-left corner of the # polygon's bounding box has non-negative coordinates. self._shiftx = 0 self._shifty = 0 for vertex in vertices: x, y = vertex if x < self._shiftx: self._shiftx = x if y < self._shifty: self._shifty = y v = [(i - self._shiftx, j - self._shifty) for i, j in vertices] # convert to integer coordinates: self._vertices = np.asarray(list(map(_round_vertex, v))) self._shiftx = int(round(self._shiftx)) self._shifty = int(round(self._shifty)) 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] # a hack for incomplete filling .any() fails if 0 is in ymin_ind # if ymin_ind.any(): yminindlen, = ymin_ind.shape if yminindlen: 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=f'E{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). # see comments in the __init__ function for the reason of introducing # polygon shifts (self._shiftx & self._shifty). Here we need to shift # it back. (ny, nx) = data.shape y = np.min(list(self._GET.keys())) AET = [] scline = self._scan_line_range[-1] while y <= scline: if y < scline: AET = self.update_AET(y, AET) if self._bbox[2] <= 0: y += 1 continue scan_line = Edge('scan_line', start=[self._bbox[0], y], stop=[self._bbox[0] + self._bbox[2], y]) x = [int(np.ceil(e.compute_AET_entry(scan_line)[1])) for e in AET if e is not None] xnew = np.sort(x) ysh = y + self._shifty if ysh < 0 or ysh >= ny: y += 1 continue for i, j in zip(xnew[::2], xnew[1::2]): xstart = max(0, i + self._shiftx) xend = min(j + self._shiftx, nx - 1) data[ysh][xstart:xend + 1] = self._rid 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) if np.allclose(np.cross(u, v), 0, rtol=0, atol=1e2 * np.finfo(float).eps): return np.array(self._start) return np.cross(v, w) / D * u + self._start def is_parallel(self, edge): u = self._stop - self._start v = edge._stop - edge._start return np.allclose(np.cross(u, v), 0, rtol=0, atol=1e2 * np.finfo(float).eps) def _round_vertex(v): x, y = v return (int(round(x)), int(round(y))) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/gwcs/selector.py0000644000175100001770000006022214573367100015307 0ustar00runnerdocker# 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.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.str_: mapper = np.asanyarray(mapper, dtype=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.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.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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/gwcs/spectroscopy.py0000644000175100001770000002567614573367100016242 0ustar00runnerdocker# 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 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1710091850.3511648 gwcs-0.21.0/gwcs/tests/0000755000175100001770000000000014573367112014260 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/gwcs/tests/__init__.py0000644000175100001770000000017114573367100016365 0ustar00runnerdocker# Licensed under a 3-clause BSD style license - see LICENSE.rst """ This packages contains affiliated package tests. """ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/gwcs/tests/conftest.py0000644000175100001770000004677014573367100016472 0ustar00runnerdocker""" 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.time import Time from astropy import coordinates as coord from astropy.modeling import models from .. import coordinate_frames as cf from .. import spectroscopy as sp from .. import wcs from .. import geometry # 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_quantity_shift(): frame = cf.CoordinateFrame(name="quantity", axes_order=(0, 1), naxes=2, axes_type=("SPATIAL", "SPATIAL"), unit=(u.km, u.km)) pipe = [(detector_2d, model_2d_shift), (frame, None)] return wcs.WCS(pipe) @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, [1, 2, 3, 4] * 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(scope="function") 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.01, 0.5, 0.01 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 @pytest.fixture(scope="function") def gwcs_1d_spectral(): """ A simple 1D spectral WCS. """ wave_model = models.Shift(-5) | models.Multiply(3.7) | models.Shift(20) wave_model.bounding_box = (7, 50) wave_frame = cf.SpectralFrame(axes_order=(0, ), unit=u.Hz, axes_names=("Frequency",)) detector_frame = cf.CoordinateFrame( name="detector", naxes=1, axes_order=(0, ), axes_type=("pixel",), unit=(u.pix, ) ) owcs = wcs.WCS(forward_transform=wave_model, output_frame=wave_frame, input_frame=detector_frame) owcs.array_shape = (44, ) owcs.pixel_shape = (44, ) return owcs @pytest.fixture(scope="function") def gwcs_spec_cel_time_4d(): """ A complex 4D mixed celestial + spectral + time WCS. """ # spectroscopic frame: wave_model = models.Shift(-5) | models.Multiply(3.7) | models.Shift(20) wave_model.bounding_box = (7, 50) wave_frame = cf.SpectralFrame(name='wave', unit=u.m, axes_order=(0,), axes_names=('lambda',)) # time frame: time_model = models.Identity(1) # models.Linear1D(10, 0) time_frame = cf.TemporalFrame(Time("2010-01-01T00:00"), name='time', unit=u.s, axes_order=(3,)) # Values from data/acs.hdr: crpix = (12, 13) crval = (5.63, -72.05) cd = [[1.291E-05, 5.9532E-06], [5.02215E-06, -1.2645E-05]] aff = models.AffineTransformation2D(matrix=cd, name='rotation') offx = models.Shift(-crpix[0], name='x_translation') offy = models.Shift(-crpix[1], name='y_translation') wcslin = models.Mapping((1, 0)) | (offx & offy) | aff tan = models.Pix2Sky_TAN(name='tangent_projection') n2c = models.RotateNative2Celestial(*crval, 180, name='sky_rotation') cel_model = wcslin | tan | n2c icrs = cf.CelestialFrame(reference_frame=coord.ICRS(), name='sky', axes_order=(2, 1)) wcs_forward = wave_model & cel_model & time_model comp_frm = cf.CompositeFrame(frames=[wave_frame, icrs, time_frame], name='TEST 4D FRAME') detector_frame = cf.CoordinateFrame( name="detector", naxes=4, axes_order=(0, 1, 2, 3), axes_type=("pixel", "pixel", "pixel", "pixel"), unit=(u.pix, u.pix, u.pix, u.pix) ) w = wcs.WCS(forward_transform=wcs_forward, output_frame=comp_frm, input_frame=detector_frame) w.bounding_box = ((0, 63), (0, 127), (0, 255), (0, 9)) w.array_shape = (10, 256, 128, 64) w.pixel_shape = (64, 128, 256, 10) return w @pytest.fixture( scope="function", params=[ (2, 1, 0), (2, 0, 1), pytest.param((1, 0, 2), marks=pytest.mark.skip(reason="Fails round-trip for -TAB axis 3")), ] ) def gwcs_cube_with_separable_spectral(request): cube_size = (128, 64, 100) axes_order = request.param spectral_axes_order = (axes_order.index(2), ) cel_axes_order = (axes_order.index(0), axes_order.index(1)) # Values from data/acs.hdr: crpix = (64, 32) crval = (5.63056810618, -72.0545718428) cd = [[1.29058667557984E-05, 5.95320245884555E-06], [5.02215195623825E-06, -1.2645010396976E-05]] aff = models.AffineTransformation2D(matrix=cd, name='rotation') offx = models.Shift(-crpix[0], name='x_translation') offy = models.Shift(-crpix[1], name='y_translation') wcslin = (offx & offy) | aff tan = models.Pix2Sky_TAN(name='tangent_projection') n2c = models.RotateNative2Celestial(*crval, 180, name='sky_rotation') icrs = cf.CelestialFrame(reference_frame=coord.ICRS(), name='sky', axes_order=cel_axes_order) spec = cf.SpectralFrame( name='wave', unit=[u.m,], axes_order=spectral_axes_order, axes_names=('lambda',) ) comp_frm = cf.CompositeFrame(frames=[icrs, spec], name='TEST 3D FRAME WITH SPECTRAL AXIS') wcs_forward = ((wcslin & models.Identity(1)) | (tan & models.Identity(1)) | (n2c & models.Identity(1)) | models.Mapping(axes_order)) 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)) w = wcs.WCS(forward_transform=wcs_forward, output_frame=comp_frm, input_frame=detector_frame) w.bounding_box = tuple((0, k - 1) for k in cube_size) w.pixel_shape = cube_size w.array_shape = w.pixel_shape[::-1] return w, axes_order @pytest.fixture( scope="function", params=[ (2, 0, 1), (2, 1, 0), pytest.param((0, 2, 1), marks=pytest.mark.skip(reason="Fails round-trip for -TAB axis 2")), pytest.param((1, 0, 2), marks=pytest.mark.skip(reason="Fails round-trip for -TAB axis 3")), ] ) def gwcs_cube_with_separable_time(request): """ A mixed celestial + time WCS. """ cube_size = (64, 32, 128) axes_order = request.param time_axes_order = (axes_order.index(2), ) cel_axes_order = (axes_order.index(0), axes_order.index(1)) 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) ) # time frame: time_model = models.Identity(1) # models.Linear1D(10, 0) time_frame = cf.TemporalFrame(Time("2010-01-01T00:00"), name='time', unit=u.s, axes_order=time_axes_order) # Values from data/acs.hdr: crpix = (12, 13) crval = (5.63, -72.05) cd = [[1.291E-05, 5.9532E-06], [5.02215E-06, -1.2645E-05]] aff = models.AffineTransformation2D(matrix=cd, name='rotation') offx = models.Shift(-crpix[0], name='x_translation') offy = models.Shift(-crpix[1], name='y_translation') wcslin = models.Mapping((1, 0)) | (offx & offy) | aff tan = models.Pix2Sky_TAN(name='tangent_projection') n2c = models.RotateNative2Celestial(*crval, 180, name='sky_rotation') cel_model = wcslin | tan | n2c icrs = cf.CelestialFrame(reference_frame=coord.ICRS(), name='sky', axes_order=cel_axes_order) wcs_forward = (cel_model & time_model) | models.Mapping(axes_order) comp_frm = cf.CompositeFrame(frames=[icrs, time_frame], name='TEST 3D FRAME WITH TIME') w = wcs.WCS(forward_transform=wcs_forward, output_frame=comp_frm, input_frame=detector_frame) w.bounding_box = tuple((0, k - 1) for k in cube_size) w.pixel_shape = cube_size w.array_shape = w.pixel_shape[::-1] return w @pytest.fixture(scope="function") def gwcs_7d_complex_mapping(): """ Useful features of this WCS (axes indices here are 0-based): - includes two celestial axes: input (0, 1) maps to world (2 - RA, 1 - Dec) - includes one separable frame with one axis: 4 -> 2 - includes one frame with 3 input and 4 output axes (1 degenerate), with separable world axes (3, 5) and (0, 6). """ offx = models.Shift(-64, name='x_translation') offy = models.Shift(-32, name='y_translation') cd = np.array([[1.2906, 0.59532], [0.50222, -1.2645]]) aff = models.AffineTransformation2D(matrix=1e-5 * cd, name='rotation') aff2 = models.AffineTransformation2D(matrix=cd, name='rotation2') wcslin = (offx & offy) | aff tan = models.Pix2Sky_TAN(name='tangent_projection') n2c = models.RotateNative2Celestial(5.630568, -72.0546, 180, name='skyrot') icrs = cf.CelestialFrame(reference_frame=coord.ICRS(), name='sky', axes_order=(2, 1)) spec = cf.SpectralFrame(name='wave', unit=[u.m], axes_order=(4,), axes_names=('lambda',)) cmplx = cf.CoordinateFrame( name="complex", naxes=4, axes_order=(3, 5, 0, 6), axis_physical_types=(['em.wl', 'em.wl', 'time', 'time']), axes_type=("SPATIAL", "SPATIAL", "TIME", "TIME"), axes_names=("x", "y", "t", 'tau'), unit=(u.m, u.m, u.second, u.second) ) comp_frm = cf.CompositeFrame(frames=[icrs, spec, cmplx], name='TEST 7D') wcs_forward = ((wcslin & models.Shift(-3.14) & models.Scale(2.7) & aff2) | (tan & models.Identity(1) & models.Identity(1) & models.Identity(2)) | (n2c & models.Identity(1) & models.Identity(1) & models.Identity(2)) | models.Mapping((3, 1, 0, 4, 2, 5, 3))) detector_frame = cf.CoordinateFrame( name="detector", naxes=6, axes_order=(0, 1, 2, 3, 4, 5), axes_type=("pixel", "pixel", "pixel", "pixel", "pixel", "pixel"), unit=(u.pix, u.pix, u.pix, u.pix, u.pix, u.pix) ) pipeline = [('detector', wcs_forward), (comp_frm, None)] w = wcs.WCS(forward_transform=wcs_forward, output_frame=comp_frm, input_frame=detector_frame) w.bounding_box = ((0, 15), (0, 31), (0, 20), (0, 10), (0, 10), (0, 1)) w.array_shape = (2, 11, 11, 21, 32, 16) w.pixel_shape = (16, 32, 21, 11, 11, 2) return w @pytest.fixture def spher_to_cart(): return geometry.SphericalToCartesian() @pytest.fixture def cart_to_spher(): return geometry.CartesianToSpherical() ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1710091850.355165 gwcs-0.21.0/gwcs/tests/data/0000755000175100001770000000000014573367112015171 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/gwcs/tests/data/__init__.py0000644000175100001770000000000014573367100017265 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/gwcs/tests/data/acs.hdr0000644000175100001770000007760214573367100016447 0ustar00runnerdockerSIMPLE = 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/gwcs/tests/data/acs_wfc.hdr0000644000175100001770000010260514573367100017276 0ustar00runnerdockerSIMPLE = 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/gwcs/tests/data/miri_lrs_wcs.asdf0000644000175100001770000020267014573367100020530 0ustar00runnerdocker#ASDF 1.0.0 #ASDF_STANDARD 1.5.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.6.0} history: extensions: - !core/extension_metadata-1.0.0 extension_class: asdf.extension.BuiltinExtension software: !core/software-1.0.0 {name: asdf, version: 2.6.0} - !core/extension_metadata-1.0.0 extension_class: gwcs.extension.GWCSExtension software: !core/software-1.0.0 {name: gwcs, version: 0.13.1.dev16+g71f9b60} wcs: ! name: '' steps: - ! frame: ! axes_names: [x, y] axes_order: [0, 1] axis_physical_types: ['custom:x', 'custom:y'] name: detector unit: [!unit/unit-1.0.0 'pixel', !unit/unit-1.0.0 'pixel'] transform: !transform/compose-1.2.0 bounding_box: - [6.5, 396.5] - [302.5, 346.5] bounds: amplitude_7: [null, null] angle_22: [null, null] angle_3: [null, null] c0_0_13: [null, null] c0_0_14: [null, null] c0_0_16: [null, null] c0_0_17: [null, null] c0_10: [null, null] c0_11: [null, null] c0_1_13: [null, null] c0_1_14: [null, null] c0_1_16: [null, null] c0_1_17: [null, null] c0_2_13: [null, null] c0_2_14: [null, null] c0_3_13: [null, null] c0_3_14: [null, null] c0_4_13: [null, null] c0_4_14: [null, null] c1_0_13: [null, null] c1_0_14: [null, null] c1_0_16: [null, null] c1_0_17: [null, null] c1_10: [null, null] c1_11: [null, null] c1_1_13: [null, null] c1_1_14: [null, null] c1_2_13: [null, null] c1_2_14: [null, null] c1_3_13: [null, null] c1_3_14: [null, null] c2_0_13: [null, null] c2_0_14: [null, null] c2_1_13: [null, null] c2_1_14: [null, null] c2_2_13: [null, null] c2_2_14: [null, null] c3_0_13: [null, null] c3_0_14: [null, null] c3_1_13: [null, null] c3_1_14: [null, null] c4_0_13: [null, null] c4_0_14: [null, null] offset_1: [null, null] offset_2: [null, null] offset_20: [null, null] offset_21: [null, null] offset_4: [null, null] offset_5: [null, null] offset_8: [null, null] fixed: {amplitude_7: false, angle_22: false, angle_3: false, c0_0_13: false, c0_0_14: false, c0_0_16: false, c0_0_17: false, c0_10: false, c0_11: false, c0_1_13: false, c0_1_14: false, c0_1_16: false, c0_1_17: false, c0_2_13: false, c0_2_14: false, c0_3_13: false, c0_3_14: false, c0_4_13: false, c0_4_14: false, c1_0_13: false, c1_0_14: false, c1_0_16: false, c1_0_17: false, c1_10: false, c1_11: false, c1_1_13: false, c1_1_14: false, c1_2_13: false, c1_2_14: false, c1_3_13: false, c1_3_14: false, c2_0_13: false, c2_0_14: false, c2_1_13: false, c2_1_14: false, c2_2_13: false, c2_2_14: false, c3_0_13: false, c3_0_14: false, c3_1_13: false, c3_1_14: false, c4_0_13: false, c4_0_14: false, offset_1: false, offset_2: false, offset_20: false, offset_21: false, offset_4: false, offset_5: false, offset_8: false} forward: - !transform/remap_axes-1.2.0 bounds: {} fixed: {} mapping: [0, 1, 0, 1] - !transform/concatenate-1.2.0 forward: - !transform/compose-1.2.0 forward: - !transform/compose-1.2.0 forward: - !transform/concatenate-1.2.0 forward: - !transform/shift-1.2.0 bounds: offset: &id001 [null, null] fixed: {offset: false} offset: -325.13 - !transform/shift-1.2.0 bounds: offset: *id001 fixed: {offset: false} offset: -299.7 - !transform/rotate2d-1.2.0 angle: 0.24155757363306068 bounds: angle: &id002 [null, null] fixed: {angle: false} - !transform/compose-1.2.0 forward: - !transform/concatenate-1.2.0 forward: - !transform/shift-1.2.0 bounds: offset: *id001 fixed: {offset: false} offset: 325.13 - !transform/shift-1.2.0 bounds: offset: *id001 fixed: {offset: false} offset: 299.7 - !transform/compose-1.2.0 forward: - !transform/concatenate-1.2.0 forward: - !transform/identity-1.2.0 bounds: {} fixed: {} - !transform/constant-1.2.0 bounds: amplitude: [null, null] fixed: {amplitude: false} inverse: !transform/constant-1.2.0 bounds: amplitude: [null, null] fixed: {amplitude: false} value: 299.7 value: 299.7 - !transform/compose-1.2.0 forward: - !transform/compose-1.2.0 forward: - !transform/compose-1.2.0 forward: - !transform/compose-1.2.0 forward: - !transform/compose-1.2.0 forward: - !transform/compose-1.2.0 forward: - !transform/compose-1.2.0 forward: - !transform/concatenate-1.2.0 forward: - !transform/shift-1.2.0 bounds: offset: *id001 fixed: {offset: false} offset: -4.0 - !transform/identity-1.2.0 bounds: {} fixed: {} - !transform/concatenate-1.2.0 forward: - !transform/polynomial-1.2.0 bounds: c0: [null, null] c1: [null, null] coefficients: !core/ndarray-1.0.0 source: 0 datatype: float64 byteorder: little shape: [2] domain: [-1, 1] fixed: {c0: false, c1: false} inverse: !transform/polynomial-1.2.0 bounds: c0: [null, null] c1: [null, null] coefficients: !core/ndarray-1.0.0 source: 1 datatype: float64 byteorder: little shape: [2] domain: [-1, 1] fixed: {c0: false, c1: false} window: [-1, 1] name: M_column_correction window: [-1, 1] - !transform/polynomial-1.2.0 bounds: c0: [null, null] c1: [null, null] coefficients: !core/ndarray-1.0.0 source: 2 datatype: float64 byteorder: little shape: [2] domain: [-1, 1] fixed: {c0: false, c1: false} inverse: !transform/polynomial-1.2.0 bounds: c0: [null, null] c1: [null, null] coefficients: !core/ndarray-1.0.0 source: 3 datatype: float64 byteorder: little shape: [2] domain: [-1, 1] fixed: {c0: false, c1: false} window: [-1, 1] name: M_row_correction window: [-1, 1] - !transform/remap_axes-1.2.0 bounds: {} fixed: {} inverse: !transform/identity-1.2.0 bounds: {} fixed: {} n_dims: 2 mapping: [0, 1, 0, 1] - !transform/concatenate-1.2.0 forward: - !transform/polynomial-1.2.0 bounds: c0_0: [null, null] c0_1: [null, null] c0_2: [null, null] c0_3: [null, null] c0_4: [null, null] c1_0: [null, null] c1_1: [null, null] c1_2: [null, null] c1_3: [null, null] c2_0: [null, null] c2_1: [null, null] c2_2: [null, null] c3_0: [null, null] c3_1: [null, null] c4_0: [null, null] coefficients: !core/ndarray-1.0.0 source: 4 datatype: float64 byteorder: little shape: [5, 5] domain: - [-1, 1] - [-1, 1] fixed: {c0_0: false, c0_1: false, c0_2: false, c0_3: false, c0_4: false, c1_0: false, c1_1: false, c1_2: false, c1_3: false, c2_0: false, c2_1: false, c2_2: false, c3_0: false, c3_1: false, c4_0: false} inverse: !transform/polynomial-1.2.0 bounds: c0_0: [null, null] c0_1: [null, null] c0_2: [null, null] c0_3: [null, null] c0_4: [null, null] c1_0: [null, null] c1_1: [null, null] c1_2: [null, null] c1_3: [null, null] c2_0: [null, null] c2_1: [null, null] c2_2: [null, null] c3_0: [null, null] c3_1: [null, null] c4_0: [null, null] coefficients: !core/ndarray-1.0.0 source: 5 datatype: float64 byteorder: little shape: [5, 5] domain: - [-1, 1] - [-1, 1] fixed: {c0_0: false, c0_1: false, c0_2: false, c0_3: false, c0_4: false, c1_0: false, c1_1: false, c1_2: false, c1_3: false, c2_0: false, c2_1: false, c2_2: false, c3_0: false, c3_1: false, c4_0: false} window: - [-1, 1] - [-1, 1] name: B_correction window: - [-1, 1] - [-1, 1] - !transform/polynomial-1.2.0 bounds: c0_0: [null, null] c0_1: [null, null] c0_2: [null, null] c0_3: [null, null] c0_4: [null, null] c1_0: [null, null] c1_1: [null, null] c1_2: [null, null] c1_3: [null, null] c2_0: [null, null] c2_1: [null, null] c2_2: [null, null] c3_0: [null, null] c3_1: [null, null] c4_0: [null, null] coefficients: !core/ndarray-1.0.0 source: 6 datatype: float64 byteorder: little shape: [5, 5] domain: - [-1, 1] - [-1, 1] fixed: {c0_0: false, c0_1: false, c0_2: false, c0_3: false, c0_4: false, c1_0: false, c1_1: false, c1_2: false, c1_3: false, c2_0: false, c2_1: false, c2_2: false, c3_0: false, c3_1: false, c4_0: false} inverse: !transform/polynomial-1.2.0 bounds: c0_0: [null, null] c0_1: [null, null] c0_2: [null, null] c0_3: [null, null] c0_4: [null, null] c1_0: [null, null] c1_1: [null, null] c1_2: [null, null] c1_3: [null, null] c2_0: [null, null] c2_1: [null, null] c2_2: [null, null] c3_0: [null, null] c3_1: [null, null] c4_0: [null, null] coefficients: !core/ndarray-1.0.0 source: 7 datatype: float64 byteorder: little shape: [5, 5] domain: - [-1, 1] - [-1, 1] fixed: {c0_0: false, c0_1: false, c0_2: false, c0_3: false, c0_4: false, c1_0: false, c1_1: false, c1_2: false, c1_3: false, c2_0: false, c2_1: false, c2_2: false, c3_0: false, c3_1: false, c4_0: false} window: - [-1, 1] - [-1, 1] name: A_correction window: - [-1, 1] - [-1, 1] - !transform/remap_axes-1.2.0 bounds: {} fixed: {} inverse: !transform/remap_axes-1.2.0 bounds: {} fixed: {} mapping: [0, 1, 0, 1] mapping: [0, 1, 0, 1] - !transform/concatenate-1.2.0 forward: - !transform/polynomial-1.2.0 bounds: c0_0: [null, null] c0_1: [null, null] c1_0: [null, null] coefficients: !core/ndarray-1.0.0 source: 8 datatype: float64 byteorder: little shape: [2, 2] domain: - [-1, 1] - [-1, 1] fixed: {c0_0: false, c0_1: false, c1_0: false} name: TI_row_correction window: - [-1, 1] - [-1, 1] - !transform/polynomial-1.2.0 bounds: c0_0: [null, null] c0_1: [null, null] c1_0: [null, null] coefficients: !core/ndarray-1.0.0 source: 9 datatype: float64 byteorder: little shape: [2, 2] domain: - [-1, 1] - [-1, 1] fixed: {c0_0: false, c0_1: false, c1_0: false} name: TI_column_correction window: - [-1, 1] - [-1, 1] - !transform/identity-1.2.0 bounds: {} fixed: {} inverse: !transform/remap_axes-1.2.0 bounds: {} fixed: {} mapping: [0, 1, 0, 1] n_dims: 2 - !transform/remap_axes-1.2.0 bounds: {} fixed: {} mapping: [1, 0] - !transform/compose-1.2.0 forward: - !transform/compose-1.2.0 forward: - !transform/compose-1.2.0 forward: - !transform/concatenate-1.2.0 forward: - !transform/shift-1.2.0 bounds: offset: *id001 fixed: {offset: false} offset: -325.13 - !transform/shift-1.2.0 bounds: offset: *id001 fixed: {offset: false} offset: -299.7 - !transform/rotate2d-1.2.0 angle: 0.24155757363306068 bounds: angle: *id002 fixed: {angle: false} - !transform/remap_axes-1.2.0 bounds: {} fixed: {} mapping: [1] - !transform/tabular-1.2.0 bounding_box: [-291.60259000740217, 95.69787960086536] bounds: {} bounds_error: false fill_value: .nan fixed: {} inverse: !transform/tabular-1.2.0 bounding_box: [3.596040329703134, 14.387876373781149] bounds: {} bounds_error: false fill_value: .nan fixed: {} lookup_table: !core/ndarray-1.0.0 source: 12 datatype: float64 byteorder: little shape: [699] name: waverefinv points: - !core/ndarray-1.0.0 source: 13 datatype: float64 byteorder: little shape: [699] lookup_table: !core/ndarray-1.0.0 source: 10 datatype: float64 byteorder: little shape: [388] name: waveref points: - !core/ndarray-1.0.0 source: 11 datatype: float64 byteorder: little shape: [388] inverse: !transform/compose-1.2.0 bounds: angle_14: [null, null] angle_17: [null, null] c0_0_2: [null, null] c0_0_3: [null, null] c0_0_5: [null, null] c0_0_6: [null, null] c0_1_2: [null, null] c0_1_3: [null, null] c0_1_5: [null, null] c0_1_6: [null, null] c0_2_5: [null, null] c0_2_6: [null, null] c0_3_5: [null, null] c0_3_6: [null, null] c0_4_5: [null, null] c0_4_6: [null, null] c0_8: [null, null] c0_9: [null, null] c1_0_2: [null, null] c1_0_3: [null, null] c1_0_5: [null, null] c1_0_6: [null, null] c1_1_5: [null, null] c1_1_6: [null, null] c1_2_5: [null, null] c1_2_6: [null, null] c1_3_5: [null, null] c1_3_6: [null, null] c1_8: [null, null] c1_9: [null, null] c2_0_5: [null, null] c2_0_6: [null, null] c2_1_5: [null, null] c2_1_6: [null, null] c2_2_5: [null, null] c2_2_6: [null, null] c3_0_5: [null, null] c3_0_6: [null, null] c3_1_5: [null, null] c3_1_6: [null, null] c4_0_5: [null, null] c4_0_6: [null, null] offset_10: [null, null] offset_12: [null, null] offset_13: [null, null] offset_18: [null, null] offset_19: [null, null] fixed: {angle_14: false, angle_17: false, c0_0_2: false, c0_0_3: false, c0_0_5: false, c0_0_6: false, c0_1_2: false, c0_1_3: false, c0_1_5: false, c0_1_6: false, c0_2_5: false, c0_2_6: false, c0_3_5: false, c0_3_6: false, c0_4_5: false, c0_4_6: false, c0_8: false, c0_9: false, c1_0_2: false, c1_0_3: false, c1_0_5: false, c1_0_6: false, c1_1_5: false, c1_1_6: false, c1_2_5: false, c1_2_6: false, c1_3_5: false, c1_3_6: false, c1_8: false, c1_9: false, c2_0_5: false, c2_0_6: false, c2_1_5: false, c2_1_6: false, c2_2_5: false, c2_2_6: false, c3_0_5: false, c3_0_6: false, c3_1_5: false, c3_1_6: false, c4_0_5: false, c4_0_6: false, offset_10: false, offset_12: false, offset_13: false, offset_18: false, offset_19: false} forward: - !transform/concatenate-1.2.0 forward: - !transform/compose-1.2.0 forward: - !transform/compose-1.2.0 forward: - !transform/compose-1.2.0 forward: - !transform/remap_axes-1.2.0 bounds: {} fixed: {} mapping: [1, 0] - !transform/compose-1.2.0 forward: - !transform/remap_axes-1.2.0 bounds: {} fixed: {} mapping: [0, 1, 0, 1] - !transform/compose-1.2.0 forward: - !transform/concatenate-1.2.0 forward: - !transform/polynomial-1.2.0 bounds: c0_0: [null, null] c0_1: [null, null] c1_0: [null, null] coefficients: !core/ndarray-1.0.0 source: 14 datatype: float64 byteorder: little shape: [2, 2] domain: - [-1, 1] - [-1, 1] fixed: {c0_0: false, c0_1: false, c1_0: false} name: T_row_correction window: - [-1, 1] - [-1, 1] - !transform/polynomial-1.2.0 bounds: c0_0: [null, null] c0_1: [null, null] c1_0: [null, null] coefficients: !core/ndarray-1.0.0 source: 15 datatype: float64 byteorder: little shape: [2, 2] domain: - [-1, 1] - [-1, 1] fixed: {c0_0: false, c0_1: false, c1_0: false} name: T_column_correction window: - [-1, 1] - [-1, 1] - !transform/compose-1.2.0 forward: - !transform/remap_axes-1.2.0 bounds: {} fixed: {} mapping: [0, 1, 0, 1] - !transform/compose-1.2.0 forward: - !transform/concatenate-1.2.0 forward: - !transform/polynomial-1.2.0 bounds: c0_0: [null, null] c0_1: [null, null] c0_2: [null, null] c0_3: [null, null] c0_4: [null, null] c1_0: [null, null] c1_1: [null, null] c1_2: [null, null] c1_3: [null, null] c2_0: [null, null] c2_1: [null, null] c2_2: [null, null] c3_0: [null, null] c3_1: [null, null] c4_0: [null, null] coefficients: !core/ndarray-1.0.0 source: 16 datatype: float64 byteorder: little shape: [5, 5] domain: - [-1, 1] - [-1, 1] fixed: {c0_0: false, c0_1: false, c0_2: false, c0_3: false, c0_4: false, c1_0: false, c1_1: false, c1_2: false, c1_3: false, c2_0: false, c2_1: false, c2_2: false, c3_0: false, c3_1: false, c4_0: false} window: - [-1, 1] - [-1, 1] - !transform/polynomial-1.2.0 bounds: c0_0: [null, null] c0_1: [null, null] c0_2: [null, null] c0_3: [null, null] c0_4: [null, null] c1_0: [null, null] c1_1: [null, null] c1_2: [null, null] c1_3: [null, null] c2_0: [null, null] c2_1: [null, null] c2_2: [null, null] c3_0: [null, null] c3_1: [null, null] c4_0: [null, null] coefficients: !core/ndarray-1.0.0 source: 17 datatype: float64 byteorder: little shape: [5, 5] domain: - [-1, 1] - [-1, 1] fixed: {c0_0: false, c0_1: false, c0_2: false, c0_3: false, c0_4: false, c1_0: false, c1_1: false, c1_2: false, c1_3: false, c2_0: false, c2_1: false, c2_2: false, c3_0: false, c3_1: false, c4_0: false} window: - [-1, 1] - [-1, 1] - !transform/compose-1.2.0 forward: - !transform/identity-1.2.0 bounds: {} fixed: {} n_dims: 2 - !transform/compose-1.2.0 forward: - !transform/concatenate-1.2.0 forward: - !transform/polynomial-1.2.0 bounds: c0: [null, null] c1: [null, null] coefficients: !core/ndarray-1.0.0 source: 18 datatype: float64 byteorder: little shape: [2] domain: [-1, 1] fixed: {c0: false, c1: false} window: [-1, 1] - !transform/polynomial-1.2.0 bounds: c0: [null, null] c1: [null, null] coefficients: !core/ndarray-1.0.0 source: 19 datatype: float64 byteorder: little shape: [2] domain: [-1, 1] fixed: {c0: false, c1: false} window: [-1, 1] - !transform/concatenate-1.2.0 forward: - !transform/shift-1.2.0 bounds: offset: &id003 [null, null] fixed: {offset: false} offset: 4.0 - !transform/identity-1.2.0 bounds: {} fixed: {} - !transform/compose-1.2.0 forward: - !transform/concatenate-1.2.0 forward: - !transform/shift-1.2.0 bounds: offset: *id003 fixed: {offset: false} offset: -325.13 - !transform/shift-1.2.0 bounds: offset: *id003 fixed: {offset: false} offset: -299.7 - !transform/rotate2d-1.2.0 angle: 0.24155757363306068 bounds: angle: &id004 [null, null] fixed: {angle: false} - !transform/remap_axes-1.2.0 bounds: {} fixed: {} mapping: [0] n_inputs: 2 - !transform/tabular-1.2.0 bounding_box: [3.596040329703134, 14.387876373781149] bounds: {} bounds_error: false fill_value: .nan fixed: {} lookup_table: !core/ndarray-1.0.0 source: 20 datatype: float64 byteorder: little shape: [699] name: waverefinv points: - !core/ndarray-1.0.0 source: 21 datatype: float64 byteorder: little shape: [699] - !transform/compose-1.2.0 forward: - !transform/rotate2d-1.2.0 angle: -0.24155757363306068 bounds: angle: *id004 fixed: {angle: false} - !transform/concatenate-1.2.0 forward: - !transform/shift-1.2.0 bounds: offset: *id003 fixed: {offset: false} offset: 325.13 - !transform/shift-1.2.0 bounds: offset: *id003 fixed: {offset: false} offset: 299.7 - ! frame: ! frames: - ! axes_names: [v2, v3] axes_order: [0, 1] axis_physical_types: ['custom:v2', 'custom:v3'] name: v2v3_spatial unit: [!unit/unit-1.0.0 'arcsec', !unit/unit-1.0.0 'arcsec'] - ! axes_names: [lambda] axes_order: [2] axis_physical_types: [em.wl] name: spec unit: [!unit/unit-1.0.0 'um'] name: v2v3 transform: !transform/concatenate-1.2.0 bounds: angles_2: [null, null] factor_0: [null, null] factor_1: [null, null] fixed: {angles_2: false, factor_0: false, factor_1: false} forward: - !transform/compose-1.2.0 forward: - !transform/concatenate-1.2.0 forward: - !transform/scale-1.2.0 bounds: factor: &id005 [null, null] factor: 0.0002777777777777778 fixed: {factor: false} - !transform/scale-1.2.0 bounds: factor: *id005 factor: 0.0002777777777777778 fixed: {factor: false} - !transform/rotate_sequence_3d-1.0.0 angles: [-0.12597594444444443, 0.10374517305555556, 0.0, -72.0545718, -5.630568] axes_order: zyxyz bounds: angles: [null, null] fixed: {angles: false} name: v23tosky rotation_type: spherical - !transform/identity-1.2.0 bounds: {} fixed: {} - ! frame: ! frames: - ! axes_names: [RA, DEC] axes_order: [0, 1] axis_physical_types: [pos.eq.ra, pos.eq.dec] name: icrs reference_frame: ! frame_attributes: {} unit: [!unit/unit-1.0.0 'deg', !unit/unit-1.0.0 'deg'] - ! axes_names: [lambda] axes_order: [2] axis_physical_types: [em.wl] name: spec unit: [!unit/unit-1.0.0 'um'] name: world ... BLK0۴ke-.u33333)?BLK0Eտ5Nh{@D@BLK0۴ke-.u33333)?BLK0Eտ5Nh{@D@BLK0 Z惂<LCjX?_@ r+9:mM)3><ǹ?"KmL-?@n.>TF>6 \UnҪx->Fa>V9>`f>BLK0*W˲VN{C&IiujBCƞ>@ Ո8x:?Z4~\W?rfƾd;P>6X=#>`hg=eLD-BLK0ȓr)ǸW`ӷi?ϝ_2*0IAn d>([RѨ@H`f0'9YE (p]>o-L'? t>3MX/Egy>\` ^>BLK09UHw o+u F_|0W4?Wʽxн{D=_=mR?w8j{?h  X,,R[ތ>Iİ؃>%Mu,i"qF!Z9 ؐ=BLK0 bdإZ*O^ ףp=vwcCnqN?g$.?BLK0 ҍZoJ鄊zG!{g$.cCnqN?BLK0 U Lb~xR0h9-,@"\B,@RV,@ gބ,@DLh,@`c♐,@CF,@*,@a^x,@Fp,@BIzh,@Xw`,@mX,@`P,@tWH,@ <ځ@,@B7s8,@`a=a0,@a MN(,@AeW8 ,@V,@-j,@L),@ )+@r+@aƺ+@}AY+@&//+@}}+@y%+@n@!+@2jl+@HZ,4+@$+@Bٽ+@<~+@;+@s +@m٭+@*b|+@t+@|}k+@Coc+@:[+@B1R+@َaJ+@KuB+@LIE9+@'ޒ71+@{(+@%a +@'+@$)+@ZԬ+@-{s*@^?ծ*@r{*@,3*@[o*@J*@n p*@ k*@ǘTK*@CRk*@X*@Sky*@%J֗*@b0*@mՆ*@W1}*@V,˜(u*@sl*@WIc*@Z*@m?R*@X7I|I*@H@*@d7*@/*@ǙG&*@kq*@Jd*@4G *@ 1&*@:)@L)@J)@")@t+)@('0)@?1)@0.)@ .')@8oݑ)@fϭ )@w9 )@B8ߍ)@fs„)@^G|{)@Xm|r)@cxSRi)@V%$`)@/V)@EyM)@@~D)@=M>;)@IP1)@M/()@O~{c)@-)@${P? )@e^)@T-i(@Yt(@|F/(@IZ(@^z'N(@Hg((@I/XY(@8m׷(@'Q(@:f Ť(@b5(@Ȏ(@](@$ (@`a (@T~'@8_Q'@B'@;Vҷ'@h'@Y'@[B'@'@?` '@?ah˖'@b}'@R,_'@oh;'@+5}'@Sr'@{a׭h'@5s^'@l2T'@I'@ݠ?'@wo0O5'@Q*'@TX '@V,{!7'@0/  '@,+^'@3&@S#o&@z"&@/d.g&@ן/&@G&@>d&@-M&@NLi&@p޺&@qD &@#FT&@Z˕w&@MRl&@ﮑb&@y5W&@*^L&@AXA&@p36&@צ+&@/? &@:&@} O &@4]%@ڪ %@tij%@ʅS%@k5s%@Q9PN%@<T"%@}G%@_"%@)tu%@`+-%@l߄%@Cy%@zA+n%@b%@"ZW%@oiK%@j@%@ 4%@{[)%@t%@>,%@l%@z$|$@&$@(x i$@$@$@;ta$@$@`i1$@>$@SB$@&sh7=$@91.x$@ԉl$@9}X_$@RS$@yoG$@Q;$@!o/$@}"$@Z5U$@{ $@IUIy#@D#@s#@#@A#@"*~#@j#@VV%#@;Z#@QdՄ#@ k~#@r*oq#@xŽd#@0VW#@ bJ#@A=#@!_d0#@/##@h#@ѡ#@%H"@!q["@n"@ =?"@T{b"@,ɷ"@Y)o""@ n"@"-|"@"Cހ"@)s"@yҎqe"@ZQ2"W"@_*I"@W@ ;"@bY,"@"]"@41}"@r1"@Nj!@ӆ؎o!@[|!@%s!@ ޹!@׫Y;!@0o!@+]2Ǎ!@;(~!@2Op!@3H(a!@X)R!@kC!@3!@=5$!@"2!@.C!@> @ x @m @k @m`˸ @.[P @&TW @_}!聉 @`Xy @MMi @5Y @ Q{iI @o09 @S( @c| @$E @@\iG@MEq@5R@6wkh@F~QF@h8$@&2@X՘@|"ջ@=n@eJu@&ͭQ@8-@W @%p@@@RJw@3Q@hѭ,@E@VA*@ +Pɹ@?!@&U2l@qWD@qԗ:pb@.*-@!S@]S@ދ@=FT@ZH@P@ZU˳ħ@; l@R|/@\ü@h@Ho@mŪ,@/o@w@V(X@|6$0 @/Dؿ@0Kn@E@4@h[@J5@Dr@nna@ 5oc@K`ʰ @BLK0 LSGiVSkk59r.L)r8,ٝrе rjy|q=Nq q5ĐqχqĊqhKRq3hq:q4 ~yqYziqfwYqᖁtIqwSq9q3hX%n)q+9jqfg qdpvlap1:>^p[pd}Wp^TpH?Qp1 XNp*KypdGipVDYpAIp/ރq>9pȡdC;)paeE8p(&4 p( rcoZ`]o琹VonR]PoJro%}դCRoWH=2oX6o0n3*n#!#nU^{n/ rnRn=f 2n!d nSL&mQmZmjmi,=qmQQmw1mp(m2l olNlwl8[ql)Ql1lJ8}Fl~>kFk1kUՄkGDy~qk{cxQkq1kqdkkJejD ^jxOXjQjQKqj;EQjB$>1jt8j2W&2iڹ+i Am%i@țirO]qiYQi] 1i 塠i=lcDho$hzh/hiph;+wPhm0hhӥobg-1g7򩿰gk;MgupgI7Pg80g5Xܟgi{ff=$fnjfukf1pfcCyPf Ws0fɑlffe/IB`ea' YẻSe5-MpeOFPe-Du@0e_:eR3dU`-d`'d*ا d\oKpd[Pd} 0d6d'cYb~c#"c!cioc$0h OcW)/c>TcŬbLnb"/@bU[㺏bⲇobit+Ob5ϧ/bxrbQaza <^aaoaOI{Oa*Bt/ana84h`a`LGH|[` U`UNo`܌gHO`dN B/`I;`䢥j_]%]_4Q^_&,|D_7^,2 +^X;R^^I8^"X]f>)]tp]]RD]\JGŝ\͎]\P֫\M[Ve[٬][}\x[;lZHc_Z!R]Z0iFZw>Y9YLo,YA[]Yiu0Y xwXr{X֔\XIP yObӛ_6O-6*FNR߹,6NoHM5MXfL5LK5K zJJ92a5JVGIrP.5I"Hu(n4HA.G 44G;FA4Fk90f=E5e<(RSe;K^qe:Sjde9v1e8~d72d6kd5@%fd4޳C3d3kad2Qc1؝c0Rw.yi,p*,(V+h8&#C$R=\l"yt OGB-K)w|GI#) ? 9 ؿ0j?~T?r9a@I @욬G@bj4@ 9ރ@9D@B!@6ө C#@_úmC%@1P1C'@ ݉QD)@iqD+@X}E-@@AE/@00@·d'1@{FZ2@q[o(3@!c 4@GV5@J%6@u>X7@;28@&t9@ZV:@ǎ 8$;@/UW<@=@ݼ>@rܿ?@n7PS@@*@@2DSA@գ]A@CwSB@xɅB@SC@mgC@QSD@O4ID@TE@+)E@ݞBTF@$ \F@Y~uTG@G@j`TH@MH@/1zBTI@dtI@m$UJ@g'J@aAUK@:[wZK@pUsUL@gOYL@JIʦUM@.C;M@F=UN@{6N@0 VO@*%O@ O+P@@p,kP@B2 )9P@#EP@yR+Q@R_kQ@ lQ@H|xQ@{+R@}v4kR@잫R@pR@N]+S@jkS@ѫS@wdS@h?+T@RZ^kT@K۰T@=XiT@#/!,U@ R*lU@Xϒ7U@LKDU@Q,V@)E]lV@tjV@^?-wV@,W@9lW@.VW@ɐ3W@BLK0؄tbc\ &j(W@eW@dW@ƾW@,`W@1W@x5W@3TW@ӑf/W@˗W@B+̚W@W@4y`W@!W@eH̨W@$^W@W)ٝW@-s:W@Q$W@UW@y:†W@5{`W@V4nzW@QM(RtW@ mW@&vgW@6q1`W@p>g ZW@qUESW@6?JLW@/|.EW@=Y=W@W6W@#$ /W@*Zf'W@6ZW@zW@R*9W@wZW@TV@stWoV@&lV@V@tV@v}SV@˯fV@{FV@WNV@F퉬V@<7V@{=}R@jR@UMP{R@5*DiR@WR@$)ER@y~3R@`!R@s^R@Q@ztBQ@#Q@)gkQ@hQ@/V@Q@DQ@-{Q@[8hQ@JUQ@YHxBQ@ʹ/Q@hӠQ@AuM Q@n(P@[}P@P@eXP@P@tHޓP@aXP@>rUlP@iXP@ZCP@t/P@ /nP@YP@`99O@Z5'O@ O@WZÀiO@y?O@Ԉb)O@pt=N@2m%N@ܖN@.=!rmN@窈BN@+N@ CM@ rM@lM@X7kM@mw?M@ M@|L@r3PܛL@rrGL@{'bL@ 6L@XqG L@@\$GK@KmK@܍ǁK@vBHTK@#d&K@M6 J@ιJ@%]J@x9anJ@?J@zMJ@LyWI@UI@fkI@0]FUI@?i%I@>WH@K]H@.͖H@GRwefH@hũ6H@AG\_H@NWVeG@'ZG@mtG@|}CG@ }G@lF@㽎EF@,F~F@~:MF@v}F@"ʭE@cbaE@@tdE@_qSE@|I E@ǬD@J2D@[b&FD@!R.4UD@n1!D@>JC@FC@yikC@qpSC@坥gC@lO}B@ 5B@$o*łB@.NB@%pB@0ϢA@f5"Y|A@)FzA@r`:DA@XcA@o@@Sߣ@@ m@@ v7@@B k@@?@5uSң(?@I>@9)M>@)=@fp=@32a@=@vX<@"<@;@Чy B;@l)T:@d܁a:@+NZ9@,?p~9@|> 9@*8@-,o(8@T+7@pzB7@Ii6@Eq.[6@bP5@Ar5@%V4@brF4@<4@=PD3@]l'3@2@92@1@r GK1@0@ܢh[0@ /@>Z~.@: e-@ ,@(%D+@)@+@6̛*@}V)@z)/q((@~pq1'@2 /:&@e_C%@%KJ$@1e}Q#@jX"@$W]!@'- c @*=@!1@= @s@|1P@V"@'7@N5@w ` @ @gy@%m@ʠ%n?=h?? \L?pu$ӿRwOy3W~К-Ai/WiD! +pb)DuXp[CFu _Z ?#[˱<j6v+ 8,9!Q79H"@TW#pGg$w Lw%i)&i ᝙'Z^t(L)w*[+&u),M<.٫j$/[ppQ00jp41t1:L24e2?FRf3Tw3EV4o5FRt5/+6y!O64AcI7vUU7jnxh8kX{88͈9p:b:vϥ;;C9;`"_<SL#ܩ>=o(xW=?j$?l2@xq|@3N<@ТAʀf|\A;ZAscbA=Br*Bؗ)tBĞ# Ch kCCZ,D[PD`D,~ZTF؞lkF'=F=GZUGGGϡGjL'@HyhU؎H>Hhf,IK{I"QI>nJ jJփtxJe KCZKzK)K2*KK~SR T[V7TkcT#tTX7ٺT`p-UT$>UY>Uu.jU쨄זU5yUG=UfUV GHV҆tVxV8/V/BVk)'WT$TWӁWW(1W)^ Xt6X>dXJZX=K )X&X\AYoLHYn)vYȷYmn0Y0E7ZB$c.Zxl\ZO՟ZpZ<\Z7BZqq]e[ &`E[Ҿs[enQA[?g[适;Z\5`/\aɣ{^\ ʛ\06\ۯHE\ ]6S81K]z]8ſn]wh7,]hx4 ^&9^ xUi^oø^b7^rafqM^cp*_@Z_0Ju_K{_-_3;`s'`ț;D?`+W`Vp` `ce ` `S`H`.n޽`3~&aOq&Saf-6axDcOaXƪha3oa)a.zea3~1a/Wa5aoQ9bu~ 1b-^JbzrcbIG2$}bK/ubW?bVb~{3b bv}cA30cQIc cc}c+#xcDTec^cqs/ac\pckdU/V3d!2$Md8Xrhd&wad0ղdUdT!r#xd\dqje& eԉ;e r*VeB>u^pee%Pe<e+[XgR.tgVg##4gJl'g-<נgfKgWAh\M/e5hapQhX`mhzpp#Mp*b]p lp*'\pm|p7pɄpR !p氤p^Zp t(pIpxS6ZTp Ό q$q>1v(q=58qG[GqaMWq75gq R^wq(}V3qum q A@q3̶qͧ)q$]lqيq j}qOtr$nr<6dm&rƈp6rBLK0j:)O=!.K`ʰ @MZ @Ӿ @5# @[YC @Lc @{' @'7W @kzF @ U @8ceU@7ht @{K@@ƿS`@3@G%@TQ@σ@@WxO@=@@a]@#pN}@gI.@ν=@1ML@3-\@<. 6 @E=@]Ez-@"u;MO=@ČT$M@f\\@idl@#l|@Lsx@{M@R"@22 @Iƒ@va@y:v@K@\ @h@"*@Bȟ:@tJ@QIZ@(6 j@My@leȉ@}9@r@RH@g@!@8@ %@|"P-q@: 5F@Q<(@bi~D7@8LG@SW@H[og@fcDw@ k@.r@zæ@r&O@> m@UÑB@Xm}@7@@>@ek%@@5@$E@T@h*N׿d@ Bߔt@Yi@Nq|>@6@@4@d @xh@=@$@^.M,@F4"@];2@Du{CfB@5K;R@Rb@*Zq@cb@nj@qd@y9@T2L@I@a@:yz@ܐ4b@~7@ @b/@d?@ƋO@`_@J6K5o@M @eߎ@0}y@Ҕ3@t^@ħ4@a @Z@ @"# @@:J+]@Q32-@i:=@&xBL@Ș2J\@jQl@ ȦY[|@`a0@Pi@pګ@&x@6>I@UY@zm.@w@1 @`릭@̥*@_W:@F,J@Z@*i@,BHիy@Y݀@pqU@v*@0@V@Ϥ@^ @<T@)@."@"FG*'@]27@fu9}G@uARW@/I'g@LPv@ӣXц@]`@2h{@oP@v2w%@JF@a@\y@ty@.N@B#%@ע4@\D@(T@wd@l6Lt@NE!@e@R}ˣ@s렳@-u@8J@ۡ @|[ @ @"@b:!t"@RD)I2@i0B@H8Q@r@a@,Hq@.Or@ߠWG@rZ_@g@&n@X>v@UC~p@mE@>@q@+@$夙/@㟬n?@hYCO@ _@*n@NB~@YBӗ@ql@4A@֠p@x*@@@^X k@@ @.@DF ,@]A(<@u/L@*7i\@̤o?>l@n)G|@N@V@TX^@fg@2m<@:Ju@a@}@~y@ @¨ne@d(:*@:@I@JWY@i@6cy@0N8@e? @t}@᷸@m@Z'a@6@ @@ V@"@:'@&R`7@i>'5G@j. W@ 6f@l>v@P&F@M^@U3@6U]@&e@z>l@Vt@m=|\@`1@@k$@F%4@ߢD@ZT@,T/d@*t@pBك@Zɮ@q<у@VX@-@j@<$@@@ @+ @b @3c @@ @պ( @0 @wך8 @H޴@ @ꑢH @n&jP @LTX @ ).?` @])h @.%5p @0w @<= @HzӇ @rTWE @C`4ɨ @lM @w} @Th @R @X\= @)b' @?d @˾ @k @m @>s @ @m{ @K{!@(f!@SQ!@$);!@4&&!@@.!@Ly5!@hXV=!@9d3E!@ pM!@{%U!@ʩ]!@}-ze!@Ndm!@a5Ou!@>9}!@=$!@!@cD!@4!@LΤ!@lи!@ JT!@x'؍!@I!\x!@-b!@8cM!@D7!@Pxk"!@^\U !@/h2s!@t!@z"@ "@s"@D"@`v#"@=a+"@K3"@6;"@Yԙ C"@* K"@R"@l%Z"@ Ib"@n&-j"@?%r"@14z"@$@Z)$@\$@9b$@$@`i$@1$@q%@ %@hy}%@uEg%@F)"R#%@5=+%@@܈'3%@L ;%@XB%@[dsJ%@,pPR%@{-Z%@· b%@#j%@pħ{r%@A+fz%@~P%@[3;%@8%%@;%@V%@'B%@ϱ%@ J%@gΤ%@k!DR%@<-!y%@ 9Yd%@DN%@Pa9%@\#%@Qhri%@"tO&@,q&@ċ &@x&@f &@7(&@}x0&@Zb8&@7 M@&@{7H&@L"P&@Η X&@_&@ g&@f#o&@a%Cw&@21 +&@=&@H2v&@T`&@v`:K&@Glq5&@xNB &@+ &@J&@&@\Q&@-մ&@|Y&@Y݉&@6at&@q^&@BhI&@3'@p'@'@ex'@W)B%'@(5-'@@5'@Lه='@X E'@ldrM'@=pp]U'@|MG]'@߇*2e'@m'@"u'@R|'@#*܄'@{ƌ'@X2'@5'@g:'@8p'@ A['@ E'@I0'@|!d'@M-AQ'@9'@DX'@P'@\`'@bh'@3toh(@Ln (@Ջ)pY(@C(@ww.#(@H+(@3(@z:(@WB(@4 J(@]R(@.Z(@̖b(@ mj(@Wr(@r%c"Bz(@C1@,(@=*(@H(@T1(@`֡(@Xl9(@)xn(@KA(@ˏ(ŀ(@Ik(@mU(@>P@(@*(@yX(@V(@3`(@S)@$g)@)@o)@~ )@h)bwi()@95?S0)@ A>8)@L)@)@XֆH)@}d O)@NpW)@|m_)@Jg)@'o)@w)@c!})@4g)@Û)R)@x<)@U1')@x2)@I9)@)@ @Ѿ)@Ļ)@!H)@^-a̐)@/9>P{)@Ee)@PWP)@\:)@sh_%)@Dt*@lg *@I*@&o*@%*@Yv-*@*5*@ƚ~y=*@wdE*@TNM*@n1 9U*@?#]*@e*@ ɕl*@t*@%|*@T1`!*@%==*@H)*@Tw*@`0b*@ilL*@:x87*@ k!*@܏H@ *@%*@~H*@O*@ O*@ʙӠ*@vW*@Su*@d0_`+@5 J +@f5+@+@n #+@y)*+@J5_v2+@A<:+@L~B+@XJ+@dӅR+@_p tZ+@0|^b+@jIj+@ғG3r+@$z+@t+@E +@ûݑ+@Θ(ș+@u+@R0+@Z/+@+ 8r+@ \+@?G+@!1+@o-G+@@9^+@E;O+@P+@\V+@hڰ,@Ut^,@&,@ifp,@ȗFZ ,@#nE(,@j/0,@;u8,@ Ǻ@,@җ}G,@tO,@QW,@P. _,@! g,@ o,@Ɣnw,@%Y,@e1C,@6=] .,@I:,@T(,@`,@zl/خ,@Kx¶,@7,@h,@BLK0 UϜ4q&+C- Fy@eCnqN?h$.?BLK0 Maxd!&޺]o>/.yh$.cCnqN?BLK09UHw o+u F_|0W4?Wʽxн{D=_=mR?w8j{?h  X,,R[ތ>Iİ؃>%Mu,i"qF!Z9 ؐ=BLK0*W˲VN{C&IiujBCƞ>@ Ո8x:?Z4~\W?rfƾd;P>6X=#>`hg=eLD-BLK0Eտ5Nh{@D@BLK0Eտ5Nh{@D@BLK0؄tbc\ &j(W@eW@dW@ƾW@,`W@1W@x5W@3TW@ӑf/W@˗W@B+̚W@W@4y`W@!W@eH̨W@$^W@W)ٝW@-s:W@Q$W@UW@y:†W@5{`W@V4nzW@QM(RtW@ mW@&vgW@6q1`W@p>g ZW@qUESW@6?JLW@/|.EW@=Y=W@W6W@#$ /W@*Zf'W@6ZW@zW@R*9W@wZW@TV@stWoV@&lV@V@tV@v}SV@˯fV@{FV@WNV@F퉬V@<7V@{=}R@jR@UMP{R@5*DiR@WR@$)ER@y~3R@`!R@s^R@Q@ztBQ@#Q@)gkQ@hQ@/V@Q@DQ@-{Q@[8hQ@JUQ@YHxBQ@ʹ/Q@hӠQ@AuM Q@n(P@[}P@P@eXP@P@tHޓP@aXP@>rUlP@iXP@ZCP@t/P@ /nP@YP@`99O@Z5'O@ O@WZÀiO@y?O@Ԉb)O@pt=N@2m%N@ܖN@.=!rmN@窈BN@+N@ CM@ rM@lM@X7kM@mw?M@ M@|L@r3PܛL@rrGL@{'bL@ 6L@XqG L@@\$GK@KmK@܍ǁK@vBHTK@#d&K@M6 J@ιJ@%]J@x9anJ@?J@zMJ@LyWI@UI@fkI@0]FUI@?i%I@>WH@K]H@.͖H@GRwefH@hũ6H@AG\_H@NWVeG@'ZG@mtG@|}CG@ }G@lF@㽎EF@,F~F@~:MF@v}F@"ʭE@cbaE@@tdE@_qSE@|I E@ǬD@J2D@[b&FD@!R.4UD@n1!D@>JC@FC@yikC@qpSC@坥gC@lO}B@ 5B@$o*łB@.NB@%pB@0ϢA@f5"Y|A@)FzA@r`:DA@XcA@o@@Sߣ@@ m@@ v7@@B k@@?@5uSң(?@I>@9)M>@)=@fp=@32a@=@vX<@"<@;@Чy B;@l)T:@d܁a:@+NZ9@,?p~9@|> 9@*8@-,o(8@T+7@pzB7@Ii6@Eq.[6@bP5@Ar5@%V4@brF4@<4@=PD3@]l'3@2@92@1@r GK1@0@ܢh[0@ /@>Z~.@: e-@ ,@(%D+@)@+@6̛*@}V)@z)/q((@~pq1'@2 /:&@e_C%@%KJ$@1e}Q#@jX"@$W]!@'- c @*=@!1@= @s@|1P@V"@'7@N5@w ` @ @gy@%m@ʠ%n?=h?? \L?pu$ӿRwOy3W~К-Ai/WiD! +pb)DuXp[CFu _Z ?#[˱<j6v+ 8,9!Q79H"@TW#pGg$w Lw%i)&i ᝙'Z^t(L)w*[+&u),M<.٫j$/[ppQ00jp41t1:L24e2?FRf3Tw3EV4o5FRt5/+6y!O64AcI7vUU7jnxh8kX{88͈9p:b:vϥ;;C9;`"_<SL#ܩ>=o(xW=?j$?l2@xq|@3N<@ТAʀf|\A;ZAscbA=Br*Bؗ)tBĞ# Ch kCCZ,D[PD`D,~ZTF؞lkF'=F=GZUGGGϡGjL'@HyhU؎H>Hhf,IK{I"QI>nJ jJփtxJe KCZKzK)K2*KK~SR T[V7TkcT#tTX7ٺT`p-UT$>UY>Uu.jU쨄זU5yUG=UfUV GHV҆tVxV8/V/BVk)'WT$TWӁWW(1W)^ Xt6X>dXJZX=K )X&X\AYoLHYn)vYȷYmn0Y0E7ZB$c.Zxl\ZO՟ZpZ<\Z7BZqq]e[ &`E[Ҿs[enQA[?g[适;Z\5`/\aɣ{^\ ʛ\06\ۯHE\ ]6S81K]z]8ſn]wh7,]hx4 ^&9^ xUi^oø^b7^rafqM^cp*_@Z_0Ju_K{_-_3;`s'`ț;D?`+W`Vp` `ce ` `S`H`.n޽`3~&aOq&Saf-6axDcOaXƪha3oa)a.zea3~1a/Wa5aoQ9bu~ 1b-^JbzrcbIG2$}bK/ubW?bVb~{3b bv}cA30cQIc cc}c+#xcDTec^cqs/ac\pckdU/V3d!2$Md8Xrhd&wad0ղdUdT!r#xd\dqje& eԉ;e r*VeB>u^pee%Pe<e+[XgR.tgVg##4gJl'g-<נgfKgWAh\M/e5hapQhX`mhzpp#Mp*b]p lp*'\pm|p7pɄpR !p氤p^Zp t(pIpxS6ZTp Ό q$q>1v(q=58qG[GqaMWq75gq R^wq(}V3qum q A@q3̶qͧ)q$]lqيq j}qOtr$nr<6dm&rƈp6rBLK0j:)O=!.K`ʰ @MZ @Ӿ @5# @[YC @Lc @{' @'7W @kzF @ U @8ceU@7ht @{K@@ƿS`@3@G%@TQ@σ@@WxO@=@@a]@#pN}@gI.@ν=@1ML@3-\@<. 6 @E=@]Ez-@"u;MO=@ČT$M@f\\@idl@#l|@Lsx@{M@R"@22 @Iƒ@va@y:v@K@\ @h@"*@Bȟ:@tJ@QIZ@(6 j@My@leȉ@}9@r@RH@g@!@8@ %@|"P-q@: 5F@Q<(@bi~D7@8LG@SW@H[og@fcDw@ k@.r@zæ@r&O@> m@UÑB@Xm}@7@@>@ek%@@5@$E@T@h*N׿d@ Bߔt@Yi@Nq|>@6@@4@d @xh@=@$@^.M,@F4"@];2@Du{CfB@5K;R@Rb@*Zq@cb@nj@qd@y9@T2L@I@a@:yz@ܐ4b@~7@ @b/@d?@ƋO@`_@J6K5o@M @eߎ@0}y@Ҕ3@t^@ħ4@a @Z@ @"# @@:J+]@Q32-@i:=@&xBL@Ș2J\@jQl@ ȦY[|@`a0@Pi@pګ@&x@6>I@UY@zm.@w@1 @`릭@̥*@_W:@F,J@Z@*i@,BHիy@Y݀@pqU@v*@0@V@Ϥ@^ @<T@)@."@"FG*'@]27@fu9}G@uARW@/I'g@LPv@ӣXц@]`@2h{@oP@v2w%@JF@a@\y@ty@.N@B#%@ע4@\D@(T@wd@l6Lt@NE!@e@R}ˣ@s렳@-u@8J@ۡ @|[ @ @"@b:!t"@RD)I2@i0B@H8Q@r@a@,Hq@.Or@ߠWG@rZ_@g@&n@X>v@UC~p@mE@>@q@+@$夙/@㟬n?@hYCO@ _@*n@NB~@YBӗ@ql@4A@֠p@x*@@@^X k@@ @.@DF ,@]A(<@u/L@*7i\@̤o?>l@n)G|@N@V@TX^@fg@2m<@:Ju@a@}@~y@ @¨ne@d(:*@:@I@JWY@i@6cy@0N8@e? @t}@᷸@m@Z'a@6@ @@ V@"@:'@&R`7@i>'5G@j. W@ 6f@l>v@P&F@M^@U3@6U]@&e@z>l@Vt@m=|\@`1@@k$@F%4@ߢD@ZT@,T/d@*t@pBك@Zɮ@q<у@VX@-@j@<$@@@ @+ @b @3c @@ @պ( @0 @wך8 @H޴@ @ꑢH @n&jP @LTX @ ).?` @])h @.%5p @0w @<= @HzӇ @rTWE @C`4ɨ @lM @w} @Th @R @X\= @)b' @?d @˾ @k @m @>s @ @m{ @K{!@(f!@SQ!@$);!@4&&!@@.!@Ly5!@hXV=!@9d3E!@ pM!@{%U!@ʩ]!@}-ze!@Ndm!@a5Ou!@>9}!@=$!@!@cD!@4!@LΤ!@lи!@ JT!@x'؍!@I!\x!@-b!@8cM!@D7!@Pxk"!@^\U !@/h2s!@t!@z"@ "@s"@D"@`v#"@=a+"@K3"@6;"@Yԙ C"@* K"@R"@l%Z"@ Ib"@n&-j"@?%r"@14z"@$@Z)$@\$@9b$@$@`i$@1$@q%@ %@hy}%@uEg%@F)"R#%@5=+%@@܈'3%@L ;%@XB%@[dsJ%@,pPR%@{-Z%@· b%@#j%@pħ{r%@A+fz%@~P%@[3;%@8%%@;%@V%@'B%@ϱ%@ J%@gΤ%@k!DR%@<-!y%@ 9Yd%@DN%@Pa9%@\#%@Qhri%@"tO&@,q&@ċ &@x&@f &@7(&@}x0&@Zb8&@7 M@&@{7H&@L"P&@Η X&@_&@ g&@f#o&@a%Cw&@21 +&@=&@H2v&@T`&@v`:K&@Glq5&@xNB &@+ &@J&@&@\Q&@-մ&@|Y&@Y݉&@6at&@q^&@BhI&@3'@p'@'@ex'@W)B%'@(5-'@@5'@Lه='@X E'@ldrM'@=pp]U'@|MG]'@߇*2e'@m'@"u'@R|'@#*܄'@{ƌ'@X2'@5'@g:'@8p'@ A['@ E'@I0'@|!d'@M-AQ'@9'@DX'@P'@\`'@bh'@3toh(@Ln (@Ջ)pY(@C(@ww.#(@H+(@3(@z:(@WB(@4 J(@]R(@.Z(@̖b(@ mj(@Wr(@r%c"Bz(@C1@,(@=*(@H(@T1(@`֡(@Xl9(@)xn(@KA(@ˏ(ŀ(@Ik(@mU(@>P@(@*(@yX(@V(@3`(@S)@$g)@)@o)@~ )@h)bwi()@95?S0)@ A>8)@L)@)@XֆH)@}d O)@NpW)@|m_)@Jg)@'o)@w)@c!})@4g)@Û)R)@x<)@U1')@x2)@I9)@)@ @Ѿ)@Ļ)@!H)@^-a̐)@/9>P{)@Ee)@PWP)@\:)@sh_%)@Dt*@lg *@I*@&o*@%*@Yv-*@*5*@ƚ~y=*@wdE*@TNM*@n1 9U*@?#]*@e*@ ɕl*@t*@%|*@T1`!*@%==*@H)*@Tw*@`0b*@ilL*@:x87*@ k!*@܏H@ *@%*@~H*@O*@ O*@ʙӠ*@vW*@Su*@d0_`+@5 J +@f5+@+@n #+@y)*+@J5_v2+@A<:+@L~B+@XJ+@dӅR+@_p tZ+@0|^b+@jIj+@ғG3r+@$z+@t+@E +@ûݑ+@Θ(ș+@u+@R0+@Z/+@+ 8r+@ \+@?G+@!1+@o-G+@@9^+@E;O+@P+@\V+@hڰ,@Ut^,@&,@ifp,@ȗFZ ,@#nE(,@j/0,@;u8,@ Ǻ@,@җ}G,@tO,@QW,@P. _,@! g,@ o,@Ɣnw,@%Y,@e1C,@6=] .,@I:,@T(,@`,@zl/خ,@Kx¶,@7,@h,@#ASDF BLOCK INDEX %YAML 1.1 --- - 35600 - 35670 - 35740 - 35810 - 35880 - 36134 - 36388 - 36642 - 36896 - 36982 - 37068 - 40226 - 43384 - 49030 - 54676 - 54762 - 54848 - 55102 - 55356 - 55426 - 55496 - 61142 ... ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/gwcs/tests/data/miriwcs.asdf0000644000175100001770000002762214573367100017513 0ustar00runnerdocker#ASDF 1.0.0 #ASDF_STANDARD 1.4.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.5.2} history: extensions: - !core/extension_metadata-1.0.0 extension_class: astropy.io.misc.asdf.extension.AstropyAsdfExtension software: {name: astropy, version: '4.0'} - !core/extension_metadata-1.0.0 extension_class: gwcs.extension.GWCSExtension software: {name: gwcs, version: 0.12.0} - !core/extension_metadata-1.0.0 extension_class: asdf.extension.BuiltinExtension software: {name: asdf, version: 2.5.2} wcs: ! name: '' steps: - ! frame: ! axes_names: [x, y] axes_order: [0, 1] axis_physical_types: ['custom:x', 'custom:y'] name: detector unit: [!unit/unit-1.0.0 'pixel', !unit/unit-1.0.0 'pixel'] transform: !transform/compose-1.1.0 bounding_box: - [-0.5, 1023.5] - [3.5, 1027.5] forward: - !transform/concatenate-1.1.0 forward: - !transform/shift-1.2.0 {offset: 0.15} - !transform/shift-1.2.0 {offset: -0.59} - !transform/compose-1.1.0 forward: - !transform/compose-1.1.0 forward: - !transform/compose-1.1.0 forward: - !transform/compose-1.1.0 forward: - !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: -4.0} - !transform/identity-1.1.0 {} - !transform/concatenate-1.1.0 forward: - !transform/polynomial-1.1.0 coefficients: !core/ndarray-1.0.0 source: 0 datatype: float64 byteorder: little shape: [2] inverse: !transform/polynomial-1.1.0 coefficients: !core/ndarray-1.0.0 source: 1 datatype: float64 byteorder: little shape: [2] name: M_column_correction - !transform/polynomial-1.1.0 coefficients: !core/ndarray-1.0.0 source: 2 datatype: float64 byteorder: little shape: [2] inverse: !transform/polynomial-1.1.0 coefficients: !core/ndarray-1.0.0 source: 3 datatype: float64 byteorder: little shape: [2] name: M_row_correction - !transform/remap_axes-1.1.0 inverse: !transform/identity-1.1.0 {n_dims: 2} mapping: [0, 1, 0, 1] - !transform/concatenate-1.1.0 forward: - !transform/polynomial-1.1.0 coefficients: !core/ndarray-1.0.0 source: 4 datatype: float64 byteorder: little shape: [5, 5] inverse: !transform/polynomial-1.1.0 coefficients: !core/ndarray-1.0.0 source: 5 datatype: float64 byteorder: little shape: [5, 5] name: B_correction - !transform/polynomial-1.1.0 coefficients: !core/ndarray-1.0.0 source: 6 datatype: float64 byteorder: little shape: [5, 5] inverse: !transform/polynomial-1.1.0 coefficients: !core/ndarray-1.0.0 source: 7 datatype: float64 byteorder: little shape: [5, 5] name: A_correction - !transform/remap_axes-1.1.0 inverse: !transform/remap_axes-1.1.0 mapping: [0, 1, 0, 1] mapping: [0, 1, 0, 1] - !transform/concatenate-1.1.0 forward: - !transform/polynomial-1.1.0 coefficients: !core/ndarray-1.0.0 source: 8 datatype: float64 byteorder: little shape: [2, 2] name: TI_row_correction - !transform/polynomial-1.1.0 coefficients: !core/ndarray-1.0.0 source: 9 datatype: float64 byteorder: little shape: [2, 2] name: TI_column_correction - !transform/identity-1.1.0 inverse: !transform/remap_axes-1.1.0 mapping: [0, 1, 0, 1] n_dims: 2 - !transform/remap_axes-1.1.0 mapping: [1, 0] inverse: !transform/compose-1.1.0 forward: - !transform/compose-1.1.0 forward: - !transform/remap_axes-1.1.0 mapping: [1, 0] - !transform/compose-1.1.0 forward: - !transform/remap_axes-1.1.0 mapping: [0, 1, 0, 1] - !transform/compose-1.1.0 forward: - !transform/concatenate-1.1.0 forward: - !transform/polynomial-1.1.0 coefficients: !core/ndarray-1.0.0 source: 10 datatype: float64 byteorder: little shape: [2, 2] name: T_row_correction - !transform/polynomial-1.1.0 coefficients: !core/ndarray-1.0.0 source: 11 datatype: float64 byteorder: little shape: [2, 2] name: T_column_correction - !transform/compose-1.1.0 forward: - !transform/remap_axes-1.1.0 mapping: [0, 1, 0, 1] - !transform/compose-1.1.0 forward: - !transform/concatenate-1.1.0 forward: - !transform/polynomial-1.1.0 coefficients: !core/ndarray-1.0.0 source: 12 datatype: float64 byteorder: little shape: [5, 5] - !transform/polynomial-1.1.0 coefficients: !core/ndarray-1.0.0 source: 13 datatype: float64 byteorder: little shape: [5, 5] - !transform/compose-1.1.0 forward: - !transform/identity-1.1.0 {n_dims: 2} - !transform/compose-1.1.0 forward: - !transform/concatenate-1.1.0 forward: - !transform/polynomial-1.1.0 coefficients: !core/ndarray-1.0.0 source: 14 datatype: float64 byteorder: little shape: [2] - !transform/polynomial-1.1.0 coefficients: !core/ndarray-1.0.0 source: 15 datatype: float64 byteorder: little shape: [2] - !transform/concatenate-1.1.0 forward: - !transform/shift-1.2.0 {offset: 4.0} - !transform/identity-1.1.0 {} - !transform/concatenate-1.1.0 forward: - !transform/shift-1.2.0 {offset: -0.15} - !transform/shift-1.2.0 {offset: 0.59} - ! frame: ! axes_names: [x, y] axes_order: [0, 1] axis_physical_types: ['custom:x', 'custom:y'] name: v2v3 unit: [!unit/unit-1.0.0 'arcsec', !unit/unit-1.0.0 'arcsec'] transform: !transform/compose-1.1.0 forward: - !transform/concatenate-1.1.0 forward: - !transform/scale-1.2.0 {factor: 0.0002777777777777778} - !transform/scale-1.2.0 {factor: 0.0002777777777777778} - !transform/rotate_sequence_3d-1.0.0 angles: [-0.12597594444444443, 0.10374517305555556, 0.0, -72.0545718, -5.630568] axes_order: zyxyz name: v23tosky rotation_type: spherical - ! frame: ! axes_names: [lon, lat] axes_order: [0, 1] axis_physical_types: [pos.eq.ra, pos.eq.dec] name: world reference_frame: ! frame_attributes: {} unit: [!unit/unit-1.0.0 'deg', !unit/unit-1.0.0 'deg'] ... BLK0۴ke-.u33333)?BLK0Eտ5Nh{@D@BLK0۴ke-.u33333)?BLK0Eտ5Nh{@D@BLK0 Z惂<LCjX?_@ r+9:mM)3><ǹ?"KmL-?@n.>TF>6 \UnҪx->Fa>V9>`f>BLK0*W˲VN{C&IiujBCƞ>@ Ո8x:?Z4~\W?rfƾd;P>6X=#>`hg=eLD-BLK0ȓr)ǸW`ӷi?ϝ_2*0IAn d>([RѨ@H`f0'9YE (p]>o-L'? t>3MX/Egy>\` ^>BLK09UHw o+u F_|0W4?Wʽxн{D=_=mR?w8j{?h  X,,R[ތ>Iİ؃>%Mu,i"qF!Z9 ؐ=BLK0 bdإZ*O^ ףp=vwcCnqN?g$.?BLK0 ҍZoJ鄊zG!{g$.cCnqN?BLK0 UϜ4q&+C- Fy@eCnqN?h$.?BLK0 Maxd!&޺]o>/.yh$.cCnqN?BLK09UHw o+u F_|0W4?Wʽxн{D=_=mR?w8j{?h  X,,R[ތ>Iİ؃>%Mu,i"qF!Z9 ؐ=BLK0*W˲VN{C&IiujBCƞ>@ Ո8x:?Z4~\W?rfƾd;P>6X=#>`hg=eLD-BLK0Eտ5Nh{@D@BLK0Eտ5Nh{@D@#ASDF BLOCK INDEX %YAML 1.1 --- - 9730 - 9800 - 9870 - 9940 - 10010 - 10264 - 10518 - 10772 - 11026 - 11112 - 11198 - 11284 - 11370 - 11624 - 11878 - 11948 ... ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/gwcs/tests/data/nircamwcs.asdf0000644000175100001770000002143014573367100020013 0ustar00runnerdocker#ASDF 1.0.0 #ASDF_STANDARD 1.5.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.7.1} history: extensions: - !core/extension_metadata-1.0.0 extension_class: jwst.transforms.jwextension.JWSTExtension software: !core/software-1.0.0 {name: jwst, version: 0.17.2.dev47+g8aa1a931} - !core/extension_metadata-1.0.0 extension_class: astropy.io.misc.asdf.extension.AstropyExtension software: !core/software-1.0.0 {name: astropy, version: 4.0.1.post1} - !core/extension_metadata-1.0.0 extension_class: gwcs.extension.GWCSExtension software: !core/software-1.0.0 {name: gwcs, version: 0.14.1a1.dev15+g0620090.d20201027} - !core/extension_metadata-1.0.0 extension_class: asdf.extension.BuiltinExtension software: !core/software-1.0.0 {name: asdf, version: 2.7.1} - !core/extension_metadata-1.0.0 extension_class: astropy.io.misc.asdf.extension.AstropyAsdfExtension software: !core/software-1.0.0 {name: astropy, version: 4.0.1.post1} wcs: ! name: '' steps: - ! frame: ! axes_names: [x, y] axes_order: [0, 1] axis_physical_types: ['custom:x', 'custom:y'] name: detector unit: [!unit/unit-1.0.0 pixel, !unit/unit-1.0.0 pixel] transform: !transform/compose-1.2.0 bounding_box: - [-0.5, 2047.5] - [-0.5, 2047.5] forward: - !transform/compose-1.2.0 forward: - !transform/compose-1.2.0 forward: - !transform/concatenate-1.2.0 forward: - !transform/shift-1.2.0 {offset: 1.0} - !transform/shift-1.2.0 {offset: 1.0} - !transform/concatenate-1.2.0 forward: - !transform/shift-1.2.0 {offset: -1024.5} - !transform/shift-1.2.0 {offset: -1024.5} - !transform/compose-1.2.0 forward: - !transform/compose-1.2.0 forward: - !transform/remap_axes-1.1.0 mapping: [0, 1, 0, 1] - !transform/concatenate-1.2.0 forward: - !transform/polynomial-1.2.0 coefficients: !core/ndarray-1.0.0 source: 0 datatype: float64 byteorder: little shape: [6, 6] - !transform/polynomial-1.2.0 coefficients: !core/ndarray-1.0.0 source: 1 datatype: float64 byteorder: little shape: [6, 6] - !transform/compose-1.2.0 forward: - !transform/remap_axes-1.1.0 mapping: [0, 1, 0, 1] - !transform/concatenate-1.2.0 forward: - !transform/polynomial-1.2.0 coefficients: !core/ndarray-1.0.0 source: 2 datatype: float64 byteorder: little shape: [2, 2] - !transform/polynomial-1.2.0 coefficients: !core/ndarray-1.0.0 source: 3 datatype: float64 byteorder: little shape: [2, 2] - !transform/concatenate-1.2.0 forward: - !transform/shift-1.2.0 {offset: 86.039011} - !transform/shift-1.2.0 {offset: -493.385704} inverse: !transform/compose-1.2.0 forward: - !transform/concatenate-1.2.0 forward: - !transform/shift-1.2.0 {offset: -86.039011} - !transform/shift-1.2.0 {offset: 493.385704} - !transform/compose-1.2.0 forward: - !transform/compose-1.2.0 forward: - !transform/compose-1.2.0 forward: - !transform/remap_axes-1.1.0 mapping: [0, 1, 0, 1] - !transform/concatenate-1.2.0 forward: - !transform/polynomial-1.2.0 coefficients: !core/ndarray-1.0.0 source: 4 datatype: float64 byteorder: little shape: [2, 2] - !transform/polynomial-1.2.0 coefficients: !core/ndarray-1.0.0 source: 5 datatype: float64 byteorder: little shape: [2, 2] - !transform/compose-1.2.0 forward: - !transform/remap_axes-1.1.0 mapping: [0, 1, 0, 1] - !transform/concatenate-1.2.0 forward: - !transform/polynomial-1.2.0 coefficients: !core/ndarray-1.0.0 source: 6 datatype: float64 byteorder: little shape: [6, 6] - !transform/polynomial-1.2.0 coefficients: !core/ndarray-1.0.0 source: 7 datatype: float64 byteorder: little shape: [6, 6] - !transform/compose-1.2.0 forward: - !transform/concatenate-1.2.0 forward: - !transform/shift-1.2.0 {offset: 1024.5} - !transform/shift-1.2.0 {offset: 1024.5} - !transform/concatenate-1.2.0 forward: - !transform/shift-1.2.0 {offset: -1.0} - !transform/shift-1.2.0 {offset: -1.0} - ! frame: ! axes_names: [x, y] axes_order: [0, 1] axis_physical_types: ['custom:x', 'custom:y'] name: v2v3 unit: [!unit/unit-1.0.0 arcsec, !unit/unit-1.0.0 arcsec] transform: !transform/compose-1.2.0 forward: - !transform/compose-1.2.0 forward: - !transform/compose-1.2.0 forward: - !transform/concatenate-1.2.0 forward: - !transform/scale-1.2.0 {factor: 0.0002777777777777778} - !transform/scale-1.2.0 {factor: 0.0002777777777777778} - ! {transform_type: spherical_to_cartesian, wrap_lon_at: 180} - !transform/rotate_sequence_3d-1.0.0 angles: [0.023917627222222224, 0.13700764222222223, 359.9258631115845, -71.99550858333333, -5.868934166666667] axes_order: zyxyz rotation_type: cartesian - ! {transform_type: cartesian_to_spherical} name: v23tosky - ! frame: ! axes_names: [lon, lat] axes_order: [0, 1] axis_physical_types: [pos.eq.ra, pos.eq.dec] name: world reference_frame: ! frame_attributes: {} unit: [!unit/unit-1.0.0 deg, !unit/unit-1.0.0 deg] ... BLK0   IZ19OWbh]@z+DtNՕ߹SB8;6#?>O@D==zؿ۫U<x^Č>H*T{nkυnYS[4VH A= sL¼G[jsf<5>dg9qkSZ?O{>zjL:9?*/_>B/ "4]u ./@h?:a5m ׾b3-+н? TS↾Ȓ=?}̜پ9vQ0Sd>E;қrGn}нBLK0   w{7StT{ (n/@˦oj[? ؾdD%Jνgδ?)7ײMG]:lR>5ښ/VdTd>۾㡋Y%~ۻDc3>M P>qw4Wy=%]xFѽUBq#ASDF BLOCK INDEX %YAML 1.1 --- - 7180 - 7522 - 7864 - 7950 - 8036 - 8122 - 8208 - 8550 ... ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/gwcs/tests/data/simple_wcs2.hdr0000644000175100001770000000550014573367100020114 0ustar00runnerdockerWCSAXES = 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/gwcs/tests/data/stokes.txt0000644000175100001770000000550014573367100017237 0ustar00runnerdockerWCSAXES = 4 / Number of coordinate axes CRPIX1 = 126.0 / Pixel coordinate of reference point CRPIX2 = 126.0 / Pixel coordinate of reference point CRPIX3 = 1.0 / Pixel coordinate of reference point CRPIX4 = 1.0 / Pixel coordinate of reference point CDELT1 = -2.777777777778E-05 / [deg] Coordinate increment at reference point CDELT2 = 2.777777777778E-05 / [deg] Coordinate increment at reference point CDELT3 = 20000205938.09 / [Hz] Coordinate increment at reference point CDELT4 = 1.0 / Coordinate increment at reference point CUNIT1 = 'deg' / Units of coordinate increment and value CUNIT2 = 'deg' / Units of coordinate increment and value CUNIT3 = 'Hz' / Units of coordinate increment and value CTYPE1 = 'RA---SIN' / Right ascension, orthographic/synthesis projectCTYPE2 = 'DEC--SIN' / Declination, orthographic/synthesis projection CTYPE3 = 'FREQ' / Frequency (linear) CTYPE4 = 'STOKES' / Coordinate type code CRVAL1 = 202.78453375 / [deg] Coordinate value at reference point CRVAL2 = 30.50915555556 / [deg] Coordinate value at reference point CRVAL3 = 233000102969.0 / [Hz] Coordinate value at reference point CRVAL4 = 1.0 / Coordinate value at reference point PV2_1 = 0.0 / SIN projection parameter PV2_2 = 0.0 / SIN projection parameter LONPOLE = 180.0 / [deg] Native longitude of celestial pole LATPOLE = 30.50915555556 / [deg] Native latitude of celestial pole RESTFRQ = 224000000000.1 / [Hz] Line rest frequency TIMESYS = 'UTC' / Time scale MJDREF = 0.0 / [d] MJD of fiducial time DATE-OBS= '2014-07-01T21:36:05.280000' / ISO-8601 time of observation MJD-OBS = 56839.900061111 / [d] MJD of observation OBSGEO-X= 2225142.180269 / [m] observatory X-coordinate OBSGEO-Y= -5440307.370349 / [m] observatory Y-coordinate OBSGEO-Z= -2481029.851874 / [m] observatory Z-coordinate RADESYS = 'FK5' / Equatorial coordinate system EQUINOX = 2000.0 / [yr] Equinox of equatorial coordinates SPECSYS = 'TOPOCENT' / Reference frame of spectral coordinates END ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/gwcs/tests/test_api.py0000644000175100001770000004406714573367100016452 0ustar00runnerdocker# 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 import astropy.modeling.models as m import gwcs.coordinate_frames as cf import gwcs # 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_2d_generic(gwcs_2d_quantity_shift): waoc = gwcs_2d_quantity_shift.world_axis_object_components assert waoc == [('SPATIAL', 0, 'value'), ('SPATIAL1', 0, 'value')] 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[0:3] == [('celestial', 0, 'spherical.lon'), ('celestial', 1, 'spherical.lat'), ('spectral', 0, 'value')] assert waoc[3][0:2] == ('temporal', 0) 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_2d_generic(gwcs_2d_quantity_shift): waoc = gwcs_2d_quantity_shift.world_axis_object_classes assert waoc['SPATIAL'][0] is u.Quantity assert waoc['SPATIAL1'][0] is u.Quantity assert waoc['SPATIAL'][1] == tuple() assert waoc['SPATIAL1'][1] == tuple() assert 'unit' in waoc['SPATIAL'][2] assert 'unit' in waoc['SPATIAL1'][2] assert waoc['SPATIAL'][2]['unit'] == u.km assert waoc['SPATIAL1'][2]['unit'] == u.km 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 elif isinstance(wc1, coord.StokesCoord): 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].transform.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 = coord.StokesCoord([['I', 'Q', 'U', 'V'], ['I', 'Q', 'U', 'V'], ['I', 'Q', 'U', 'V'], ['I', 'Q', 'U', 'V']]) assert (out == expected).all() pixel_input = [-1, 4] out = hlvl.pixel_to_world(pixel_input*u.pix) assert np.isnan(out.value).all() pixel_input = [[-1, 4], [1, 2]] out = hlvl.pixel_to_world(pixel_input*u.pix) assert np.isnan(out[0].value).all() assert (out[1] == ['Q', 'U']).all() out = hlvl.pixel_to_world(1*u.pix) assert isinstance(out, coord.StokesCoord) 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)) assert wcsobj.array_shape == wcsobj.pixel_shape[::-1] wcsobj.pixel_shape = (1111, 2222) assert wcsobj.array_shape == (2222, 1111) @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 def test_composite_many_base_frame(): q_frame_1 = cf.CoordinateFrame(name='distance', axes_order=(0,), naxes=1, axes_type="SPATIAL", unit=(u.m,)) q_frame_2 = cf.CoordinateFrame(name='distance', axes_order=(1,), naxes=1, axes_type="SPATIAL", unit=(u.m,)) frame = cf.CompositeFrame([q_frame_1, q_frame_2]) wao_classes = frame._world_axis_object_classes assert len(wao_classes) == 2 assert not set(wao_classes.keys()).difference({"SPATIAL", "SPATIAL1"}) wao_components = frame._world_axis_object_components assert len(wao_components) == 2 assert not {c[0] for c in wao_components}.difference({"SPATIAL", "SPATIAL1"}) def test_coordinate_frame_api(): forward = m.Linear1D(slope=0.1*u.deg/u.pix, intercept=0*u.deg) output_frame = cf.CoordinateFrame(1, "SPATIAL", (0,), unit=(u.deg,), name="sepframe") input_frame = cf.CoordinateFrame(1, "PIXEL", (0,), unit=(u.pix,)) wcs = gwcs.WCS(forward_transform=forward, input_frame=input_frame, output_frame=output_frame) world = wcs.pixel_to_world(0) assert isinstance(world, u.Quantity) pixel = wcs.world_to_pixel(world) assert isinstance(pixel, float) pixel2 = wcs.invert(world) assert u.allclose(pixel2, 0*u.pix) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/gwcs/tests/test_api_slicing.py0000644000175100001770000004120414573367100020150 0ustar00runnerdocker import astropy.units as u from astropy.coordinates import Galactic, SkyCoord, SpectralCoord from astropy.wcs.wcsapi.wrappers 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 SpectralCoord 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) == EXPECTED_ELLIPSIS_REPR.strip() assert EXPECTED_ELLIPSIS_REPR.strip() in repr(wcs) 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) == EXPECTED_SPECTRAL_SLICE_REPR.strip() assert EXPECTED_SPECTRAL_SLICE_REPR.strip() in repr(wcs) 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 SpectralCoord 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) == EXPECTED_SPECTRAL_RANGE_REPR.strip() assert EXPECTED_SPECTRAL_RANGE_REPR.strip() in repr(wcs) 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 SpectralCoord 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), (10.24, 20, 25)) assert_allclose(wcs.array_index_to_world_values(44, 39), (10.24, 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) == EXPECTED_CELESTIAL_SLICE_REPR.strip() assert EXPECTED_CELESTIAL_SLICE_REPR.strip() in repr(wcs) 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 SpectralCoord 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) == EXPECTED_CELESTIAL_RANGE_REPR.strip() assert EXPECTED_CELESTIAL_RANGE_REPR.strip() in repr(wcs) 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 SpectralCoord 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) == EXPECTED_NO_SHAPE_REPR.strip() assert EXPECTED_NO_SHAPE_REPR.strip() in repr(wcs) # 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) == EXPECTED_ELLIPSIS_REPR_NONE_TYPES.strip() assert EXPECTED_ELLIPSIS_REPR_NONE_TYPES.strip() in repr(wcs) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/gwcs/tests/test_coordinate_systems.py0000644000175100001770000004104314573367100021606 0ustar00runnerdocker# Licensed under a 3-clause BSD style license - see LICENSE.rst import pytest import logging import numpy as np from numpy.testing import assert_allclose import astropy.units as u from astropy.time import Time, TimeDelta from astropy import coordinates as coord from astropy.tests.helper import assert_quantity_allclose from astropy.modeling import models as m from astropy.wcs.wcsapi.fitswcs import CTYPE_TO_UCD1 from astropy.coordinates import StokesCoord from .. import WCS 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]) def test_coordinates_composite_order(): time = cf.TemporalFrame(Time("2011-01-01T00:00:00"), name='time', unit=[u.s, ], axes_order=(0, )) dist = cf.CoordinateFrame(name='distance', naxes=1, axes_type=["SPATIAL"], unit=[u.m, ], axes_order=(1, )) frame = cf.CompositeFrame([time, dist]) result = frame.coordinates(0, 0) assert result[0] == Time("2011-01-01T00:00:00") assert u.allclose(result[1], 0*u.m) def test_bare_baseframe(): # This is a regression test for the following call: frame = cf.CoordinateFrame(1, "SPATIAL", (0,), unit=(u.km,)) assert u.allclose(frame.coordinate_to_quantity((1*u.m,)), 1*u.m) # Now also setup the same situation through the whole call stack to be safe. w = WCS(forward_transform=m.Tabular1D(points=np.arange(10)*u.pix, lookup_table=np.arange(10)*u.km), output_frame=frame, input_frame=cf.CoordinateFrame(1, "PIXEL", (0,), unit=(u.pix,), name="detector_frame") ) assert u.allclose(w.world_to_pixel(0*u.km), 0) @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) 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 assert t.coordinates(TimeDelta(10, format='sec')) == 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(1) == 'I' assert sf.coordinates(1 * u.pix) == 'I' assert sf.coordinate_to_quantity(StokesCoord('I')) == 1 * u.one assert sf.coordinate_to_quantity(1) == 1 @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) def test_ucd1_to_ctype_not_out_of_sync(caplog): """ Test that our code is not out-of-sync with ``astropy``'s definition of ``CTYPE_TO_UCD1`` and our dictionary of allowed duplicates. If this test is failing, update ``coordinate_frames._ALLOWED_UCD_DUPLICATES`` dictionary with new types defined in ``astropy``'s ``CTYPE_TO_UCD1``. """ cf._ucd1_to_ctype_name_mapping( ctype_to_ucd=CTYPE_TO_UCD1, allowed_ucd_duplicates=cf._ALLOWED_UCD_DUPLICATES ) assert len(caplog.record_tuples) == 0 def test_ucd1_to_ctype(caplog): new_ctype_to_ucd = { 'RPT1': 'new.repeated.type', 'RPT2': 'new.repeated.type', 'RPT3': 'new.repeated.type', } ctype_to_ucd = dict(**CTYPE_TO_UCD1, **new_ctype_to_ucd) inv_map = cf._ucd1_to_ctype_name_mapping( ctype_to_ucd=ctype_to_ucd, allowed_ucd_duplicates=cf._ALLOWED_UCD_DUPLICATES ) assert caplog.record_tuples[-1][1] == logging.WARNING and \ caplog.record_tuples[-1][2].startswith( "Found unsupported duplicate physical type" ) for k, v in cf._ALLOWED_UCD_DUPLICATES.items(): assert inv_map.get(k, '') == v for k, v in inv_map.items(): assert ctype_to_ucd[v] == k assert inv_map['new.repeated.type'] in new_ctype_to_ucd ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/gwcs/tests/test_extension.py0000644000175100001770000000361514573367100017707 0ustar00runnerdockerimport io import warnings import asdf import asdf_wcs_schemas import gwcs.extension import pytest @pytest.mark.skipif(asdf_wcs_schemas.__version__ < "0.2.0", reason="version 0.2 provides the new manifests") def test_empty_extension(): """ Test that an empty extension was installed for gwcs 1.0.0 and that extensions are installed for gwcs 1.0.1 and 1.1.0 """ extensions = gwcs.extension.get_extensions() assert len(extensions) > 1 extensions_by_uri = {ext.extension_uri: ext for ext in extensions} # check for duplicate uris assert len(extensions_by_uri) == len(extensions) # check that all 3 versions are installed for version in ('1.0.0', '1.0.1', '1.1.0'): assert f"asdf://asdf-format.org/astronomy/gwcs/extensions/gwcs-{version}" in extensions_by_uri # the 1.0.0 extension should support no tags or types legacy = extensions_by_uri["asdf://asdf-format.org/astronomy/gwcs/extensions/gwcs-1.0.0"] assert len(legacy.tags) == 0 assert len(legacy.converters) == 0 def test_open_legacy_without_warning(): """ Opening a file produced with extension 1.0.0 should not produce any warnings because of the empty extension registered for 1.0.0 """ asdf_bytes = b"""#ASDF 1.0.0 #ASDF_STANDARD 1.5.0 %YAML 1.1 %TAG ! tag:stsci.edu:asdf/ --- !core/asdf-1.1.0 asdf_library: !core/software-1.0.0 {author: The ASDF Developers, homepage: 'http://github.com/asdf-format/asdf', name: asdf, version: 2.9.2} history: extensions: - !core/extension_metadata-1.0.0 extension_class: asdf.extension._manifest.ManifestExtension extension_uri: asdf://asdf-format.org/astronomy/gwcs/extensions/gwcs-1.0.0 software: !core/software-1.0.0 {name: gwcs, version: 0.18.0} foo: 1 ...""" with warnings.catch_warnings(): warnings.simplefilter("error") with asdf.open(io.BytesIO(asdf_bytes)) as af: assert af['foo'] == 1 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/gwcs/tests/test_geometry.py0000644000175100001770000001633714573367100017533 0ustar00runnerdocker# Licensed under a 3-clause BSD style license - see LICENSE.rst from itertools import product, permutations import io import pytest import asdf import numpy as np from astropy import units as u try: from asdf_astropy.testing.helpers import assert_model_roundtrip except ImportError: from asdf_astropy.converters.transform.tests.test_transform import assert_model_roundtrip from .. import geometry _INV_SQRT2 = 1.0 / np.sqrt(2.0) def test_spherical_cartesian_inverse(): t = geometry.SphericalToCartesian() assert type(t.inverse) == geometry.CartesianToSpherical t = geometry.CartesianToSpherical() assert type(t.inverse) == geometry.SphericalToCartesian @pytest.mark.parametrize( 'testval, unit, wrap_at', product( [ (45.0, -90.0, (0.0, 0.0, -1.0)), (45.0, -45.0, (0.5, 0.5, -_INV_SQRT2)), (45, 0.0, (_INV_SQRT2, _INV_SQRT2, 0.0)), (45.0, 45, (0.5, 0.5, _INV_SQRT2)), (45.0, 90.0, (0.0, 0.0, 1.0)), (135.0, -90.0, (0.0, 0.0, -1.0)), (135.0, -45.0, (-0.5, 0.5, -_INV_SQRT2)), (135.0, 0.0, (-_INV_SQRT2, _INV_SQRT2, 0.0)), (135.0, 45.0, (-0.5, 0.5, _INV_SQRT2)), (135.0, 90.0, (0.0, 0.0, 1.0)), (225.0, -90.0, (0.0, 0.0, -1.0)), (225.0, -45.0, (-0.5, -0.5, -_INV_SQRT2)), (225.0, 0.0, (-_INV_SQRT2, -_INV_SQRT2, 0.0)), (225.0, 45.0, (-0.5, -0.5, _INV_SQRT2)), (225.0, 90.0, (0.0, 0.0, 1.0)), (315.0, -90.0, (0.0, 0.0, -1.0)), (315.0, -45.0, (0.5, -0.5, -_INV_SQRT2)), (315.0, 0.0, (_INV_SQRT2, -_INV_SQRT2, 0.0)), (315.0, 45.0, (0.5, -0.5, _INV_SQRT2)), (315.0, 90.0, (0.0, 0.0, 1.0)), ], [1, 1 * u.deg, 3600.0 * u.arcsec, np.pi / 180.0 * u.rad], [180, 360], ) ) def test_spherical_to_cartesian(testval, unit, wrap_at): s2c = geometry.SphericalToCartesian(wrap_lon_at=wrap_at) ounit = 1 if unit == 1 else u.dimensionless_unscaled lon, lat, expected = testval if wrap_at == 180: lon = np.mod(lon - 180.0, 360.0) - 180.0 xyz = s2c(lon * unit, lat * unit) if unit != 1: assert xyz[0].unit == u.dimensionless_unscaled assert u.allclose(xyz, u.Quantity(expected, ounit), atol=1e-15 * ounit) @pytest.mark.parametrize( 'lon, lat, unit, wrap_at', list(product( [0, 45, 90, 135, 180, 225, 270, 315, 360], [-90, -89, -55, 0, 25, 89, 90], [1, 1 * u.deg, 3600.0 * u.arcsec, np.pi / 180.0 * u.rad], [180, 360], )) ) def test_spher2cart_roundrip(lon, lat, unit, wrap_at): s2c = geometry.SphericalToCartesian(wrap_lon_at=wrap_at) c2s = geometry.CartesianToSpherical(wrap_lon_at=wrap_at) ounit = 1 if unit == 1 else u.deg if wrap_at == 180: lon = np.mod(lon - 180.0, 360.0) - 180.0 assert s2c.wrap_lon_at == wrap_at assert c2s.wrap_lon_at == wrap_at assert u.allclose( c2s(*s2c(lon * unit, lat * unit)), (lon * ounit, lat * ounit), atol=1e-15 * ounit ) def test_cart2spher_at_pole(cart_to_spher): assert np.allclose(cart_to_spher(0, 0, 1), (0, 90), rtol=0, atol=1e-15) @pytest.mark.parametrize( 'lonlat, unit, wrap_at', list(product( [ [[1], [-80]], [[325], [-89]], [[0, 1, 120, 180, 225, 325, 359], [-89, 0, 89, 10, -15, 45, -30]], [np.array([0.0, 1, 120, 180, 225, 325, 359]), np.array([-89, 0.0, 89, 10, -15, 45, -30])] ], [None, 1 * u.deg], [180, 360], )) ) def test_spher2cart_roundrip_arr(lonlat, unit, wrap_at): lon, lat = lonlat s2c = geometry.SphericalToCartesian(wrap_lon_at=wrap_at) c2s = geometry.CartesianToSpherical(wrap_lon_at=wrap_at) if wrap_at == 180: if isinstance(lon, np.ndarray): lon = np.mod(lon - 180.0, 360.0) - 180.0 else: lon = [((l - 180.0) % 360.0) - 180.0 for l in lon] atol = 1e-15 if unit is None: olon = lon olat = lat else: olon = lon * u.deg olat = lat * u.deg lon = lon * unit lat = lat * unit atol = atol * u.deg assert u.allclose( c2s(*s2c(lon, lat)), (olon, olat), atol=atol ) @pytest.mark.parametrize('unit1, unit2', [(u.deg, 1), (1, u.deg)]) def test_spherical_to_cartesian_mixed_Q(spher_to_cart, unit1, unit2): with pytest.raises(TypeError) as arg_err: spher_to_cart(135.0 * unit1, 45.0 * unit2) assert (arg_err.value.args[0] == "All arguments must be of the same type " "(i.e., quantity or not).") @pytest.mark.parametrize( 'x, y, z', sorted(list(set( tuple(permutations([1 * u.m, 1, 1])) + tuple(permutations([1 * u.m, 1 * u.m, 1])) )), key=str) ) def test_cartesian_to_spherical_mixed_Q(cart_to_spher, x, y, z): with pytest.raises(TypeError) as arg_err: cart_to_spher(x, y, z) assert (arg_err.value.args[0] == "All arguments must be of the same type " "(i.e., quantity or not).") @pytest.mark.parametrize('wrap_at', ['1', 180., True, 180j, [180], -180, 0]) def test_c2s2c_wrong_wrap_type(spher_to_cart, cart_to_spher, wrap_at): err_msg = "'wrap_lon_at' must be an integer number: 180 or 360" with pytest.raises(ValueError) as arg_err: geometry.SphericalToCartesian(wrap_lon_at=wrap_at) assert arg_err.value.args[0] == err_msg with pytest.raises(ValueError) as arg_err: spher_to_cart.wrap_lon_at = wrap_at assert arg_err.value.args[0] == err_msg with pytest.raises(ValueError) as arg_err: geometry.CartesianToSpherical(wrap_lon_at=wrap_at) assert arg_err.value.args[0] == err_msg with pytest.raises(ValueError) as arg_err: cart_to_spher.wrap_lon_at = wrap_at assert arg_err.value.args[0] == err_msg def test_cartesian_spherical_asdf(tmpdir): s2c0 = geometry.SphericalToCartesian(wrap_lon_at=360) c2s0 = geometry.CartesianToSpherical(wrap_lon_at=180) # asdf round-trip test: assert_model_roundtrip(c2s0, tmpdir) assert_model_roundtrip(s2c0, tmpdir) # create file object f = asdf.AsdfFile({'c2s': c2s0, 's2c': s2c0}) # write to... buf = io.BytesIO() f.write_to(buf) # read back: buf.seek(0) f = asdf.open(buf) # retrieve transformations: c2s = f['c2s'] s2c = f['s2c'] pcoords = [ (45.0, -90.0), (45.0, -45.0), (45, 0.0), (45.0, 45), (45.0, 90.0), (135.0, -90.0), (135.0, -45.0), (135.0, 0.0), (135.0, 45.0), (135.0, 90.0) ] ncoords = [ (225.0, -90.0), (225.0, -45.0), (225.0, 0.0), (225.0, 45.0), (225.0, 90.0), (315.0, -90.0), (315.0, -45.0), (315.0, 0.0), (315.0, 45.0), (315.0, 90.0) ] for lon, lat in pcoords: xyz = s2c(lon, lat) assert xyz == s2c0(lon, lat) lon2, lat2 = c2s(*xyz) assert lon2, lat2 == c2s0(*xyz) assert np.allclose((lon, lat), (lon2, lat2)) for lon, lat in ncoords: xyz = s2c(lon, lat) assert xyz == s2c0(lon, lat) lon2, lat2 = c2s(*xyz) lon3, lat3 = s2c.inverse(*xyz) assert lon2, lat2 == c2s0(*xyz) assert np.allclose((lon, lat), (lon2 + 360, lat2)) assert np.allclose((lon, lat), (lon3, lat2)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/gwcs/tests/test_region.py0000644000175100001770000002212414573367100017152 0ustar00runnerdocker # 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, dtype=int) 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 return ar def two_polygons(): ar = np.zeros((301, 301), dtype=int) 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 ar[:31, 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=int) mask = pol.scan(mask) pol1 = polygon1() assert_equal(mask, pol1) def test_polygon_zero_width_bbox(): vert = [(1, 1), (1, 3), (1, 6), (1, 1)] pol = region.Polygon('1', vert) mask = np.zeros((9, 9), dtype=int) mask = pol.scan(mask) assert not np.any(mask) 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/gwcs/tests/test_spectroscopy_models.py0000644000175100001770000000565214573367100021776 0ustar00runnerdockerimport 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/gwcs/tests/test_utils.py0000644000175100001770000001021714573367100017027 0ustar00runnerdocker# Licensed under a 3-clause BSD style license - see LICENSE.rst import os.path 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.tests.helper import assert_quantity_allclose import pytest from numpy.testing import assert_allclose from .. import utils as gwutils from ..utils import UnsupportedProjectionError from . import data data_path = os.path.split(os.path.abspath(data.__file__))[0] 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(os.path.join(data_path, "simple_wcs2.hdr")) gw1 = gwutils.make_fitswcs_transform(hdr) w1 = fitswcs.WCS(hdr) assert_allclose(gw1(1, 2), w1.wcs_pix2world(1, 2, 0), atol=10 ** -8) def test_lon_pole(): tan = models.Pix2Sky_TAN() car = models.Pix2Sky_CAR() azp = models.Pix2Sky_AZP(mu=-1.35, gamma=25.8458) 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, 0.34 * u.rad), tan), 180 * u.deg) assert_quantity_allclose(gwutils._compute_lon_pole((1 * u.rad, 0.34 * u.rad), azp), 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.00075833, -0.00071019, -0.00066204, -0.00061389, -0.00056574, -0.00051759, -0.00046944, -0.0004213 , -0.00037315, -0.000325]), np.array([-0.00072222, -0.00067407, -0.00062593, -0.00057778, -0.00052963, -0.00048148, -0.00043333, -0.00038519, -0.00033704, -0.00028889]) ) 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(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] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/gwcs/tests/test_wcs.py0000644000175100001770000013602014573367100016464 0ustar00runnerdocker# Licensed under a 3-clause BSD style license - see LICENSE.rst import warnings import os.path import pytest 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 from astropy import wcs as astwcs from astropy.wcs import wcsapi from astropy.time import Time import asdf 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 from .utils import _gwcs_from_hst_fits_wcs from . import data data_path = os.path.split(os.path.abspath(data.__file__))[0] 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', )) time = cf.TemporalFrame(name='time', unit=[u.s, ], axes_order=(3, ), axes_names=('time', ), reference_frame=Time("2020-01-01")) stokes = cf.StokesFrame(axes_order=(2,)) pipe = [wcs.Step(detector, m1), wcs.Step(focal, m2), wcs.Step(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 len(gw._pipeline) == 2 assert gw.pipeline[0].frame == "detector" with pytest.warns(DeprecationWarning, match="Indexing a WCS.pipeline step is deprecated."): assert gw.pipeline[0][0] == "detector" assert gw.pipeline[1].frame == "icrs" with pytest.warns(DeprecationWarning, match="Indexing a WCS.pipeline step is deprecated."): assert gw.pipeline[1][0] == "icrs" assert np.isin(gw.available_frames, ['detector', 'icrs']).all() gw = wcs.WCS(output_frame=icrs, input_frame=detector) assert gw._pipeline[0].frame == "detector" with pytest.warns(DeprecationWarning, match="Indexing a WCS.pipeline step is deprecated."): assert gw._pipeline[0][0] == "detector" assert gw._pipeline[1].frame == "icrs" with pytest.warns(DeprecationWarning, match="Indexing a WCS.pipeline step is deprecated."): assert gw._pipeline[1][0] == "icrs" assert np.isin(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_insert_frame(): """ Test inserting a frame into an existing pipeline """ w = wcs.WCS(pipe[:]) original_result = w(1, 2) mnew = models.Shift(1) & models.Shift(1) new_frame = cf.Frame2D(name='new') # Insert at the beginning w.insert_frame(new_frame, mnew, w.input_frame) assert_allclose(w(0, 1), original_result) tr = w.get_transform('detector', w.output_frame) assert_allclose(tr(1, 2), original_result) # Insert at the end w = wcs.WCS(pipe[:]) with pytest.raises(ValueError, match=r"New coordinate frame.*"): w.insert_frame('not a frame', mnew, new_frame) w.insert_frame('icrs', mnew, new_frame) assert_allclose([x - 1 for x in w(1, 2)], original_result) tr = w.get_transform('detector', 'icrs') assert_allclose(tr(1, 2), original_result) # Force error by trying same operation with pytest.raises(ValueError, match=r".*both frames.*"): w.insert_frame('icrs', mnew, new_frame) 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].transform(x, y), (fx, fy)) assert_allclose(w.pipeline[0].transform(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_backward_transform_has_inverse(): """ Test that backward transform has an inverse, which is the forward transform """ poly = models.Polynomial1D(1, c0=4) poly.inverse = models.Polynomial1D(1, c0=-3) # this is NOT the actual inverse of poly w = wcs.WCS(forward_transform=poly & models.Scale(2), output_frame='sky') assert_allclose(w.backward_transform.inverse(1, 2), w(1, 2)) 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) # CompositeFrame - [celestial, Stokes] output_frame = cf.CompositeFrame(frames=[icrs, stokes]) transform = m1 & models.Identity(1) 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 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)) # Test that bounding_box with quantities can be assigned and evaluates bb = ((1 * u.pix, 5 * u.pix), (2 * u.pix, 6 * u.pix)) trans = models.Shift(10 * u .pix) & models.Shift(2 * u.pix) pipeline = [('detector', trans), ('sky', None)] w = wcs.WCS(pipeline) w.bounding_box = bb assert_allclose(w(-1*u.pix, -1*u.pix), (np.nan, np.nan)) def test_compound_bounding_box(): trans3 = models.Shift(10) & models.Scale(2) & models.Shift(-1) pipeline = [('detector', trans3), ('sky', None)] w = wcs.WCS(pipeline) cbb = { 1: ((-1, 10), (6, 15)), 2: ((-1, 5), (3, 17)), 3: ((-3, 7), (1, 27)), } # Test attaching a valid bounding box (ignoring input 'x') w.attach_compound_bounding_box(cbb, [('x',)]) from astropy.modeling.bounding_box import CompoundBoundingBox cbb = CompoundBoundingBox.validate(trans3, cbb, selector_args=[('x',)], order='F') assert w.bounding_box == cbb assert w.bounding_box is trans3.bounding_box # Test evaluating assert_allclose(w(13, 2, 1), (np.nan, np.nan, np.nan)) assert_allclose(w(13, 2, 2), (np.nan, np.nan, np.nan)) assert_allclose(w(13, 0, 3), (np.nan, np.nan, np.nan)) # No bounding box for selector with pytest.raises(RuntimeError): w(13, 13, 4) # Test attaching a invalid bounding box (not ignoring input 'x') with pytest.raises(ValueError): w.attach_compound_bounding_box(cbb, [('x', False)]) # Test that bounding_box with quantities can be assigned and evaluates trans = models.Shift(10 * u .pix) & models.Shift(2 * u.pix) pipeline = [('detector', trans), ('sky', None)] w = wcs.WCS(pipeline) cbb = { 1 * u.pix: (1 * u.pix, 5 * u.pix), 2 * u.pix: (2 * u.pix, 6 * u.pix) } w.attach_compound_bounding_box(cbb, [('x1',)]) from astropy.modeling.bounding_box import CompoundBoundingBox cbb = CompoundBoundingBox.validate(trans, cbb, selector_args=[('x1',)], order='F') assert w.bounding_box == cbb assert w.bounding_box is trans.bounding_box assert_allclose(w(-1*u.pix, 1*u.pix), (np.nan, np.nan)) assert_allclose(w(7*u.pix, 2*u.pix), (np.nan, np.nan)) 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(os.path.join(data_path, "acs.hdr"), endcard=False) with pytest.warns(astwcs.FITSFixedWarning) as caught_warnings: # this raises a warning unimportant for this testing the pix2world # FITSFixedWarning(u'The WCS transformation has more axes (2) than # the image it is associated with (0)') # FITSFixedWarning: 'datfix' made the change # 'Set MJD-OBS to 53436.000000 from DATE-OBS'. [astropy.wcs.wcs] w = astwcs.WCS(hdr) assert len(caught_warnings) == 2 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") world_coords = coord.SkyCoord(ra, dec, unit = (u.deg, u.deg)) w = wcs_from_points(xy=(x, y), world_coords=world_coords, proj_point=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_coords=world_coords, proj_point=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, time]) transform = m1 & models.Scale(1.5) & models.Scale(2) det = cf.CoordinateFrame(naxes=4, unit=(u.pix, u.pix, u.pix, u.pix), axes_order=(0, 1, 2, 3), axes_type=('length', 'length', 'length', 'length')) w = wcs.WCS(forward_transform=transform, output_frame=output_frame, input_frame=det) wrapped = wcsapi.HighLevelWCSWrapper(w) r, d, lam, t = w(xv, yv, xv, xv) world_coord = w.pixel_to_world(xv, yv, xv, xv) assert isinstance(world_coord[0], coord.SkyCoord) assert isinstance(world_coord[1], u.Quantity) assert isinstance(world_coord[2], Time) 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) assert_allclose((world_coord[2] - time.reference_frame).to(u.s).value, t) wrapped_world_coord = wrapped.pixel_to_world(xv, yv, xv, xv) assert_allclose(wrapped_world_coord[0].data.lon.value, r) assert_allclose(wrapped_world_coord[0].data.lat.value, d) assert_allclose(wrapped_world_coord[1].value, lam) assert_allclose((world_coord[2] - time.reference_frame).to(u.s).value, t) x1, y1, z1, k1 = w.world_to_pixel(*world_coord) assert_allclose(x1, xv) assert_allclose(y1, yv) assert_allclose(z1, xv) assert_allclose(k1, xv) x1, y1, z1, k1 = wrapped.world_to_pixel(*world_coord) assert_allclose(x1, xv) assert_allclose(y1, yv) assert_allclose(z1, xv) assert_allclose(k1, xv) class TestImaging(object): def setup_class(self): hdr = fits.Header.fromtextfile(os.path.join(data_path, "acs.hdr"), endcard=False) with pytest.warns(astwcs.FITSFixedWarning) as caught_warnings: # this raises a warning unimportant for this testing the pix2world # FITSFixedWarning(u'The WCS transformation has more axes (2) than # the image it is associated with (0)') # FITSFixedWarning: 'datfix' made the change # 'Set MJD-OBS to 53436.000000 from DATE-OBS'. [astropy.wcs.wcs] self.fitsw = astwcs.WCS(hdr) assert len(caught_warnings) == 2 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 = [wcs.Step('detector', distortion), wcs.Step('focal', wcs_forward), wcs.Step(sky_cs, None) ] self.wcs = wcs.WCS(input_frame=det, output_frame=sky_cs, forward_transform=pipeline) self.xv, self.yv = xv, yv 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(10, 20, with_units=True) assert np.allclose(self.wcs.invert(sky_coord), (10, 20)) 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) def test_to_fits_sip(): y, x = np.mgrid[:1024:10, :1024:10] xflat = np.ravel(x[1:-1, 1:-1]) yflat = np.ravel(y[1:-1, 1:-1]) fn = os.path.join(data_path, 'miriwcs.asdf') with asdf.open(fn, lazy_load=False, copy_arrays=True, ignore_missing_extensions=True) as af: miriwcs = af.tree['wcs'] bounding_box = ((0, 1024), (0, 1024)) mirisip = miriwcs.to_fits_sip(bounding_box, max_inv_pix_error=0.1, verbose=True) fitssip = astwcs.WCS(mirisip) fitsvalx, fitsvaly = fitssip.all_pix2world(xflat + 1, yflat + 1, 1) gwcsvalx, gwcsvaly = miriwcs(xflat, yflat) assert_allclose(gwcsvalx, fitsvalx, atol=1e-10, rtol=0) assert_allclose(gwcsvaly, fitsvaly, atol=1e-10, rtol=0) fits_inverse_valx, fits_inverse_valy = fitssip.all_world2pix(fitsvalx, fitsvaly, 1) assert_allclose(xflat, fits_inverse_valx - 1, atol=0.1, rtol=0) assert_allclose(yflat, fits_inverse_valy - 1, atol=0.1, rtol=0) mirisip = miriwcs.to_fits_sip(bounding_box=None, max_inv_pix_error=0.1) fitssip = astwcs.WCS(mirisip) fitsvalx, fitsvaly = fitssip.all_pix2world(xflat + 1, yflat + 1, 1) assert_allclose(gwcsvalx, fitsvalx, atol=4e-11, rtol=0) assert_allclose(gwcsvaly, fitsvaly, atol=4e-11, rtol=0) with pytest.raises(ValueError): miriwcs.bounding_box = None mirisip = miriwcs.to_fits_sip(bounding_box=None, max_inv_pix_error=0.1) @pytest.mark.parametrize('matrix_type', ['CD', 'PC-CDELT1', 'PC-SUM1', 'PC-DET1', 'PC-SCALE']) def test_to_fits_sip_pc_normalization(gwcs_simple_imaging_units, matrix_type): y, x = np.mgrid[:1024:10, :1024:10] xflat = np.ravel(x[1:-1, 1:-1]) yflat = np.ravel(y[1:-1, 1:-1]) bounding_box = ((0, 1024), (0, 1024)) # create a simple imaging WCS without distortions: cdmat = np.array([[1.29e-5, 5.95e-6], [5.02e-6, -1.26e-5]]) aff = models.AffineTransformation2D(matrix=cdmat, name='rotation') offx = models.Shift(-501, name='x_translation') offy = models.Shift(-501, name='y_translation') wcslin = (offx & offy) | aff n2c = models.RotateNative2Celestial(5.63, -72.05, 180, name='sky_rotation') tan = models.Pix2Sky_TAN(name='tangent_projection') wcs_forward = wcslin | tan | n2c sky_cs = cf.CelestialFrame(reference_frame=coord.ICRS(), name='sky') pipeline = [('detector', wcs_forward), (sky_cs, None)] wcs_lin = wcs.WCS( input_frame=cf.Frame2D(name='detector'), output_frame=sky_cs, forward_transform=pipeline ) _, _, celestial_group = wcs_lin._separable_groups(detect_celestial=True) fits_wcs = wcs_lin._to_fits_sip( celestial_group=celestial_group, keep_axis_position=False, bounding_box=bounding_box, max_pix_error=0.1, degree=None, max_inv_pix_error=0.1, inv_degree=None, npoints=32, crpix=None, projection='TAN', matrix_type=matrix_type, verbose=True ) fitssip = astwcs.WCS(fits_wcs) fitsvalx, fitsvaly = fitssip.wcs_pix2world(xflat, yflat, 0) inv_fitsvalx, inv_fitsvaly = fitssip.wcs_world2pix(fitsvalx, fitsvaly, 0) gwcsvalx, gwcsvaly = wcs_lin(xflat, yflat) assert_allclose(gwcsvalx, fitsvalx, atol=4e-11, rtol=0) assert_allclose(gwcsvaly, fitsvaly, atol=4e-11, rtol=0) assert_allclose(xflat, inv_fitsvalx, atol=5e-9, rtol=0) assert_allclose(yflat, inv_fitsvaly, atol=5e-9, rtol=0) def test_to_fits_sip_composite_frame(gwcs_cube_with_separable_spectral): w, axes_order = gwcs_cube_with_separable_spectral dec_axis = int(axes_order.index(1) > axes_order.index(0)) + 1 ra_axis = 3 - dec_axis fw_hdr = w.to_fits_sip() assert fw_hdr[f'CTYPE{dec_axis}'] == 'DEC--TAN' assert fw_hdr[f'CTYPE{ra_axis}'] == 'RA---TAN' assert fw_hdr['WCSAXES'] == 2 assert fw_hdr['NAXIS'] == 2 assert fw_hdr['NAXIS1'] == 128 assert fw_hdr['NAXIS2'] == 64 fw = astwcs.WCS(fw_hdr) gskyval = w(1, 60, 55, with_units=True)[0] fskyval = fw.all_pix2world(1, 60, 0) fskyval = [float(fskyval[ra_axis - 1]), float(fskyval[dec_axis - 1])] assert np.allclose([gskyval.ra.value, gskyval.dec.value], fskyval) def test_to_fits_sip_composite_frame_galactic(gwcs_3d_galactic_spectral): w = gwcs_3d_galactic_spectral fw_hdr = w.to_fits_sip() assert fw_hdr['CTYPE1'] == 'GLAT-TAN' fw = astwcs.WCS(fw_hdr) gskyval = w(7, 8, 9, with_units=True)[0] assert np.allclose([gskyval.b.value, gskyval.l.value], fw.all_pix2world(7, 9, 0), atol=1e-3) def test_to_fits_sip_composite_frame_keep_axis(gwcs_cube_with_separable_spectral): from inspect import signature, Parameter w, axes_order = gwcs_cube_with_separable_spectral _, _, celestial_group = w._separable_groups(detect_celestial=True) pars = signature(w.to_fits_sip).parameters kwargs = { k: v.default for k, v in pars.items() if v.default is not Parameter.empty } kwargs['matrix_type'] = 'CD' fw_hdr = w._to_fits_sip( celestial_group=celestial_group, keep_axis_position=True, **kwargs ) ra_axis = axes_order.index(0) + 1 dec_axis = axes_order.index(1) + 1 fw_hdr['CD1_3'] = 1 fw_hdr['CRPIX3'] = 1 assert fw_hdr[f'CTYPE{dec_axis}'] == 'DEC--TAN' assert fw_hdr[f'CTYPE{ra_axis}'] == 'RA---TAN' assert fw_hdr['WCSAXES'] == 2 with pytest.warns(astwcs.FITSFixedWarning, match='The WCS transformation has more axes'): # this raises a warning unimportant for this testing the pix2world # FITSFixedWarning(u'The WCS transformation has more axes (3) than # the image it is associated with (2)') fw = astwcs.WCS(fw_hdr) gskyval = w(1, 45, 55)[1:] assert np.allclose(gskyval, fw.all_pix2world([[1, 45, 55]], 0)[0][1:]) def test_to_fits_tab_no_bb(gwcs_3d_galactic_spectral): # gWCS: w = gwcs_3d_galactic_spectral w.bounding_box = None # FITS WCS -TAB: with pytest.raises(ValueError): hdr, bt = w.to_fits_tab() def test_to_fits_tab_cube(gwcs_3d_galactic_spectral): # gWCS: w = gwcs_3d_galactic_spectral # FITS WCS -TAB: hdr, bt = w.to_fits_tab() hdulist = fits.HDUList( [fits.PrimaryHDU(np.ones(w.pixel_n_dim * (2, )), hdr), bt] ) fits_wcs = astwcs.WCS(hdulist[0].header, hdulist) hdr, bt = w.to_fits_tab(bounding_box=w.bounding_box) hdulist = fits.HDUList( [fits.PrimaryHDU(np.ones(w.pixel_n_dim * (2, )), hdr), bt] ) fits_wcs_user_bb = astwcs.WCS(hdulist[0].header, hdulist) # test points: (xmin, xmax), (ymin, ymax), (zmin, zmax) = w.bounding_box np.random.seed(1) x = xmin + (xmax - xmin) * np.random.random(100) y = ymin + (ymax - ymin) * np.random.random(100) z = zmin + (zmax - zmin) * np.random.random(100) # test: assert np.allclose(w(x, y, z), fits_wcs.wcs_pix2world(x, y, z, 0), rtol=1e-6, atol=1e-7) assert np.allclose(w(x, y, z), fits_wcs_user_bb.wcs_pix2world(x, y, z, 0), rtol=1e-6, atol=1e-7) @pytest.mark.filterwarnings('ignore:.*The WCS transformation has more axes.*') def test_to_fits_tab_7d(gwcs_7d_complex_mapping): # gWCS: w = gwcs_7d_complex_mapping # create FITS headers and -TAB headers hdr, bt = w.to_fits(projection='TAN') # create FITS WCS object: hdus = [fits.PrimaryHDU(np.zeros(w.array_shape), hdr)] hdus.extend(bt) hdulist = fits.HDUList(hdus) fits_wcs = astwcs.WCS(hdulist[0].header, hdulist) # test points: np.random.seed(1) npts = 100 pts = np.zeros((len(w.bounding_box) + 1, npts)) for k, r in enumerate(w.bounding_box): xmin, xmax = w.bounding_box[k] pts[k, :] = xmin + (xmax - xmin) * np.random.random(npts) world_crds = w(*pts[:-1, :]) # test forward transformation: assert np.allclose(world_crds, fits_wcs.wcs_pix2world(*pts, 0)) # test round-tripping: assert np.allclose(pts, fits_wcs.wcs_world2pix(*world_crds, 0)) @pytest.mark.skip(reason="Fails round-trip for -TAB axis 4") def test_to_fits_mixed_4d(gwcs_spec_cel_time_4d): # gWCS: w = gwcs_spec_cel_time_4d # create FITS headers and -TAB headers hdr, bt = w.to_fits() # create FITS WCS object: hdus = [fits.PrimaryHDU(np.zeros(w.array_shape), hdr)] hdus.extend(bt) hdulist = fits.HDUList(hdus) fits_wcs = astwcs.WCS(hdulist[0].header, hdulist) # test points: np.random.seed(1) npts = 100 pts = np.zeros((len(w.bounding_box), npts)) for k, r in enumerate(w.bounding_box): xmin, xmax = w.bounding_box[k] pts[k, :] = xmin + (xmax - xmin) * np.random.random(npts) world_crds = w(*pts) # test forward transformation: assert np.allclose(world_crds, fits_wcs.wcs_pix2world(*pts, 0)) # test round-tripping: pts2 = np.array(fits_wcs.wcs_world2pix(*world_crds, 0)) assert np.allclose(pts, pts2, rtol=1e-5, atol=1e-5) def test_to_fits_no_sip_used(gwcs_spec_cel_time_4d): # gWCS: w = gwcs_spec_cel_time_4d # create FITS headers and -TAB headers with pytest.warns(UserWarning, match='SIP distortion is not supported when the number'): # UserWarning: SIP distortion is not supported when the number # of axes in WCS is larger than 2. Setting 'degree' # to 1 and 'max_inv_pix_error' to None. hdr, _ = w.to_fits(degree=3) # check that FITS WCS is not using SIP assert not hdr['?_ORDER'] assert not hdr['?P_ORDER'] assert not hdr['A_?_?'] assert not hdr['B_?_?'] assert not any(s.endswith('-SIP') for s in hdr['CTYPE?'].values()) def test_to_fits_1D_round_trip(gwcs_1d_spectral): # gWCS: w = gwcs_1d_spectral # FITS WCS -SIP (for celestial) and -TAB (for spectral): hdr, bt = w.to_fits() hdulist = fits.HDUList( [fits.PrimaryHDU(np.ones(w.array_shape), hdr), bt[0]] ) fits_wcs = astwcs.WCS(hdulist[0].header, hdulist) # test points: np.random.seed(1) (xmin, xmax) = w.bounding_box.bounding_box() x = xmin + (xmax - xmin) * np.random.random(100) # test forward transformation: wt = fits_wcs.wcs_pix2world(x, 0) assert np.allclose(w(x), wt, rtol=1e-6, atol=1e-7) # test inverse (round-trip): xinv = fits_wcs.wcs_world2pix(wt[0], 0)[0] assert np.allclose(x, xinv, rtol=1e-6, atol=1e-7) def test_to_fits_sip_tab_cube(gwcs_cube_with_separable_spectral): # gWCS: w, axes_order = gwcs_cube_with_separable_spectral # FITS WCS -SIP (for celestial) and -TAB (for spectral): hdr, bt = w.to_fits(projection=models.Sky2Pix_TAN(name='TAN')) # create FITS WCS object: hdus = [fits.PrimaryHDU(np.zeros(w.array_shape), hdr)] hdus.extend(bt) hdulist = fits.HDUList(hdus) fits_wcs = astwcs.WCS(hdulist[0].header, hdulist) # test points: (xmin, xmax), (ymin, ymax), (zmin, zmax) = w.bounding_box np.random.seed(1) x = xmin + (xmax - xmin) * np.random.random(100) y = ymin + (ymax - ymin) * np.random.random(100) z = zmin + (zmax - zmin) * np.random.random(100) world_crds = w(x, y, z) # test forward transformation: assert np.allclose(world_crds, fits_wcs.wcs_pix2world(x, y, z, 0)) # test round-tripping: assert np.allclose((x, y, z), fits_wcs.wcs_world2pix(*world_crds, 0)) def test_to_fits_tab_time_cube(gwcs_cube_with_separable_time): # gWCS: w = gwcs_cube_with_separable_time # FITS WCS -SIP (for celestial) and -TAB (for spectral): hdr, bt = w.to_fits(projection=models.Sky2Pix_TAN(name='TAN')) # create FITS WCS object: hdus = [fits.PrimaryHDU(np.zeros(w.array_shape), hdr)] hdus.extend(bt) hdulist = fits.HDUList(hdus) fits_wcs = astwcs.WCS(hdulist[0].header, hdulist) assert np.allclose(hdulist[1].data['coordinates'].ravel(), np.arange(128)) # test points: (xmin, xmax), (ymin, ymax), (zmin, zmax) = w.bounding_box np.random.seed(1) x = xmin + (xmax - xmin) * np.random.random(5) y = ymin + (ymax - ymin) * np.random.random(5) z = zmin + (zmax - zmin) * np.random.random(5) world_crds = w(x, y, z) # test forward transformation: assert np.allclose(world_crds, fits_wcs.wcs_pix2world(x, y, z, 0)) # test round-tripping: assert np.allclose((x, y, z), fits_wcs.wcs_world2pix(*world_crds, 0), rtol=1e-5, atol=1e-5) def test_to_fits_tab_miri_image(): # gWCS: fn = os.path.join(data_path, 'miriwcs.asdf') with asdf.open(fn, copy_arrays=True, lazy_load=False, ignore_missing_extensions=True) as af: w = af.tree['wcs'] # FITS WCS -TAB: hdr, bt = w.to_fits_tab(sampling=0.5) hdulist = fits.HDUList( [fits.PrimaryHDU(np.ones(w.pixel_n_dim * (2, )), hdr), bt] ) fits_wcs = astwcs.WCS(hdulist[0].header, hdulist) # test points: (xmin, xmax), (ymin, ymax) = w.bounding_box np.random.seed(1) x = xmin + (xmax - xmin) * np.random.random(100) y = ymin + (ymax - ymin) * np.random.random(100) # test: assert np.allclose(w(x, y), fits_wcs.wcs_pix2world(x, y, 0), rtol=1e-6, atol=1e-7) def test_to_fits_tab_miri_lrs(): fn = os.path.join(data_path, 'miri_lrs_wcs.asdf') with asdf.open(fn, copy_arrays=True, lazy_load=False, ignore_missing_extensions=True) as af: w = af.tree['wcs'] # FITS WCS -TAB: hdr, bt = w.to_fits(sampling=0.25) hdulist = fits.HDUList( [fits.PrimaryHDU(np.ones(w.pixel_n_dim * (2, )), hdr), bt[0]] ) with pytest.warns(astwcs.FITSFixedWarning, match='The WCS transformation has more axes'): # this raises a warning unimportant for this testing the pix2world # FITSFixedWarning(u'The WCS transformation has more axes (3) than # the image it is associated with (2)') fits_wcs = astwcs.WCS(hdulist[0].header, hdulist) # test points: (xmin, xmax), (ymin, ymax) = w.bounding_box np.random.seed(1) x = xmin + (xmax - xmin) * np.random.random(100) y = ymin + (ymax - ymin) * np.random.random(100) # test: ref = np.array(w(x, y)) tab = np.array(fits_wcs.wcs_pix2world(x, y, 0, 0)) m = np.cumprod(np.isfinite(ref), dtype=np.bool_, axis=0) assert hdr['WCSAXES'] == 3 assert np.allclose(ref[m], tab[m], rtol=5e-6, atol=5e-6, equal_nan=True) def test_in_image(): # create a 1-dim WCS: w1 = wcs.WCS( [ (cf.SpectralFrame(name='input', axes_names=('x',), unit=(u.pix,)), models.Scale(2)), (cf.SpectralFrame(name='output', axes_names=('x'), unit=(u.pix,)), None) ] ) w1.bounding_box = (1, 5) assert np.isscalar(w1.in_image(4)) assert w1.in_image(4) assert not w1.in_image(14) assert np.array_equal( w1.in_image([[-1, 4, 11], [2, 3, 12]]), [[False, True, False], [True, True, False]], ) # create a 2-dim WCS: w2 = wcs.WCS([(cf.Frame2D(name='input', axes_names=('x', 'y'), unit=(u.pix, u.pix)), models.Scale(2) & models.Scale(1.5)), (cf.Frame2D(name='output', axes_names=('x', 'y'), unit=(u.pix, u.pix)), None)]) w2.bounding_box = [(1, 100), (2, 20)] assert np.isscalar(w2.in_image(2, 6)) assert not np.isscalar(w2.in_image([2], [6])) assert w2.in_image(4, 6) assert not w2.in_image(5, 0) assert np.array_equal( w2.in_image( [[9, 10, 11, 15], [8, 9, 67, 98], [2, 2, np.nan, 102]], [[9, np.nan, 11, 15], [8, 9, 67, 98], [1, 1, np.nan, -10]] ), [[ True, False, True, True], [ True, True, False, False], [False, False, False, False]], ) def test_iter_inv(): fn = os.path.join(data_path, 'nircamwcs.asdf') with asdf.open(fn, lazy_load=False, copy_arrays=False, ignore_missing_extensions=True) as af: w = af.tree['wcs'] # remove analytic/user-supplied inverse: w.pipeline[0].transform.inverse = None w.bounding_box = None # test single point assert np.allclose((1, 2), w.invert(*w(1, 2))) assert np.allclose( (np.nan, np.nan), w.numerical_inverse(*w(np.nan, 2)), equal_nan=True ) # prepare to test a vector of points: np.random.seed(10) x, y = 2047 * np.random.random((2, 10000)) # "truth" # test adaptive: xp, yp = w.invert( *w(x, y), adaptive=True, detect_divergence=True, quiet=False ) assert np.allclose((x, y), (xp, yp)) with asdf.open(fn, lazy_load=False, copy_arrays=False, ignore_missing_extensions=True) as af: w = af.tree['wcs'] # test single point assert np.allclose((1, 2), w.numerical_inverse(*w(1, 2))) assert np.allclose( (np.nan, np.nan), w.numerical_inverse(*w(np.nan, 2)), equal_nan=True ) # don't detect devergence xp, yp = w.numerical_inverse( *w(x, y), adaptive=True, detect_divergence=False, quiet=False ) assert np.allclose((x, y), (xp, yp)) with pytest.raises(wcs.NoConvergence) as e: w.numerical_inverse( *w([1, 20, 200, 2000], [200, 1000, 2000, 5]), adaptive=True, detect_divergence=True, maxiter=2, # force not reaching requested accuracy quiet=False ) xp, yp = e.value.best_solution.T assert e.value.slow_conv.size == 4 assert np.all(np.sort(e.value.slow_conv) == np.arange(4)) # test non-adaptive: xp, yp = w.numerical_inverse( *w(x, y, with_bounding_box=False), adaptive=False, detect_divergence=True, quiet=False, with_bounding_box=False ) assert np.allclose((x, y), (xp, yp)) # test non-adaptive: x[0] = 3000 y[0] = 10000 xp, yp = w.numerical_inverse( *w(x, y, with_bounding_box=False), adaptive=False, detect_divergence=True, quiet=False, with_bounding_box=False ) assert np.allclose((x, y), (xp, yp)) # test non-adaptive with non-recoverable divergence: x[0] = 300000 y[0] = 1000000 with pytest.raises(wcs.NoConvergence) as e: xp, yp = w.numerical_inverse( *w(x, y, with_bounding_box=False), adaptive=False, detect_divergence=True, quiet=False, with_bounding_box=False ) assert np.allclose((x, y), (xp, yp)) xp, yp = e.value.best_solution.T assert np.allclose((x[1:], y[1:]), (xp[1:], yp[1:])) assert e.value.divergent[0] == 0 def test_tabular_2d_quantity(): shape = (3, 3) data = np.arange(np.prod(shape)).reshape(shape) * u.m / u.s # The integer location is at the centre of the pixel. points_unit = u.pix points = [(np.arange(size) - 0) * points_unit for size in shape] kwargs = { 'bounds_error': False, 'fill_value': np.nan, 'method': 'nearest', } forward = models.Tabular2D(points, data, **kwargs) input_frame = cf.CoordinateFrame(2, ("PIXEL", "PIXEL"), (0,1), unit=(u.pix, u.pix), name="detector") output_frame = cf.CoordinateFrame(1, "CUSTOM", (0,), unit=(u.m/u.s,)) w = wcs.WCS(forward_transform=forward, input_frame=input_frame, output_frame=output_frame) bb = w.bounding_box assert all(u.allclose(u.Quantity(b), [0, 2] * u.pix) for b in bb) def test_initialize_wcs_with_list(): # test that you can initialize a wcs with a pipeline that is a list # containing both Step() and (frame, transform) tuples # make pipeline consisting of tuples and Steps shift1 = models.Shift(10 * u .pix) & models.Shift(2 * u.pix) shift2 = models.Shift(3 * u.pix) pipeline = [('detector', shift1), wcs.Step('extra_step', shift2)] extra_step = ('extra_step', None) pipeline.append(extra_step) # make sure no warnings occur when creating wcs with this pipeline with warnings.catch_warnings(): warnings.simplefilter("error") wcs.WCS(pipeline) def test_sip_roundtrip(): hdr = fits.Header.fromtextfile(os.path.join(data_path, "acs.hdr"), endcard=False) nx = ny = 1024 hdr['naxis'] = 2 hdr['naxis1'] = nx hdr['naxis2'] = ny with warnings.catch_warnings(): warnings.simplefilter("ignore", category=astwcs.FITSFixedWarning) gw = _gwcs_from_hst_fits_wcs(hdr) hdr_back = gw.to_fits_sip( max_pix_error=1e-6, max_inv_pix_error=None, npoints=64, crpix=(hdr['crpix1'], hdr['crpix2']) ) for k in ['naxis', 'naxis1', 'naxis2', 'ctype1', 'ctype2', 'a_order', 'b_order']: assert hdr[k] == hdr_back[k] for k in ['cd1_1', 'cd1_2', 'cd2_1', 'cd2_2']: assert np.allclose(hdr[k], hdr_back[k], atol=0, rtol=1e-9) for t in ('a', 'b'): order = hdr[f'{t}_order'] for i in range(order + 1): for j in range(order + 1): if 1 < i + j <= order: k = f'{t}_{i}_{j}' assert np.allclose( hdr[k], hdr_back[k], atol=0.0, rtol=1.0e-8 * 10**(i + j) ) def test_spatial_spectral_stokes(): """ Converts a FITS WCS to GWCS and compares results.""" hdr = fits.Header.fromfile(os.path.join(data_path, "stokes.txt")) with warnings.catch_warnings(): warnings.simplefilter("ignore", category=astwcs.FITSFixedWarning) aw = astwcs.WCS(hdr) crpix = aw.wcs.crpix crval = aw.wcs.crval cdelt = aw.wcs.cdelt fk5 = cf.CelestialFrame(reference_frame=coord.FK5(), name='FK5') detector = cf.Frame2D(name='detector', axes_order=(0, 1)) spec = cf.SpectralFrame(name='FREQ', unit=[u.Hz, ], axes_order=(2, ), axes_names=('freq', )) stokes = cf.StokesFrame(axes_order=(3,)) world = cf.CompositeFrame(frames=[fk5, spec, stokes]) det2sky = (models.Shift(-crpix[0]) & models.Shift(-crpix[1]) | models.Scale(cdelt[0]) & models.Scale(cdelt[1]) | models.Pix2Sky_SIN() | models.RotateNative2Celestial(crval[0], crval[1], 180)) det2freq = models.Shift(-crpix[2]) | models.Scale(cdelt[2]) | models.Shift(crval[2]) det2stokes = models.Shift(-crpix[3]) | models.Scale(cdelt[3]) | models.Shift(crval[3]) gw = wcs.WCS([wcs.Step(detector, det2sky & det2freq & det2stokes), wcs.Step(world, None)] ) x1 = np.array([0, 0, 0, 0, 0]) x2 = np.array([0, 1, 2, 3, 4]) gw_sky, gw_spec, gw_stokes = gw.pixel_to_world(x1+1, x1+1, x1+1, x2+1) aw_sky, aw_spec, aw_stokes = aw.pixel_to_world(x1, x1, x1, x2) assert_allclose(gw_sky.data.lon, aw_sky.data.lon) assert_allclose(gw_sky.data.lat, aw_sky.data.lat) assert_allclose(gw_spec.value, aw_spec.value) assert_allclose(gw_stokes.value, aw_stokes.value) def test_wcs_str(): w = wcs.WCS(output_frame="icrs") assert 'icrs' in str(w) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/gwcs/tests/utils.py0000644000175100001770000000334714573367100015776 0ustar00runnerdockerimport numpy as np from astropy.modeling.models import ( Shift, Polynomial2D, Pix2Sky_TAN, RotateNative2Celestial, Mapping ) from astropy import coordinates as coord from astropy import units from astropy import wcs as fits_wcs from .. wcs import WCS from .. import coordinate_frames as cf def _gwcs_from_hst_fits_wcs(header, hdu=None): # NOTE: this function ignores table distortions def coeffs_to_poly(mat, degree): pol = Polynomial2D(degree=degree) for i in range(mat.shape[0]): for j in range(mat.shape[1]): if 0 < i + j <= degree: setattr(pol, f'c{i}_{j}', mat[i, j]) return pol w = fits_wcs.WCS(header, hdu) ny, nx = w.pixel_shape x0, y0 = w.wcs.crpix - 1 cd = w.wcs.piximg_matrix cfx, cfy = np.dot(cd, [w.sip.a.ravel(), w.sip.b.ravel()]) a = np.reshape(cfx, w.sip.a.shape) b = np.reshape(cfy, w.sip.b.shape) a[1, 0] = cd[0, 0] a[0, 1] = cd[0, 1] b[1, 0] = cd[1, 0] b[0, 1] = cd[1, 1] polx = coeffs_to_poly(a, w.sip.a_order) poly = coeffs_to_poly(b, w.sip.b_order) # construct GWCS: det2sky = ( (Shift(-x0) & Shift(-y0)) | Mapping((0, 1, 0, 1)) | (polx & poly) | Pix2Sky_TAN() | RotateNative2Celestial(*w.wcs.crval, 180) ) detector_frame = cf.Frame2D(name="detector", axes_names=("x", "y"), unit=(units.pix, units.pix)) sky_frame = cf.CelestialFrame( reference_frame=getattr(coord, w.wcs.radesys).__call__(), name=w.wcs.radesys, unit=(units.deg, units.deg) ) pipeline = [(detector_frame, det2sky), (sky_frame, None)] gw = WCS(pipeline) gw.bounding_box = ((-0.5, nx - 0.5), (-0.5, ny - 0.5)) return gw ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/gwcs/utils.py0000644000175100001770000003425014573367100014631 0ustar00runnerdocker# 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 from astropy.time import Time, TimeDelta from astropy.wcs import Celprm # 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=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 skycoord) 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 celestial longitude and latitude of the fiducial point - typically right ascension and declination. These are given by the ``CRVALia`` keywords in ``FITS``. projection : `astropy.modeling.projections.Projection` A `~astropy.modeling.projections.Projection` model instance. Returns ------- lonpole : float or `~astropy/units.Quantity` Native longitude of the celestial pole in degrees. """ if isinstance(skycoord, coords.SkyCoord): lon = skycoord.spherical.lon.value lat = skycoord.spherical.lat.value unit = u.deg else: lon, lat = skycoord unit = None if isinstance(lon, u.Quantity): lon = lon.to(u.deg).to_value() unit = u.deg if isinstance(lat, u.Quantity): lat = lat.to(u.deg).to_value() unit = u.deg cel = Celprm() cel.ref = [lon, lat] cel.prj.code = projection.prjprm.code pvrange = projection.prjprm.pvrange if pvrange: i1 = pvrange // 100 i2 = i1 + (pvrange % 100) + 1 cel.prj.pv = i1 * [None] + list(projection.prjprm.pv[i1:i2]) cel.set() lonpole = cel.ref[2] if unit is not None: lonpole = lonpole * unit return lonpole 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 - 1), 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, (Time, TimeDelta)): 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/gwcs/wcs.py0000644000175100001770000036245214573367100014275 0ustar00runnerdocker# Licensed under a 3-clause BSD style license - see LICENSE.rst import functools import itertools import warnings import astropy.io.fits as fits import numpy as np import numpy.linalg as npla from astropy import units as u from astropy.modeling import fix_inputs, projections from astropy.modeling.bounding_box import CompoundBoundingBox from astropy.modeling.bounding_box import ModelBoundingBox as Bbox from astropy.modeling.core import Model from astropy.modeling.models import (Const1D, Identity, Mapping, Polynomial2D, RotateCelestial2Native, Shift, Sky2Pix_TAN) from astropy.wcs.utils import celestial_frame_to_wcs, proj_plane_pixel_scales from scipy import linalg, optimize from . import coordinate_frames as cf from . import utils from .api import GWCSAPIMixin from .utils import CoordinateFrameError from .wcstools import grid_from_bounding_box __all__ = ['WCS', 'Step', 'NoConvergence'] _ITER_INV_KWARGS = ['tolerance', 'maxiter', 'adaptive', 'detect_divergence', 'quiet'] class NoConvergence(Exception): """ An error class used to report non-convergence and/or divergence of numerical methods. It is used to report errors in the iterative solution used by the :py:meth:`~astropy.wcs.WCS.all_world2pix`. Attributes ---------- best_solution : `numpy.ndarray` Best solution achieved by the numerical method. accuracy : `numpy.ndarray` Estimate of the accuracy of the ``best_solution``. niter : `int` Number of iterations performed by the numerical method to compute ``best_solution``. divergent : None, `numpy.ndarray` Indices of the points in ``best_solution`` array for which the solution appears to be divergent. If the solution does not diverge, ``divergent`` will be set to `None`. slow_conv : None, `numpy.ndarray` Indices of the solutions in ``best_solution`` array for which the solution failed to converge within the specified maximum number of iterations. If there are no non-converging solutions (i.e., if the required accuracy has been achieved for all input data points) then ``slow_conv`` will be set to `None`. """ def __init__(self, *args, best_solution=None, accuracy=None, niter=None, divergent=None, slow_conv=None): super().__init__(*args) self.best_solution = best_solution self.accuracy = accuracy self.niter = niter self.divergent = divergent self.slow_conv = slow_conv class _WorldAxisInfo(): def __init__(self, axis, frame, world_axis_order, cunit, ctype, input_axes): """ A class for holding information about a world axis from an output frame. Parameters ---------- axis : int Output axis number [in the forward transformation]. frame : cf.CoordinateFrame Coordinate frame to which this axis belongs. world_axis_order : int Index of this axis in `gwcs.WCS.output_frame.axes_order` cunit : str Axis unit using FITS convension (``CUNIT``). ctype : str Axis FITS type (``CTYPE``). input_axes : tuple of int Tuple of input axis indices contributing to this world axis. """ self.axis = axis self.frame = frame self.world_axis_order = world_axis_order self.cunit = cunit self.ctype = ctype self.input_axes = input_axes 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._approx_inverse = None self._available_frames = [] self._pipeline = [] self._name = name self._initialize_wcs(forward_transform, input_frame, output_frame) self._pixel_shape = None pipe = [] for step in self._pipeline: if isinstance(step, Step): pipe.append(Step(step.frame, step.transform)) else: pipe.append(Step(*step)) self._pipeline = pipe 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: if isinstance(item, Step): name, frame_obj = self._get_frame_name(item.frame) else: 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_frames.CoordinateFrame` Initial coordinate frame name of object. to_frame : str, or instance of `~gwcs.coordinate_frames.CoordinateFrame` End coordinate frame name or object. Returns ------- transform : `~astropy.modeling.Model` Transform between two frames. """ if not self._pipeline: return None from_ind = self._get_frame_index(from_frame) to_ind = self._get_frame_index(to_frame) if to_ind < from_ind: transforms = [step.transform for step in self._pipeline[to_ind: from_ind]] transforms = [tr.inverse for tr in transforms[::-1]] elif to_ind == from_ind: return None else: transforms = [step.transform for step in self._pipeline[from_ind: to_ind]] 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_frames.CoordinateFrame` Initial coordinate frame. to_frame : str, or instance of `~gwcs.coordinate_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].transform = 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]]) return functools.reduce(lambda x, y: x | y, [step.transform 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)) try: backward.inverse except NotImplementedError: # means "hasattr" won't work backward.inverse = self.forward_transform return backward def _get_frame_index(self, frame): """ Return the index in the pipeline where this frame is locate. """ if isinstance(frame, cf.CoordinateFrame): frame = frame.name frame_names = [step.frame if isinstance(step.frame, str) else step.frame.name for step in self._pipeline] try: return frame_names.index(frame) except ValueError as e: raise CoordinateFrameError(f"Frame {frame} is not in the available frames") from e 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.coordinates.SpectralCoord` 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 bounding_box to it # before evaluating it. The order Model.bounding_box is reversed. 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 in_image(self, *args, **kwargs): """ This method tests if one or more of the input world coordinates are contained within forward transformation's image and that it maps to the domain of definition of the forward transformation. In practical terms, this function tests that input world coordinate(s) can be converted to input frame and that it is within the forward transformation's ``bounding_box`` when defined. Parameters ---------- args : float, array like, `~astropy.coordinates.SkyCoord` or `~astropy.units.Unit` coordinates to be inverted kwargs : dict keyword arguments to be passed either to ``backward_transform`` (when defined) or to the iterative invert method. Returns ------- result : bool, numpy.ndarray A single boolean value or an array of boolean values with `True` indicating that the WCS footprint contains the coordinate and `False` if input is outside the footprint. """ kwargs['with_bounding_box'] = True kwargs['fill_value'] = np.nan coords = self.invert(*args, **kwargs) result = np.isfinite(coords) if self.input_frame.naxes > 1: result = np.all(result, axis=0) if self.bounding_box is None or not np.any(result): return result if self.input_frame.naxes == 1: x1, x2 = self.bounding_box.bounding_box() if len(np.shape(args[0])) > 0: result[result] = (coords[result] >= x1) & (coords[result] <= x2) elif result: result = (coords >= x1) and (coords <= x2) else: if len(np.shape(args[0])) > 0: for c, (x1, x2) in zip(coords, self.bounding_box): result[result] = (c[result] >= x1) & (c[result] <= x2) elif result: result = all([(c >= x1) and (c <= x2) for c, (x1, x2) in zip(coords, self.bounding_box)]) return result def invert(self, *args, **kwargs): """ Invert coordinates from output frame to input frame using analytical or user-supplied inverse. When neither analytical nor user-supplied inverses are defined, a numerical solution will be attempted using :py:meth:`numerical_inverse`. .. note:: Currently numerical inverse is implemented only for 2D imaging WCS. Parameters ---------- args : float, array like, `~astropy.coordinates.SkyCoord` or `~astropy.units.Unit` Coordinates to be inverted. The number of arguments must be equal to the number of world coordinates given by ``world_n_dim``. 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``). with_units : bool, optional If ``True`` returns a `~astropy.coordinates.SkyCoord` or `~astropy.coordinates.SpectralCoord` object, by using the units of the output cooridnate frame. Default is `False`. Other Parameters ---------------- kwargs : dict Keyword arguments to be passed to :py:meth:`numerical_inverse` (when defined) or to the iterative invert method. Returns ------- result : tuple or value Returns a tuple of scalar or array values for each axis. Unless ``input_frame.naxes == 1`` when it shall return the value. """ with_units = kwargs.pop('with_units', False) if not utils.isnumerical(args[0]): args = self.output_frame.coordinate_to_quantity(*args) if self.output_frame.naxes == 1: args = [args] try: if not self.backward_transform.uses_quantity: args = utils.get_values(self.output_frame.unit, *args) except (NotImplementedError, KeyError): args = utils.get_values(self.output_frame.unit, *args) if 'with_bounding_box' not in kwargs: kwargs['with_bounding_box'] = True if 'fill_value' not in kwargs: kwargs['fill_value'] = np.nan try: # remove iterative inverse-specific keyword arguments: akwargs = {k: v for k, v in kwargs.items() if k not in _ITER_INV_KWARGS} result = self.backward_transform(*args, **akwargs) except (NotImplementedError, KeyError): result = self.numerical_inverse(*args, **kwargs, with_units=with_units) 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 numerical_inverse(self, *args, tolerance=1e-5, maxiter=50, adaptive=True, detect_divergence=True, quiet=True, with_bounding_box=True, fill_value=np.nan, with_units=False, **kwargs): """ Invert coordinates from output frame to input frame using numerical inverse. .. note:: Currently numerical inverse is implemented only for 2D imaging WCS. .. note:: This method uses a combination of vectorized fixed-point iterations algorithm and `scipy.optimize.root`. The later is used for input coordinates for which vectorized algorithm diverges. Parameters ---------- args : float, array like, `~astropy.coordinates.SkyCoord` or `~astropy.units.Unit` Coordinates to be inverted. The number of arguments must be equal to the number of world coordinates given by ``world_n_dim``. 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``). with_units : bool, optional If ``True`` returns a `~astropy.coordinates.SkyCoord` or `~astropy.coordinates.SpectralCoord` object, by using the units of the output cooridnate frame. Default is `False`. tolerance : float, optional *Absolute tolerance* of solution. Iteration terminates when the iterative solver estimates that the "true solution" is within this many pixels current estimate, more specifically, when the correction to the solution found during the previous iteration is smaller (in the sense of the L2 norm) than ``tolerance``. Default ``tolerance`` is 1.0e-5. maxiter : int, optional Maximum number of iterations allowed to reach a solution. Default is 50. quiet : bool, optional Do not throw :py:class:`NoConvergence` exceptions when the method does not converge to a solution with the required accuracy within a specified number of maximum iterations set by ``maxiter`` parameter. Instead, simply return the found solution. Default is `True`. adaptive : bool, optional Specifies whether to adaptively select only points that did not converge to a solution within the required accuracy for the next iteration. Default (`True`) is recommended. .. note:: The :py:meth:`numerical_inverse` uses a vectorized implementation of the method of consecutive approximations (see ``Notes`` section below) in which it iterates over *all* input points *regardless* until the required accuracy has been reached for *all* input points. In some cases it may be possible that *almost all* points have reached the required accuracy but there are only a few of input data points for which additional iterations may be needed (this depends mostly on the characteristics of the geometric distortions for a given instrument). In this situation it may be advantageous to set ``adaptive`` = `True` in which case :py:meth:`numerical_inverse` will continue iterating *only* over the points that have not yet converged to the required accuracy. .. note:: When ``detect_divergence`` is `True`, :py:meth:`numerical_inverse` will automatically switch to the adaptive algorithm once divergence has been detected. detect_divergence : bool, optional Specifies whether to perform a more detailed analysis of the convergence to a solution. Normally :py:meth:`numerical_inverse` may not achieve the required accuracy if either the ``tolerance`` or ``maxiter`` arguments are too low. However, it may happen that for some geometric distortions the conditions of convergence for the the method of consecutive approximations used by :py:meth:`numerical_inverse` may not be satisfied, in which case consecutive approximations to the solution will diverge regardless of the ``tolerance`` or ``maxiter`` settings. When ``detect_divergence`` is `False`, these divergent points will be detected as not having achieved the required accuracy (without further details). In addition, if ``adaptive`` is `False` then the algorithm will not know that the solution (for specific points) is diverging and will continue iterating and trying to "improve" diverging solutions. This may result in ``NaN`` or ``Inf`` values in the return results (in addition to a performance penalties). Even when ``detect_divergence`` is `False`, :py:meth:`numerical_inverse`, at the end of the iterative process, will identify invalid results (``NaN`` or ``Inf``) as "diverging" solutions and will raise :py:class:`NoConvergence` unless the ``quiet`` parameter is set to `True`. When ``detect_divergence`` is `True` (default), :py:meth:`numerical_inverse` will detect points for which current correction to the coordinates is larger than the correction applied during the previous iteration **if** the requested accuracy **has not yet been achieved**. In this case, if ``adaptive`` is `True`, these points will be excluded from further iterations and if ``adaptive`` is `False`, :py:meth:`numerical_inverse` will automatically switch to the adaptive algorithm. Thus, the reported divergent solution will be the latest converging solution computed immediately *before* divergence has been detected. .. note:: When accuracy has been achieved, small increases in current corrections may be possible due to rounding errors (when ``adaptive`` is `False`) and such increases will be ignored. .. note:: Based on our testing using JWST NIRCAM images, setting ``detect_divergence`` to `True` will incur about 5-10% performance penalty with the larger penalty corresponding to ``adaptive`` set to `True`. Because the benefits of enabling this feature outweigh the small performance penalty, especially when ``adaptive`` = `False`, it is recommended to set ``detect_divergence`` to `True`, unless extensive testing of the distortion models for images from specific instruments show a good stability of the numerical method for a wide range of coordinates (even outside the image itself). .. note:: Indices of the diverging inverse solutions will be reported in the ``divergent`` attribute of the raised :py:class:`NoConvergence` exception object. Returns ------- result : tuple Returns a tuple of scalar or array values for each axis. Raises ------ NoConvergence The iterative method did not converge to a solution to the required accuracy within a specified number of maximum iterations set by the ``maxiter`` parameter. To turn off this exception, set ``quiet`` to `True`. Indices of the points for which the requested accuracy was not achieved (if any) will be listed in the ``slow_conv`` attribute of the raised :py:class:`NoConvergence` exception object. See :py:class:`NoConvergence` documentation for more details. NotImplementedError Numerical inverse has not been implemented for this WCS. ValueError Invalid argument values. Examples -------- >>> from astropy.utils.data import get_pkg_data_filename >>> from gwcs import NoConvergence >>> import asdf >>> import numpy as np >>> filename = get_pkg_data_filename('data/nircamwcs.asdf', package='gwcs.tests') >>> with asdf.open(filename, copy_arrays=True, lazy_load=False, ignore_missing_extensions=True) as af: ... w = af.tree['wcs'] >>> ra, dec = w([1,2,3], [1,1,1]) >>> assert np.allclose(ra, [5.927628, 5.92757069, 5.92751337]); >>> assert np.allclose(dec, [-72.01341247, -72.01341273, -72.013413]) >>> x, y = w.numerical_inverse(ra, dec) >>> assert np.allclose(x, [1.00000005, 2.00000005, 3.00000006]); >>> assert np.allclose(y, [1.00000004, 0.99999979, 1.00000015]); >>> x, y = w.numerical_inverse(ra, dec, maxiter=3, tolerance=1.0e-10, quiet=False) Traceback (most recent call last): ... gwcs.wcs.NoConvergence: 'WCS.numerical_inverse' failed to converge to the requested accuracy after 3 iterations. >>> w.numerical_inverse( ... *w([1, 300000, 3], [2, 1000000, 5], with_bounding_box=False), ... adaptive=False, ... detect_divergence=True, ... quiet=False, ... with_bounding_box=False ... ) Traceback (most recent call last): ... gwcs.wcs.NoConvergence: 'WCS.numerical_inverse' failed to converge to the requested accuracy. After 4 iterations, the solution is diverging at least for one input point. >>> # Now try to use some diverging data: >>> divra, divdec = w([1, 300000, 3], [2, 1000000, 5], with_bounding_box=False) >>> assert np.allclose(divra, [5.92762673, 148.21600848, 5.92750827]) >>> assert np.allclose(divdec, [-72.01339464, -7.80968079, -72.01334172]) >>> try: # doctest: +SKIP ... x, y = w.numerical_inverse(divra, divdec, maxiter=20, ... tolerance=1.0e-4, adaptive=True, ... detect_divergence=True, ... quiet=False) ... except NoConvergence as e: ... print(f"Indices of diverging points: {e.divergent}") ... print(f"Indices of poorly converging points: {e.slow_conv}") ... print(f"Best solution:\\n{e.best_solution}") ... print(f"Achieved accuracy:\\n{e.accuracy}") Indices of diverging points: None Indices of poorly converging points: [1] Best solution: [[1.00000040e+00 1.99999841e+00] [6.33507833e+17 3.40118820e+17] [3.00000038e+00 4.99999841e+00]] Achieved accuracy: [[2.75925982e-05 1.18471543e-05] [3.65405005e+04 1.31364188e+04] [2.76552923e-05 1.14789013e-05]] """ if not utils.isnumerical(args[0]): args = self.output_frame.coordinate_to_quantity(*args) if self.output_frame.naxes == 1: args = [args] args = utils.get_values(self.output_frame.unit, *args) args_shape = np.shape(args) nargs = args_shape[0] arg_dim = len(args_shape) - 1 if nargs != self.world_n_dim: raise ValueError("Number of input coordinates is different from " "the number of defined world coordinates in the " f"WCS ({self.world_n_dim:d})") if self.world_n_dim != self.pixel_n_dim: raise NotImplementedError( "Support for iterative inverse for transformations with " "different number of inputs and outputs was not implemented." ) # initial guess: if nargs == 2 and self._approx_inverse is None: self._calc_approx_inv(max_inv_pix_error=5, inv_degree=None) if self._approx_inverse is None: if self.bounding_box is None: x0 = np.ones(self.pixel_n_dim) else: x0 = np.mean(self.bounding_box, axis=-1) if arg_dim == 0: argsi = args if nargs == 2 and self._approx_inverse is not None: x0 = self._approx_inverse(*argsi) if not np.all(np.isfinite(x0)): return [np.array(np.nan) for _ in range(nargs)] result = tuple(self._vectorized_fixed_point( x0, argsi, tolerance=tolerance, maxiter=maxiter, adaptive=adaptive, detect_divergence=detect_divergence, quiet=quiet, with_bounding_box=with_bounding_box, fill_value=fill_value ).T.ravel().tolist()) else: arg_shape = args_shape[1:] nelem = np.prod(arg_shape) args = np.reshape(args, (nargs, nelem)) if self._approx_inverse is None: x0 = np.full((nelem, nargs), x0) else: x0 = np.array(self._approx_inverse(*args)).T result = self._vectorized_fixed_point( x0, args.T, tolerance=tolerance, maxiter=maxiter, adaptive=adaptive, detect_divergence=detect_divergence, quiet=quiet, with_bounding_box=with_bounding_box, fill_value=fill_value ).T result = tuple(np.reshape(result, args_shape)) 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 _vectorized_fixed_point(self, pix0, world, tolerance, maxiter, adaptive, detect_divergence, quiet, with_bounding_box, fill_value): # ############################################################ # # INITIALIZE ITERATIVE PROCESS: ## # ############################################################ # make a copy of the initial approximation pix0 = np.atleast_2d(np.array(pix0)) # 0-order solution pix = np.array(pix0) world0 = np.atleast_2d(np.array(world)) world = np.array(world0) # estimate pixel scale using approximate algorithm # from https://trs.jpl.nasa.gov/handle/2014/40409 if self.bounding_box is None: crpix = np.ones(self.pixel_n_dim) else: crpix = np.mean(self.bounding_box, axis=-1) l1, phi1 = np.deg2rad(self.__call__(*(crpix - 0.5))) l2, phi2 = np.deg2rad(self.__call__(*(crpix + [-0.5, 0.5]))) l3, phi3 = np.deg2rad(self.__call__(*(crpix + 0.5))) l4, phi4 = np.deg2rad(self.__call__(*(crpix + [0.5, -0.5]))) area = np.abs(0.5 * ((l4 - l2) * (np.sin(phi1) - np.sin(phi3)) + (l1 - l3) * (np.sin(phi2) - np.sin(phi4)))) inv_pscale = 1 / np.rad2deg(np.sqrt(area)) # form equation: def f(x): w = np.array(self.__call__(*(x.T), with_bounding_box=False)).T dw = np.mod(np.subtract(w, world) - 180.0, 360.0) - 180.0 return np.add(inv_pscale * dw, x) def froot(x): return np.mod(np.subtract(self.__call__(*x, with_bounding_box=False), worldi) - 180.0, 360.0) - 180.0 # compute correction: def correction(pix): p1 = f(pix) p2 = f(p1) d = p2 - 2.0 * p1 + pix idx = np.where(d != 0) corr = pix - p2 corr[idx] = np.square(p1[idx] - pix[idx]) / d[idx] return corr # initial iteration: dpix = correction(pix) # Update initial solution: pix -= dpix # Norm (L2) squared of the correction: dn = np.sum(dpix * dpix, axis=1) dnprev = dn.copy() # if adaptive else dn tol2 = tolerance**2 # Prepare for iterative process k = 1 ind = None inddiv = None # Turn off numpy runtime warnings for 'invalid' and 'over': old_invalid = np.geterr()['invalid'] old_over = np.geterr()['over'] np.seterr(invalid='ignore', over='ignore') # ############################################################ # # NON-ADAPTIVE ITERATIONS: ## # ############################################################ if not adaptive: # Fixed-point iterations: while (np.nanmax(dn) >= tol2 and k < maxiter): # Find correction to the previous solution: dpix = correction(pix) # Compute norm (L2) squared of the correction: dn = np.sum(dpix * dpix, axis=1) # Check for divergence (we do this in two stages # to optimize performance for the most common # scenario when successive approximations converge): if detect_divergence: divergent = (dn >= dnprev) if np.any(divergent): # Find solutions that have not yet converged: slowconv = (dn >= tol2) inddiv, = np.where(divergent & slowconv) if inddiv.shape[0] > 0: # Update indices of elements that # still need correction: conv = (dn < dnprev) iconv = np.where(conv) # Apply correction: dpixgood = dpix[iconv] pix[iconv] -= dpixgood dpix[iconv] = dpixgood # For the next iteration choose # non-divergent points that have not yet # converged to the requested accuracy: ind, = np.where(slowconv & conv) world = world[ind] dnprev[ind] = dn[ind] k += 1 # Switch to adaptive iterations: adaptive = True break # Save current correction magnitudes for later: dnprev = dn # Apply correction: pix -= dpix k += 1 # ############################################################ # # ADAPTIVE ITERATIONS: ## # ############################################################ if adaptive: if ind is None: ind, = np.where(np.isfinite(pix).all(axis=1)) world = world[ind] # "Adaptive" fixed-point iterations: while (ind.shape[0] > 0 and k < maxiter): # Find correction to the previous solution: dpixnew = correction(pix[ind]) # Compute norm (L2) of the correction: dnnew = np.sum(np.square(dpixnew), axis=1) # Bookkeeping of corrections: dnprev[ind] = dn[ind].copy() dn[ind] = dnnew if detect_divergence: # Find indices of pixels that are converging: conv = np.logical_or(dnnew < dnprev[ind], dnnew < tol2) if not np.all(conv): conv = np.ones_like(dnnew, dtype=bool) iconv = np.where(conv) iiconv = ind[iconv] # Apply correction: dpixgood = dpixnew[iconv] pix[iiconv] -= dpixgood dpix[iiconv] = dpixgood # Find indices of solutions that have not yet # converged to the requested accuracy # AND that do not diverge: subind, = np.where((dnnew >= tol2) & conv) else: # Apply correction: pix[ind] -= dpixnew dpix[ind] = dpixnew # Find indices of solutions that have not yet # converged to the requested accuracy: subind, = np.where(dnnew >= tol2) # Choose solutions that need more iterations: ind = ind[subind] world = world[subind] k += 1 # ############################################################ # # FINAL DETECTION OF INVALID, DIVERGING, ## # # AND FAILED-TO-CONVERGE POINTS ## # ############################################################ # Identify diverging and/or invalid points: invalid = ((~np.all(np.isfinite(pix), axis=1)) & (np.all(np.isfinite(world0), axis=1))) # When detect_divergence is False, dnprev is outdated # (it is the norm of the very first correction). # Still better than nothing... inddiv, = np.where(((dn >= tol2) & (dn >= dnprev)) | invalid) if inddiv.shape[0] == 0: inddiv = None # If there are divergent points, attempt to find a solution using # scipy's 'hybr' method: if detect_divergence and inddiv is not None and inddiv.size: bad = [] for idx in inddiv: worldi = world0[idx] result = optimize.root( froot, pix0[idx], method='hybr', tol=tolerance / (np.linalg.norm(pix0[idx]) + 1), options={'maxfev': 2 * maxiter} ) if result['success']: pix[idx, :] = result['x'] invalid[idx] = False else: bad.append(idx) if bad: inddiv = np.array(bad, dtype=int) else: inddiv = None # Identify points that did not converge within 'maxiter' # iterations: if k >= maxiter: ind, = np.where((dn >= tol2) & (dn < dnprev) & (~invalid)) if ind.shape[0] == 0: ind = None else: ind = None # Restore previous numpy error settings: np.seterr(invalid=old_invalid, over=old_over) # ############################################################ # # RAISE EXCEPTION IF DIVERGING OR TOO SLOWLY CONVERGING ## # # DATA POINTS HAVE BEEN DETECTED: ## # ############################################################ if (ind is not None or inddiv is not None) and not quiet: if inddiv is None: raise NoConvergence( "'WCS.numerical_inverse' failed to " "converge to the requested accuracy after {:d} " "iterations.".format(k), best_solution=pix, accuracy=np.abs(dpix), niter=k, slow_conv=ind, divergent=None) else: raise NoConvergence( "'WCS.numerical_inverse' failed to " "converge to the requested accuracy.\n" "After {:d} iterations, the solution is diverging " "at least for one input point." .format(k), best_solution=pix, accuracy=np.abs(dpix), niter=k, slow_conv=ind, divergent=inddiv) if with_bounding_box and self.bounding_box is not None: # find points outside the bounding box and replace their values # with fill_value valid = np.logical_not(invalid) in_bb = np.ones_like(invalid, dtype=np.bool_) for c, (x1, x2) in zip(pix[valid].T, self.bounding_box): in_bb[valid] &= (c >= x1) & (c <= x2) pix[np.logical_not(in_bb)] = fill_value return pix 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.coordinate_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.coordinates.SpectralCoord` 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] return [step.frame if isinstance(step.frame, str) else step.frame.name for step 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_frames.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: current_transform = self._pipeline[frame_ind - 1].transform self._pipeline[frame_ind - 1].transform = current_transform | transform else: current_transform = self._pipeline[frame_ind].transform self._pipeline[frame_ind].transform = transform | current_transform def insert_frame(self, input_frame, transform, output_frame): """ Insert a new frame into an existing pipeline. This frame must be anchored to a frame already in the pipeline by a transform. This existing frame is identified solely by its name, although an entire `~gwcs.coordinate_frames.CoordinateFrame` can be passed (e.g., the `input_frame` or `output_frame` attribute). This frame is never modified. Parameters ---------- input_frame : str or `~gwcs.coordinate_frames.CoordinateFrame` Coordinate frame at start of new transform transform : `~astropy.modeling.Model` New transform to be inserted in the pipeline output_frame: str or `~gwcs.coordinate_frames.CoordinateFrame` Coordinate frame at end of new transform """ input_name, input_frame_obj = self._get_frame_name(input_frame) output_name, output_frame_obj = self._get_frame_name(output_frame) try: input_index = self._get_frame_index(input_frame) except CoordinateFrameError: input_index = None if input_frame_obj is None: raise ValueError(f"New coordinate frame {input_name} must " "be defined") try: output_index = self._get_frame_index(output_frame) except CoordinateFrameError: output_index = None if output_frame_obj is None: raise ValueError(f"New coordinate frame {output_name} must " "be defined") new_frames = [input_index, output_index].count(None) if new_frames == 0: raise ValueError("Could not insert frame as both frames " f"{input_name} and {output_name} already exist") elif new_frames == 2: raise ValueError("Could not insert frame as neither frame " f"{input_name} nor {output_name} exists") if input_index is None: self._pipeline = (self._pipeline[:output_index] + [Step(input_frame_obj, transform)] + self._pipeline[output_index:]) super(WCS, self).__setattr__(input_name, input_frame_obj) else: split_step = self._pipeline[input_index] self._pipeline = (self._pipeline[:input_index] + [Step(split_step.frame, transform), Step(output_frame_obj, split_step.transform)] + self._pipeline[input_index + 1:]) super(WCS, self).__setattr__(output_name, output_frame_obj) @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 return self._pipeline[-1].frame.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].frame 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].frame 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 return 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. if isinstance(value, CompoundBoundingBox): bbox = CompoundBoundingBox.validate(transform_0, value, order='F') else: bbox = Bbox.validate(transform_0, value, order='F') except Exception: raise transform_0.bounding_box = bbox self.set_transform(frames[0], frames[1], transform_0) def attach_compound_bounding_box(self, cbbox, selector_args): frames = self.available_frames transform_0 = self.get_transform(frames[0], frames[1]) self.bounding_box = CompoundBoundingBox.validate(transform_0, cbbox, selector_args=selector_args, order='F') 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 = [step.frame for step in self._pipeline] col2 = [] for item in self._pipeline[: -1]: model = item.transform if model is None: col2.append(None) elif 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 = utils._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") """ 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) def to_fits_sip(self, bounding_box=None, max_pix_error=0.25, degree=None, max_inv_pix_error=0.25, inv_degree=None, npoints=32, crpix=None, projection='TAN', verbose=False): """ Construct a SIP-based approximation to the WCS for the axes corresponding to the `~gwcs.coordinate_frames.CelestialFrame` in the form of a FITS header. The default mode in using this attempts to achieve roughly 0.25 pixel accuracy over the whole image. Parameters ---------- bounding_box : tuple, optional A pair of tuples, each consisting of two numbers Represents the range of pixel values in both dimensions ((xmin, xmax), (ymin, ymax)) max_pix_error : float, optional Maximum allowed error over the domain of the pixel array. This error is the equivalent pixel error that corresponds to the maximum error in the output coordinate resulting from the fit based on a nominal plate scale. Ignored when ``degree`` is an integer or a list with a single degree. degree : int, iterable, None, optional Degree of the SIP polynomial. Default value `None` indicates that all allowed degree values (``[1...9]``) will be considered and the lowest degree that meets accuracy requerements set by ``max_pix_error`` will be returned. Alternatively, ``degree`` can be an iterable containing allowed values for the SIP polynomial degree. This option is similar to default `None` but it allows caller to restrict the range of allowed SIP degrees used for fitting. Finally, ``degree`` can be an integer indicating the exact SIP degree to be fit to the WCS transformation. In this case ``max_pixel_error`` is ignored. max_inv_pix_error : float, optional Maximum allowed inverse error over the domain of the pixel array in pixel units. If None, no inverse is generated. Ignored when ``degree`` is an integer or a list with a single degree. inv_degree : int, iterable, None, optional Degree of the SIP polynomial. Default value `None` indicates that all allowed degree values (``[1...9]``) will be considered and the lowest degree that meets accuracy requerements set by ``max_pix_error`` will be returned. Alternatively, ``degree`` can be an iterable containing allowed values for the SIP polynomial degree. This option is similar to default `None` but it allows caller to restrict the range of allowed SIP degrees used for fitting. Finally, ``degree`` can be an integer indicating the exact SIP degree to be fit to the WCS transformation. In this case ``max_inv_pixel_error`` is ignored. npoints : int, optional The number of points in each dimension to sample the bounding box for use in the SIP fit. Minimum number of points is 3. crpix : list of float, None, optional Coordinates (1-based) of the reference point for the new FITS WCS. When not provided, i.e., when set to `None` (default) the reference pixel will be chosen near the center of the bounding box for axes corresponding to the celestial frame. projection : str, `~astropy.modeling.projections.Pix2SkyProjection`, optional Projection to be used for the created FITS WCS. It can be specified as a string of three characters specifying a FITS projection code from Table 13 in `Representations of World Coordinates in FITS \ `_ (Paper I), Greisen, E. W., and Calabretta, M. R., A & A, 395, 1061-1075, 2002. Alternatively, it can be an instance of one of the `astropy's Pix2Sky_* `_ projection models inherited from :py:class:`~astropy.modeling.projections.Pix2SkyProjection`. verbose : bool, optional Print progress of fits. Returns ------- FITS header with all SIP WCS keywords Raises ------ ValueError If the WCS is not at least 2D, an exception will be raised. If the specified accuracy (both forward and inverse, both rms and maximum) is not achieved an exception will be raised. Notes ----- Use of this requires a judicious choice of required accuracies. Attempts to use higher degrees (~7 or higher) will typically fail due to floating point problems that arise with high powers. """ _, _, celestial_group = self._separable_groups(detect_celestial=True) if celestial_group is None: raise ValueError("The to_fits_sip requires an output celestial frame.") hdr = self._to_fits_sip( celestial_group=celestial_group, keep_axis_position=False, bounding_box=bounding_box, max_pix_error=max_pix_error, degree=degree, max_inv_pix_error=max_inv_pix_error, inv_degree=inv_degree, npoints=npoints, crpix=crpix, projection=projection, matrix_type='CD', verbose=verbose ) return hdr def _to_fits_sip(self, celestial_group, keep_axis_position, bounding_box, max_pix_error, degree, max_inv_pix_error, inv_degree, npoints, crpix, projection, matrix_type, verbose): r""" Construct a SIP-based approximation to the WCS for the axes corresponding to the `~gwcs.coordinate_frames.CelestialFrame` in the form of a FITS header. The default mode in using this attempts to achieve roughly 0.25 pixel accuracy over the whole image. Below we describe only parameters additional to the ones explained for `to_fits_sip`. Other Parameters ---------------- frame : gwcs.coordinate_frames.CelestialFrame A celestial frame. celestial_group : list of ``_WorldAxisInfo`` A group of two celestial axes to be represented using standard image FITS WCS and maybe ``-SIP`` polynomials. keep_axis_position : bool This parameter controls whether to keep/preserve output axes indices in this WCS object when creating FITS WCS and create a FITS header with ``CTYPE`` axes indices preserved from the ``frame`` object or whether to reset the indices of output celestial axes to 1 and 2 with ``CTYPE1``, ``CTYPE2``. Default is `False`. .. warning:: Returned header will have both ``NAXIS`` and ``WCSAXES`` set to 2. If ``max(axes_mapping) > 2`` this will lead to an invalid WCS. It is caller's responsibility to adjust NAXIS to a valid value. .. note:: The ``lon``/``lat`` order is still preserved regardless of this setting. matrix_type : {'CD', 'PC-CDELT1', 'PC-SUM1', 'PC-DET1', 'PC-SCALE'} Specifies formalism (``PC`` or ``CD``) to be used for the linear transformation matrix and normalization for the ``PC`` matrix *when non-linear polynomial terms are not required to achieve requested accuracy*. .. note:: ``CD`` matrix is always used when requested SIP approximation accuracy requires non-linear terms (when ``CTYPE`` ends in ``-SIP``). This parameter is ignored when non-linear polynomial terms are used. - ``'CD'``: use ``CD`` matrix; - ``'PC-CDELT1'``: set ``PC=CD`` and ``CDELTi=1``. This is the behavior of `~astropy.wcs.WCS.to_header` method; - ``'PC-SUM1'``: normalize ``PC`` matrix such that sum of its squared elements is 1: :math:`\Sigma PC_{ij}^2=1`; - ``'PC-DET1'``: normalize ``PC`` matrix such that :math:`|\det(PC)|=1`; - ``'PC-SCALE'``: normalize ``PC`` matrix such that ``CDELTi`` are estimates of the linear pixel scales. Returns ------- FITS header with all SIP WCS keywords Raises ------ ValueError If the WCS is not at least 2D, an exception will be raised. If the specified accuracy (both forward and inverse, both rms and maximum) is not achieved an exception will be raised. """ if isinstance(matrix_type, str): matrix_type = matrix_type.upper() if matrix_type not in ['CD', 'PC-CDELT1', 'PC-SUM1', 'PC-DET1', 'PC-SCALE']: raise ValueError(f"Unsupported 'matrix_type' value: {repr(matrix_type)}.") if npoints < 8: raise ValueError("Number of sampling points is too small. 'npoints' must be >= 8.") if isinstance(projection, str): projection = projection.upper() try: sky2pix_proj = getattr(projections, f'Sky2Pix_{projection}')(name=projection) except AttributeError: raise ValueError("Unsupported FITS WCS sky projection: {projection}") elif isinstance(projection, projections.Sky2PixProjection): sky2pix_proj = projection projection = projection.name if not projection or not isinstance(projection, str) or len(projection) != 3: raise ValueError("Unsupported FITS WCS sky projection: {sky2pix_proj}") try: getattr(projections, f'Sky2Pix_{projection}')() except AttributeError: raise ValueError("Unsupported FITS WCS sky projection: {projection}") else: raise TypeError( "'projection' must be either a FITS WCS string projection code " "or an instance of astropy.modeling.projections.Pix2SkyProjection.") frame = celestial_group[0].frame lon_axis = frame.axes_order[0] lat_axis = frame.axes_order[1] # identify input axes: input_axes = [] for wax in celestial_group: input_axes.extend(wax.input_axes) input_axes = sorted(set(input_axes)) if len(input_axes) != 2: raise ValueError("Only CelestialFrame that correspond to two " "input axes are supported.") # Axis number for FITS axes. # iax? - image axes; nlon, nlat - celestial axes: if keep_axis_position: nlon = lon_axis + 1 nlat = lat_axis + 1 iax1, iax2 = (i + 1 for i in input_axes) else: nlon, nlat = (1, 2) if lon_axis < lat_axis else (2, 1) iax1 = 1 iax2 = 2 # Determine reference points. if bounding_box is None and self.bounding_box is None: raise ValueError("A bounding_box is needed to proceed.") if bounding_box is None: bounding_box = self.bounding_box bb_center = np.mean(bounding_box, axis=1) fixi_dict = { k: bb_center[k] for k in set(range(self.pixel_n_dim)).difference(input_axes) } # transform = fix_inputs(self.forward_transform, fixi_dict) # This is a workaround to the bug in https://github.com/astropy/astropy/issues/11360 # Once that bug is fixed, the code below can be replaced with fix_inputs # statement commented out immediately above. transform = _fix_transform_inputs(self.forward_transform, fixi_dict) transform = transform | Mapping((lon_axis, lat_axis), n_inputs=self.forward_transform.n_outputs) (xmin, xmax) = bounding_box[input_axes[0]] (ymin, ymax) = bounding_box[input_axes[1]] # 0-based crpix: if crpix is None: crpix1 = round(bb_center[input_axes[0]], 1) crpix2 = round(bb_center[input_axes[1]], 1) else: crpix1 = crpix[0] - 1 crpix2 = crpix[1] - 1 # check that the bounding box has some reasonable size: if (xmax - xmin) < 1 or (ymax - ymin) < 1: raise ValueError("Bounding box is too small for fitting a SIP polynomial") lon, lat = transform(crpix1, crpix2) # Now rotate to native system and deproject. Recall that transform # expects pixels in the original coordinate system, but the SIP # transform is relative to crpix coordinates, thus the initial shift. ntransform = ((Shift(crpix1) & Shift(crpix2)) | transform | RotateCelestial2Native(lon, lat, 180) | sky2pix_proj) # standard sampling: u, v = _make_sampling_grid( npoints, tuple(bounding_box[k] for k in input_axes), crpix=[crpix1, crpix2] ) undist_x, undist_y = ntransform(u, v) # Double sampling to check if sampling is sufficient. ud, vd = _make_sampling_grid( 2 * npoints, tuple(bounding_box[k] for k in input_axes), crpix=[crpix1, crpix2] ) undist_xd, undist_yd = ntransform(ud, vd) # Determine approximate pixel scale in order to compute error threshold # from the specified pixel error. Computed at the center of the array. x0, y0 = ntransform(0, 0) xx, xy = ntransform(1, 0) yx, yy = ntransform(0, 1) pixarea = np.abs((xx - x0) * (yy - y0) - (xy - y0) * (yx - x0)) plate_scale = np.sqrt(pixarea) # The fitting section. if verbose: print("\nFitting forward SIP ...") fit_poly_x, fit_poly_y, max_resid = _fit_2D_poly( degree, max_pix_error, plate_scale, u, v, undist_x, undist_y, ud, vd, undist_xd, undist_yd, verbose=verbose ) # The following is necessary to put the fit into the SIP formalism. cdmat, sip_poly_x, sip_poly_y = _reform_poly_coefficients(fit_poly_x, fit_poly_y) # cdmat = np.array([[fit_poly_x.c1_0.value, fit_poly_x.c0_1.value], # [fit_poly_y.c1_0.value, fit_poly_y.c0_1.value]]) det = cdmat[0][0] * cdmat[1][1] - cdmat[0][1] * cdmat[1][0] U = ( cdmat[1][1] * undist_x - cdmat[0][1] * undist_y) / det V = (-cdmat[1][0] * undist_x + cdmat[0][0] * undist_y) / det detd = cdmat[0][0] * cdmat[1][1] - cdmat[0][1] * cdmat[1][0] Ud = ( cdmat[1][1] * undist_xd - cdmat[0][1] * undist_yd) / detd Vd = (-cdmat[1][0] * undist_xd + cdmat[0][0] * undist_yd) / detd if max_inv_pix_error: if verbose: print("\nFitting inverse SIP ...") fit_inv_poly_u, fit_inv_poly_v, max_inv_resid = _fit_2D_poly( inv_degree, max_inv_pix_error, 1, U, V, u-U, v-V, Ud, Vd, ud-Ud, vd-Vd, verbose=verbose ) # create header with WCS info: w = celestial_frame_to_wcs(frame.reference_frame, projection=projection) w.wcs.crval = [lon, lat] w.wcs.crpix = [crpix1 + 1, crpix2 + 1] w.wcs.pc = cdmat if nlon < nlat else cdmat[::-1] w.wcs.set() hdr = w.to_header(True) # data array info: hdr.insert(0, ('NAXIS', 2, 'number of array dimensions')) hdr.insert(1, (f'NAXIS{iax1:d}', int(xmax) + 1)) hdr.insert(2, (f'NAXIS{iax2:d}', int(ymax) + 1)) assert len(hdr['NAXIS*']) == 3 # list of celestial axes related keywords: cel_kwd = ['CRVAL', 'CTYPE', 'CUNIT'] # Add SIP info: if fit_poly_x.degree > 1: mat_kind = 'CD' # CDELT is not used with CD matrix (PC->CD later): del hdr['CDELT?'] hdr['CTYPE1'] = hdr['CTYPE1'].strip() + '-SIP' hdr['CTYPE2'] = hdr['CTYPE2'].strip() + '-SIP' hdr['A_ORDER'] = fit_poly_x.degree hdr['B_ORDER'] = fit_poly_x.degree _store_2D_coefficients(hdr, sip_poly_x, 'A') _store_2D_coefficients(hdr, sip_poly_y, 'B') hdr['sipmxerr'] = (max_resid, 'Max diff from GWCS (equiv pix).') if max_inv_pix_error: hdr['AP_ORDER'] = fit_inv_poly_u.degree hdr['BP_ORDER'] = fit_inv_poly_u.degree _store_2D_coefficients(hdr, fit_inv_poly_u, 'AP', keeplinear=True) _store_2D_coefficients(hdr, fit_inv_poly_v, 'BP', keeplinear=True) hdr['sipiverr'] = (max_inv_resid, 'Max diff for inverse (pixels)') else: if matrix_type.startswith('PC'): mat_kind = 'PC' cel_kwd.append('CDELT') if matrix_type == 'PC-CDELT1': cdelt = [1.0, 1.0] elif matrix_type == 'PC-SUM1': norm = np.sqrt(np.sum(w.wcs.pc**2)) cdelt = [norm, norm] elif matrix_type == 'PC-DET1': det_pc = np.linalg.det(w.wcs.pc) norm = np.sqrt(np.abs(det_pc)) cdelt = [norm, np.sign(det_pc) * norm] elif matrix_type == 'PC-SCALE': cdelt = proj_plane_pixel_scales(w) for i in range(1, 3): s = cdelt[i - 1] hdr[f'CDELT{i}'] = s for j in range(1, 3): pc_kwd = f'PC{i}_{j}' if pc_kwd in hdr: hdr[pc_kwd] = w.wcs.pc[i - 1, j - 1] / s else: mat_kind = 'CD' del hdr['CDELT?'] hdr['sipmxerr'] = (max_resid, 'Max diff from GWCS (equiv pix).') # Construct CD matrix while remapping input axes. # We do not update comments to typical comments for CD matrix elements # (such as 'partial of second axis coordinate w.r.t. y'), because # when input frame has number of axes > 2, then imaging # axes arbitrary. old_nlon, old_nlat = (1, 2) if nlon < nlat else (2, 1) # Remap input axes (CRPIX) and output axes-related parameters # (CRVAL, CUNIT, CTYPE, CD/PC). This has to be done in two steps to avoid # name conflicts (i.e., swapping CRPIX1<->CRPIX2). # remap input axes: axis_rename = {} if iax1 != 1: axis_rename['CRPIX1'] = f'CRPIX{iax1}' if iax2 != 2: axis_rename['CRPIX2'] = f'CRPIX{iax2}' # CP/PC matrix: axis_rename[f'PC{old_nlon}_1'] = f'{mat_kind}{nlon}_{iax1}' axis_rename[f'PC{old_nlon}_2'] = f'{mat_kind}{nlon}_{iax2}' axis_rename[f'PC{old_nlat}_1'] = f'{mat_kind}{nlat}_{iax1}' axis_rename[f'PC{old_nlat}_2'] = f'{mat_kind}{nlat}_{iax2}' # remap celestial axes keywords: for kwd in cel_kwd: for iold, inew in [(1, nlon), (2, nlat)]: if iold != inew: axis_rename[f'{kwd:s}{iold:d}'] = f'{kwd:s}{inew:d}' # construct new header cards with remapped axes: new_cards = [] for c in hdr.cards: if c[0] in axis_rename: c = fits.Card(keyword=axis_rename[c.keyword], value=c.value, comment=c.comment) new_cards.append(c) hdr = fits.Header(new_cards) hdr['WCSAXES'] = 2 hdr.insert('WCSAXES', ('WCSNAME', f'{self.output_frame.name}'), after=True) # for PC matrix formalism, set diagonal elements to 0 if necessary # (by default, in PC formalism, diagonal matrix elements by default # are 0): if mat_kind == 'PC': if nlon not in [iax1, iax2]: hdr.insert( f'{mat_kind}{nlon}_{iax2}', (f'{mat_kind}{nlon}_{nlon}', 0.0, 'Coordinate transformation matrix element') ) if nlat not in [iax1, iax2]: hdr.insert( f'{mat_kind}{nlat}_{iax2}', (f'{mat_kind}{nlat}_{nlat}', 0.0, 'Coordinate transformation matrix element') ) return hdr def _separable_groups(self, detect_celestial): """ This method finds sets (groups) of separable axes - axes that are dependent on other axes within a set/group but do not depend on axes from other groups. In other words, axes from different groups are separable. Parameters ---------- detect_celestial : bool If `True`, will return, as the third return value, the group of celestial axes separately from all other (groups of) axes. If no celestial frame is detected, then return value for the celestial axes group will be set to `None`. Returns ------- axes_groups : list of lists of ``_WorldAxisInfo`` Each inner list represents a group of non-separable (among themselves) axes and each axis in a group is independent of axes in *other* groups. Each axis in a group is represented through the `_WorldAxisInfo` class used to store relevant information about an axis. When ``detect_celestial`` is set to `True`, celestial axes group is not included in this list. world_axes : list of ``_WorldAxisInfo`` A flattened version of ``axes_groups``. Even though it is not difficult to flatten ``axes_groups``, this list is a by-product of other checks and returned here for efficiency. When ``detect_celestial`` is set to `True`, celestial axes group is not included in this list. celestial_group : list of ``_WorldAxisInfo`` A group of two celestial axes. This group is returned *only when* ``detect_celestial`` is set to `True`. """ def find_frame(axis_number): for frame in frames: if axis_number in frame.axes_order: return frame else: raise RuntimeError("Encountered an output axes that does not " "belong to any output coordinate frames.") # use correlation matrix to find separable axes: corr_mat = self.axis_correlation_matrix axes_sets = [set(np.flatnonzero(r)) for r in corr_mat.T] k = 0 while len(axes_sets) - 1 > k: for m in range(len(axes_sets) - 1, k, -1): if axes_sets[k].isdisjoint(axes_sets[m]): continue axes_sets[k] = axes_sets[k].union(axes_sets[m]) del axes_sets[m] k += 1 # create a mapping of output axes to input/image axes groups: mapping = {k: tuple(np.flatnonzero(r)) for k, r in enumerate(corr_mat)} axes_groups = [] world_axes = [] # flattened version of axes_groups input_axes = [] # all input axes if isinstance(self.output_frame, cf.CompositeFrame): frames = self.output_frame.frames else: frames = [self.output_frame] celestial_group = None # identify which separable group of axes belong for s in axes_sets: axis_info_group = [] # group of separable output axes info # Find the frame to which the first axis in the group belongs. # Most likely this frame will be the frame of all other axes in # this group; if not, we will update it later. s = sorted(s) frame = find_frame(s[0]) celestial = (detect_celestial and len(s) == 2 and len(frame.axes_order) == 2 and isinstance(frame, cf.CelestialFrame)) for axno in s: if axno not in frame.axes_order: frame = find_frame(axno) celestial = False # Celestial axes must belong to the same frame # index of the axis in this frame's fidx = frame.axes_order.index(axno) if hasattr(frame.unit[fidx], 'get_format_name'): cunit = frame.unit[fidx].get_format_name(u.format.Fits).upper() else: cunit = '' axis_info = _WorldAxisInfo( axis=axno, frame=frame, world_axis_order=self.output_frame.axes_order.index(axno), cunit=cunit, ctype=cf.get_ctype_from_ucd(self.world_axis_physical_types[axno]), input_axes=mapping[axno] ) axis_info_group.append(axis_info) input_axes.extend(mapping[axno]) world_axes.extend(axis_info_group) if celestial: celestial_group = axis_info_group else: axes_groups.append(axis_info_group) # sanity check: input_axes = set(sum((ax.input_axes for ax in world_axes), world_axes[0].input_axes.__class__())) n_inputs = len(input_axes) if (n_inputs != self.pixel_n_dim or max(input_axes) + 1 != n_inputs or min(input_axes) < 0): raise ValueError("Input axes indices are inconsistent with the " "forward transformation.") if detect_celestial: return axes_groups, world_axes, celestial_group else: return axes_groups, world_axes def to_fits_tab(self, bounding_box=None, bin_ext_name='WCS-TABLE', coord_col_name='coordinates', sampling=1): """ Construct a FITS WCS ``-TAB``-based approximation to the WCS in the form of a FITS header and a binary table extension. For the description of the FITS WCS ``-TAB`` convention, see "Representations of spectral coordinates in FITS" in `Greisen, E. W. et al. A&A 446 (2) 747-771 (2006) `_ . Parameters ---------- bounding_box : tuple, optional Specifies the range of acceptable values for each input axis. The order of the axes is `~gwcs.coordinate_frames.CoordinateFrame.axes_order`. For two image axes ``bounding_box`` is of the form ``((xmin, xmax), (ymin, ymax))``. bin_ext_name : str, optional Extension name for the `~astropy.io.fits.BinTableHDU` HDU for those axes groups that will be converted using FITW WCS' ``-TAB`` algorith. Extension version will be determined automatically based on the number of separable group of axes. coord_col_name : str, optional Field name of the coordinate array in the structured array stored in `~astropy.io.fits.BinTableHDU` data. This corresponds to ``TTYPEi`` field in the FITS header of the binary table extension. sampling : float, tuple, optional The target "density" of grid nodes per pixel to be used when creating the coordinate array for the ``-TAB`` FITS WCS convention. It is equal to ``1/step`` where ``step`` is the distance between grid nodes in pixels. ``sampling`` can be specified as a single number to be used for all axes or as a `tuple` of numbers that specify the sampling for each image axis. Returns ------- hdr : `~astropy.io.fits.Header` Header with WCS-TAB information associated (to be used) with image data. bin_table_hdu : `~astropy.io.fits.BinTableHDU` Binary table extension containing the coordinate array. Raises ------ ValueError When ``bounding_box`` is not defined either through the input ``bounding_box`` parameter or this object's ``bounding_box`` property. ValueError When ``sampling`` is a `tuple` of length larger than 1 that does not match the number of image axes. RuntimeError If the number of image axes (``~gwcs.WCS.pixel_n_dim``) is larger than the number of world axes (``~gwcs.WCS.world_n_dim``). """ if bounding_box is None: if self.bounding_box is None: raise ValueError( "Need a valid bounding_box to compute the footprint." ) bounding_box = self.bounding_box else: # validate user-supplied bounding box: frames = self.available_frames transform_0 = self.get_transform(frames[0], frames[1]) Bbox.validate(transform_0, bounding_box) if self.forward_transform.n_inputs == 1: bounding_box = [bounding_box] if self.pixel_n_dim > self.world_n_dim: raise RuntimeError( "The case when the number of input axes is larger than the " "number of output axes is not supported." ) try: sampling = np.broadcast_to(sampling, (self.pixel_n_dim, )) except ValueError: raise ValueError("Number of sampling values either must be 1 " "or it must match the number of pixel axes.") _, world_axes = self._separable_groups(detect_celestial=False) hdr, bin_table_hdu = self._to_fits_tab( hdr=None, world_axes_group=world_axes, use_cd=False, bounding_box=bounding_box, bin_ext=bin_ext_name, coord_col_name=coord_col_name, sampling=sampling ) return hdr, bin_table_hdu def to_fits(self, bounding_box=None, max_pix_error=0.25, degree=None, max_inv_pix_error=0.25, inv_degree=None, npoints=32, crpix=None, projection='TAN', bin_ext_name='WCS-TABLE', coord_col_name='coordinates', sampling=1, verbose=False): """ Construct a FITS WCS ``-TAB``-based approximation to the WCS in the form of a FITS header and a binary table extension. For the description of the FITS WCS ``-TAB`` convention, see "Representations of spectral coordinates in FITS" in `Greisen, E. W. et al. A&A 446 (2) 747-771 (2006) `_ . If WCS contains celestial frame, PC/CD formalism will be used for the celestial axes. .. note:: SIP distortion fitting requires that the WCS object has only two celestial axes. When WCS does not contain celestial axes, SIP fitting parameters (``max_pix_error``, ``degree``, ``max_inv_pix_error``, ``inv_degree``, and ``projection``) are ignored. When a WCS, in addition to celestial frame, contains other types of axes, SIP distortion fitting is disabled (ony linear terms are fitted for celestial frame). Parameters ---------- bounding_box : tuple, optional Specifies the range of acceptable values for each input axis. The order of the axes is `~gwcs.coordinate_frames.CoordinateFrame.axes_order`. For two image axes ``bounding_box`` is of the form ``((xmin, xmax), (ymin, ymax))``. max_pix_error : float, optional Maximum allowed error over the domain of the pixel array. This error is the equivalent pixel error that corresponds to the maximum error in the output coordinate resulting from the fit based on a nominal plate scale. degree : int, iterable, None, optional Degree of the SIP polynomial. Default value `None` indicates that all allowed degree values (``[1...9]``) will be considered and the lowest degree that meets accuracy requerements set by ``max_pix_error`` will be returned. Alternatively, ``degree`` can be an iterable containing allowed values for the SIP polynomial degree. This option is similar to default `None` but it allows caller to restrict the range of allowed SIP degrees used for fitting. Finally, ``degree`` can be an integer indicating the exact SIP degree to be fit to the WCS transformation. In this case ``max_pixel_error`` is ignored. .. note:: When WCS object has When ``degree`` is `None` and the WCS object has max_inv_pix_error : float, optional Maximum allowed inverse error over the domain of the pixel array in pixel units. If None, no inverse is generated. inv_degree : int, iterable, None, optional Degree of the SIP polynomial. Default value `None` indicates that all allowed degree values (``[1...9]``) will be considered and the lowest degree that meets accuracy requerements set by ``max_pix_error`` will be returned. Alternatively, ``degree`` can be an iterable containing allowed values for the SIP polynomial degree. This option is similar to default `None` but it allows caller to restrict the range of allowed SIP degrees used for fitting. Finally, ``degree`` can be an integer indicating the exact SIP degree to be fit to the WCS transformation. In this case ``max_inv_pixel_error`` is ignored. npoints : int, optional The number of points in each dimension to sample the bounding box for use in the SIP fit. Minimum number of points is 3. crpix : list of float, None, optional Coordinates (1-based) of the reference point for the new FITS WCS. When not provided, i.e., when set to `None` (default) the reference pixel will be chosen near the center of the bounding box for axes corresponding to the celestial frame. projection : str, `~astropy.modeling.projections.Pix2SkyProjection`, optional Projection to be used for the created FITS WCS. It can be specified as a string of three characters specifying a FITS projection code from Table 13 in `Representations of World Coordinates in FITS \ `_ (Paper I), Greisen, E. W., and Calabretta, M. R., A & A, 395, 1061-1075, 2002. Alternatively, it can be an instance of one of the `astropy's Pix2Sky_* `_ projection models inherited from :py:class:`~astropy.modeling.projections.Pix2SkyProjection`. bin_ext_name : str, optional Extension name for the `~astropy.io.fits.BinTableHDU` HDU for those axes groups that will be converted using FITW WCS' ``-TAB`` algorith. Extension version will be determined automatically based on the number of separable group of axes. coord_col_name : str, optional Field name of the coordinate array in the structured array stored in `~astropy.io.fits.BinTableHDU` data. This corresponds to ``TTYPEi`` field in the FITS header of the binary table extension. sampling : float, tuple, optional The target "density" of grid nodes per pixel to be used when creating the coordinate array for the ``-TAB`` FITS WCS convention. It is equal to ``1/step`` where ``step`` is the distance between grid nodes in pixels. ``sampling`` can be specified as a single number to be used for all axes or as a `tuple` of numbers that specify the sampling for each image axis. verbose : bool, optional Print progress of fits. Returns ------- hdr : `~astropy.io.fits.Header` Header with WCS-TAB information associated (to be used) with image data. hdulist : a list of `~astropy.io.fits.BinTableHDU` A Python list of binary table extensions containing the coordinate array for TAB extensions; one extension per separable axes group. Raises ------ ValueError When ``bounding_box`` is not defined either through the input ``bounding_box`` parameter or this object's ``bounding_box`` property. ValueError When ``sampling`` is a `tuple` of length larger than 1 that does not match the number of image axes. RuntimeError If the number of image axes (``~gwcs.WCS.pixel_n_dim``) is larger than the number of world axes (``~gwcs.WCS.world_n_dim``). """ if bounding_box is None: if self.bounding_box is None: raise ValueError( "Need a valid bounding_box to compute the footprint." ) bounding_box = self.bounding_box else: # validate user-supplied bounding box: frames = self.available_frames transform_0 = self.get_transform(frames[0], frames[1]) Bbox.validate(transform_0, bounding_box) if self.forward_transform.n_inputs == 1: bounding_box = [bounding_box] if self.pixel_n_dim > self.world_n_dim: raise RuntimeError( "The case when the number of input axes is larger than the " "number of output axes is not supported." ) try: sampling = np.broadcast_to(sampling, (self.pixel_n_dim, )) except ValueError: raise ValueError("Number of sampling values either must be 1 " "or it must match the number of pixel axes.") world_axes_groups, _, celestial_group = self._separable_groups( detect_celestial=True ) # Find celestial axes group and treat it separately from other axes: if celestial_group: # if world_axes_groups is empty, then we have only celestial axes # and so we can allow arbitrary degree for SIP. When there are # other axes types present, issue a warning and set 'degree' to 1 # because use of SIP when world_n_dim > 2 currently is not supported by # astropy.wcs.WCS - see https://github.com/astropy/astropy/pull/11452 if world_axes_groups and (degree is None or np.max(degree) != 2): if degree is not None: warnings.warn( "SIP distortion is not supported when the number\n" "of axes in WCS is larger than 2. Setting 'degree'\n" "to 1 and 'max_inv_pix_error' to None." ) degree = 1 max_inv_pix_error = None hdr = self._to_fits_sip( celestial_group=celestial_group, keep_axis_position=True, bounding_box=bounding_box, max_pix_error=max_pix_error, degree=degree, max_inv_pix_error=max_inv_pix_error, inv_degree=inv_degree, npoints=npoints, crpix=crpix, projection=projection, matrix_type='PC-CDELT1', verbose=verbose ) use_cd = 'A_ORDER' in hdr else: use_cd = False hdr = fits.Header() hdr['NAXIS'] = 0 hdr['WCSAXES'] = 0 # now handle non-celestial axes using -TAB convention for each # separable axes group: hdulist = [] for extver0, world_axes_group in enumerate(world_axes_groups): # For each subset of separable axes call _to_fits_tab to # convert that group to a single Bin TableHDU with a # coordinate array for this group of axes: hdr, bin_table_hdu = self._to_fits_tab( hdr=hdr, world_axes_group=world_axes_group, use_cd=use_cd, bounding_box=bounding_box, bin_ext=(bin_ext_name, extver0 + 1), coord_col_name=coord_col_name, sampling=sampling ) hdulist.append(bin_table_hdu) hdr.add_comment('FITS WCS created by approximating a gWCS') return hdr, hdulist def _to_fits_tab(self, hdr, world_axes_group, use_cd, bounding_box, bin_ext, coord_col_name, sampling): """ Construct a FITS WCS ``-TAB``-based approximation to the WCS in the form of a FITS header and a binary table extension. For the description of the FITS WCS ``-TAB`` convention, see "Representations of spectral coordinates in FITS" in `Greisen, E. W. et al. A&A 446 (2) 747-771 (2006) `_ . Below we describe only parameters additional to the ones explained for `to_fits_tab`. .. warn:: For this helper function, parameters ``bounding_box`` and ``sampling`` (when provided as a tuple) are expected to have the same length as the number of input axes in the *full* WCS object. That is, the number of elements in ``bounding_box`` and ``sampling`` is not be affected by ``ignore_axes``. Other Parameters ---------------- hdr : astropy.io.fits.Header, None The first time this function is called, ``hdr`` should be set to `None` or be an empty :py:class:`~astropy.io.fits.Header` object. On subsequent calls, updated header from the previous iteration should be provided. world_axes_group : tuple of dict A list of world axes to represent through FITS' -TAB convention. This is a list of dictionaries with each dicti axes_mapping : dict A dictionary that maps output axis index to a tuple of input axis indices. In a typical scenario of two input image axes and two output celestial axes for a FITS-like WCS, this dictionary would look like ``{0: (0, 1), 1: (0, 1)}`` with the two non-separable input axes. fix_axes : dict A dictionary containing as keys image axes' indices to be fixed and as values - the values to which inputs should be kept fixed. For example, this dictionary may be used to indicate the celestial axes that should not be included into -TAB approximation because they will be approximated using -SIP. use_cd : bool When `True` - CD-matrix formalism will be used instead of the PC-matrix formalism. bin_ext : str, tuple of str and int Extension name and optionally version for the `~astropy.io.fits.BinTableHDU` HDU. When only a string extension name is provided, extension version will be set to 1. When ``bin_ext`` is a tuple, first element should be extension name and the second element is a positive integer extension version number. Returns ------- hdr : `~astropy.io.fits.Header` Header with WCS-TAB information associated (to be used) with image data. bin_table_hdu : `~astropy.io.fits.BinTableHDU` Binary table extension containing the coordinate array. Raises ------ ValueError When ``bounding_box`` is not defined either through the input ``bounding_box`` parameter or this object's ``bounding_box`` property. ValueError When ``sampling`` is a `tuple` of length larger than 1 that does not match the number of image axes. ValueError When extension version is smaller than 1. TypeError RuntimeError If the number of image axes (``~gwcs.WCS.pixel_n_dim``) is larger than the number of world axes (``~gwcs.WCS.world_n_dim``). """ if isinstance(bin_ext, str): bin_ext = (bin_ext, 1) if isinstance(bounding_box, Bbox): bounding_box = bounding_box.bounding_box(order='F') if isinstance(bounding_box, list): for index, bbox in enumerate(bounding_box): if isinstance(bbox, Bbox): bounding_box[index] = bbox.bounding_box(order='F') # identify input axes: input_axes = [] world_axes_idx = [] for ax in world_axes_group: world_axes_idx.append(ax.axis) input_axes.extend(ax.input_axes) input_axes = sorted(set(input_axes)) n_inputs = len(input_axes) n_outputs = len(world_axes_group) world_axes_idx.sort() # Create initial header and deal with non-degenerate axes if hdr is None: hdr = fits.Header() hdr['NAXIS'] = n_inputs, 'number of array dimensions' hdr['WCSAXES'] = n_outputs hdr.insert('WCSAXES', ('WCSNAME', f'{self.output_frame.name}'), after=True) else: hdr['NAXIS'] += n_inputs hdr['WCSAXES'] += n_outputs # see what axes have been already populated in the header: used_hdr_axes = [] for v in hdr['naxis*'].keys(): try: used_hdr_axes.append(int(v.split('NAXIS')[1]) - 1) except ValueError: continue degenerate_axis_start = max( self.pixel_n_dim + 1, max(used_hdr_axes) + 1 if used_hdr_axes else 1 ) # Deal with non-degenerate axes and add NAXISi to the header: offset = hdr.index('NAXIS') for iax in input_axes: iiax = int(np.searchsorted(used_hdr_axes, iax)) hdr.insert(iiax + offset + 1, (f'NAXIS{iax + 1:d}', int(max(bounding_box[iiax])) + 1)) # 1D grid coordinates: gcrds = [] cdelt = [] bb = [bounding_box[k] for k in input_axes] for (xmin, xmax), s in zip(bb, sampling): npix = max(2, 1 + int(np.ceil(abs((xmax - xmin) / s)))) gcrds.append(np.linspace(xmin, xmax, npix)) cdelt.append((npix - 1) / (xmax - xmin) if xmin != xmax else 1) # In the forward transformation, select only inputs and outputs # that we need given world_axes_group parameter: bb_center = np.mean(bounding_box, axis=1) fixi_dict = { k: bb_center[k] for k in set(range(self.pixel_n_dim)).difference(input_axes) } transform = _fix_transform_inputs(self.forward_transform, fixi_dict) transform = transform | Mapping(world_axes_idx, n_inputs=self.forward_transform.n_outputs) xyz = np.meshgrid(*gcrds[::-1], indexing='ij')[::-1] shape = xyz[0].shape xyz = [v.ravel() for v in xyz] coord = np.stack( transform(*xyz), axis=-1 ) coord = coord.reshape(shape + (len(world_axes_group), )) # create header with WCS info: if hdr is None: hdr = fits.Header() for m, axis_info in enumerate(world_axes_group): k = axis_info.axis widx = world_axes_idx.index(k) k1 = k + 1 ct = cf.get_ctype_from_ucd(self.world_axis_physical_types[k]) if len(ct) > 4: raise ValueError("Axis type name too long.") hdr[f'CTYPE{k1:d}'] = ct + (4 - len(ct)) * '-' + '-TAB' hdr[f'CUNIT{k1:d}'] = self.world_axis_units[k] hdr[f'PS{k1:d}_0'] = bin_ext[0] hdr[f'PV{k1:d}_1'] = bin_ext[1] hdr[f'PS{k1:d}_1'] = coord_col_name hdr[f'PV{k1:d}_3'] = widx + 1 hdr[f'CRVAL{k1:d}'] = 1 if widx < n_inputs: m1 = input_axes[widx] + 1 hdr[f'CRPIX{m1:d}'] = gcrds[widx][0] + 1 if use_cd: hdr[f'CD{k1:d}_{m1:d}'] = cdelt[widx] else: if k1 != m1: hdr[f'PC{k1:d}_{k1:d}'] = 0.0 hdr[f'PC{k1:d}_{m1:d}'] = 1.0 hdr[f'CDELT{k1:d}'] = cdelt[widx] else: m1 = degenerate_axis_start degenerate_axis_start += 1 hdr[f'CRPIX{m1:d}'] = 1 if use_cd: hdr[f'CD{k1:d}_{m1:d}'] = 1.0 else: if k1 != m1: hdr[f'PC{k1:d}_{k1:d}'] = 0.0 hdr[f'PC{k1:d}_{m1:d}'] = 1.0 hdr[f'CDELT{k1:d}'] = 1 # Uncomment 3 lines below to enable use of degenerate axes: # hdr['NAXIS'] = hdr['NAXIS'] + 1 # naxisi_max = max(int(k[5:]) for k in hdr['naxis*'] if k[5:].strip()) # hdr.insert(f'NAXIS{naxisi_max:d}', (f'NAXIS{m1:d}', 1), after=True) # NOTE: in this case make sure NAXIS=WCSAXES coord = coord[None, :] # structured array (data) for binary table HDU: arr = np.array( [(coord, )], dtype=[ (coord_col_name, np.float64, coord.shape), ] ) # create binary table HDU: bin_table_hdu = fits.BinTableHDU(arr, name=bin_ext[0], ver=bin_ext[1]) return hdr, bin_table_hdu def _calc_approx_inv(self, max_inv_pix_error=5, inv_degree=None, npoints=16): """ Compute polynomial fit for the inverse transformation to be used as initial aproximation/guess for the iterative solution. """ self._approx_inverse = None try: # try to use analytic inverse if available: self._approx_inverse = functools.partial(self.backward_transform, with_bounding_box=False) return except (NotImplementedError, KeyError): pass if not isinstance(self.output_frame, cf.CelestialFrame): # The _calc_approx_inv method only works with celestial frame transforms return # Determine reference points. if self.bounding_box is None: # A bounding_box is needed to proceed. return crpix = np.mean(self.bounding_box, axis=1) crval1, crval2 = self.forward_transform(*crpix) # Rotate to native system and deproject. Set center of the projection # transformation to the middle of the bounding box ("image") in order # to minimize projection effects across the entire image, # thus the initial shift. ntransform = ((Shift(crpix[0]) & Shift(crpix[1])) | self.forward_transform | RotateCelestial2Native(crval1, crval2, 180) | Sky2Pix_TAN()) # standard sampling: u, v = _make_sampling_grid(npoints, self.bounding_box, crpix=crpix) undist_x, undist_y = ntransform(u, v) # Double sampling to check if sampling is sufficient. ud, vd = _make_sampling_grid(2 * npoints, self.bounding_box, crpix=crpix) undist_xd, undist_yd = ntransform(ud, vd) fit_inv_poly_u, fit_inv_poly_v, max_inv_resid = _fit_2D_poly( None, max_inv_pix_error, 1, undist_x, undist_y, u, v, undist_xd, undist_yd, ud, vd, verbose=True ) self._approx_inverse = (RotateCelestial2Native(crval1, crval2, 180) | Sky2Pix_TAN() | Mapping((0, 1, 0, 1)) | (fit_inv_poly_u & fit_inv_poly_v) | (Shift(crpix[0]) & Shift(crpix[1]))) def _poly_fit_lu(xin, yin, xout, yout, degree, coord_pow=None): # This function fits 2D polynomials to data by writing the normal system # of equations and solving it using LU-decomposition. In theory this # should be less stable than the SVD method used by numpy's lstsq or # astropy's LinearLSQFitter because the condition of the normal matrix # is squared compared to the direct matrix. However, in practice, # in our (Mihai Cara) tests of fitting WCS distortions, solving the # normal system proved to be significantly more accurate, efficient, # and stable than SVD. # # coord_pow - a dictionary used to store powers of coordinate arrays # of the form x**p * y**q used to build the pseudo-Vandermonde matrix. # This improves efficiency especially when fitting multiple degrees # on the same coordinate grid in _fit_2D_poly by reusing computed # powers. powers = [ (i, j) for i in range(degree + 1) for j in range(degree + 1 - i) if i + j > 0 ] if coord_pow is None: coord_pow = {} nterms = len(powers) flt_type = np.longdouble # allocate array for the coefficients of the system of equations (a*x=b): a = np.empty((nterms, nterms), dtype=flt_type) bx = np.empty(nterms, dtype=flt_type) by = np.empty(nterms, dtype=flt_type) xout = xout.ravel() yout = yout.ravel() x = np.asarray(xin.ravel(), dtype=flt_type) y = np.asarray(yin.ravel(), dtype=flt_type) # pseudo_vander - a reduced Vandermonde matrix for 2D polynomials # that has only terms x^i * y^j with powers i, j that satisfy: # 0 < i + j <= degree. pseudo_vander = np.empty((x.size, nterms), dtype=float) def pow2(p, q): # computes product of powers of coordinate arrays (x**p) * (y**q) # in an efficient way avoiding unnecessary array copying # and/or raising to power if (p, q) in coord_pow: return coord_pow[(p, q)] if p == 0: arr = y**q if q > 1 else y elif q == 0: arr = x**p if p > 1 else x else: xp = x if p == 1 else x**p yq = y if q == 1 else y**q arr = xp * yq coord_pow[(p, q)] = arr return arr for i in range(nterms): pi, qi = powers[i] coord_pq = pow2(pi, qi) pseudo_vander[:, i] = coord_pq bx[i] = np.sum(xout * coord_pq, dtype=flt_type) by[i] = np.sum(yout * coord_pq, dtype=flt_type) for j in range(i, nterms): pj, qj = powers[j] coord_pq = pow2(pi + pj, qi + qj) a[i, j] = np.sum(coord_pq, dtype=flt_type) a[j, i] = a[i, j] with warnings.catch_warnings(record=True): warnings.simplefilter('error', category=linalg.LinAlgWarning) try: lu_piv = linalg.lu_factor(a) poly_coeff_x = linalg.lu_solve(lu_piv, bx).astype(float) poly_coeff_y = linalg.lu_solve(lu_piv, by).astype(float) except (ValueError, linalg.LinAlgWarning, np.linalg.LinAlgError) as e: raise np.linalg.LinAlgError( f"Failed to fit SIP. Reported error:\n{e.args[0]}" ) if not np.all(np.isfinite([poly_coeff_x, poly_coeff_y])): raise np.linalg.LinAlgError( "Failed to fit SIP. Computed coefficients are not finite." ) cond = np.linalg.cond(a.astype(float)) fitx = np.dot(pseudo_vander, poly_coeff_x) fity = np.dot(pseudo_vander, poly_coeff_y) dist = np.sqrt((xout - fitx)**2 + (yout - fity)**2) max_resid = dist.max() return poly_coeff_x, poly_coeff_y, max_resid, powers, cond def _fit_2D_poly(degree, max_error, plate_scale, xin, yin, xout, yout, xind, yind, xoutd, youtd, verbose=False): """ Fit a pair of ordinary 2D polynomials to the supplied transform. """ # The case of one pass with the specified polynomial degree if degree is None: deglist = list(range(1, 10)) elif hasattr(degree, '__iter__'): deglist = sorted(map(int, degree)) if deglist[0] < 1 or deglist[-1] > 9: raise ValueError("Allowed values for SIP degree are [1...9]") else: degree = int(degree) if degree < 1 or degree > 9: raise ValueError("Allowed values for SIP degree are [1...9]") deglist = [degree] single_degree = len(deglist) == 1 fit_error = np.inf if verbose and not single_degree: print(f'Maximum specified SIP approximation error: {max_error}') max_error *= plate_scale fit_warning_msg = "Failed to achieve requested SIP approximation accuracy." # Fit lowest degree SIP first. coord_pow = {} # hold coordinate arrays powers for optimization purpose for deg in deglist: try: cfx_i, cfy_i, fit_error_i, powers_i, cond = _poly_fit_lu( xin, yin, xout, yout, degree=deg, coord_pow=coord_pow ) if verbose and not single_degree: print( f" - SIP degree: {deg}. " f"Maximum residual: {fit_error_i / plate_scale:.5g}" ) except np.linalg.LinAlgError as e: if single_degree: # Nothing to do if failure is for the lowest degree raise e else: # Keep results from the previous iteration. Discard current fit break if not np.isfinite(cond): # Ill-conditioned system if single_degree: warnings.warn("The fit may be poorly conditioned.") cfx = cfx_i cfy = cfy_i fit_error = fit_error_i powers = powers_i break if fit_error_i >= fit_error: # Accuracy does not improve. Likely ill-conditioned system break cfx = cfx_i cfy = cfy_i powers = powers_i fit_error = fit_error_i if fit_error <= max_error: # Requested accuracy has been achieved fit_warning_msg = None break # Continue to the next degree fit_poly_x = Polynomial2D(degree=deg, c0_0=0.0) fit_poly_y = Polynomial2D(degree=deg, c0_0=0.0) for cx, cy, (p, q) in zip(cfx, cfy, powers): setattr(fit_poly_x, f'c{p:1d}_{q:1d}', cx) setattr(fit_poly_y, f'c{p:1d}_{q:1d}', cy) if fit_warning_msg: warnings.warn(fit_warning_msg, linalg.LinAlgWarning) if fit_error <= max_error or single_degree: # Check to see if double sampling meets error requirement. max_resid = _compute_distance_residual( xoutd, youtd, fit_poly_x(xind, yind), fit_poly_y(xind, yind) ) if verbose: print( "* Maximum residual, double sampled grid: " f"{max_resid / plate_scale:.5g}" ) if max_resid > min(5.0 * fit_error, max_error): warnings.warn( "Double sampling check FAILED: Sampling may be too coarse for " "the distortion model being fitted." ) # Residuals on the double-dense grid may be better estimates # of the accuracy of the fit. So we report the largest of # the residuals (on single- and double-sampled grid) as the fit error: fit_error = max(max_resid, fit_error) if verbose: if single_degree: print( f"Maximum residual: {fit_error / plate_scale:.5g}" ) else: print( f"* Final SIP degree: {deg}. " f"Maximum residual: {fit_error / plate_scale:.5g}" ) return fit_poly_x, fit_poly_y, fit_error / plate_scale def _make_sampling_grid(npoints, bounding_box, crpix): step = np.subtract.reduce(bounding_box, axis=1) / (1.0 - npoints) crpix = np.asanyarray(crpix)[:, None, None] x, y = grid_from_bounding_box(bounding_box, step=step, center=False) - crpix return x.flatten(), y.flatten() def _compute_distance_residual(undist_x, undist_y, fit_poly_x, fit_poly_y): """ Compute the distance residuals and return the rms and maximum values. """ dist = np.sqrt((undist_x - fit_poly_x)**2 + (undist_y - fit_poly_y)**2) max_resid = dist.max() return max_resid def _reform_poly_coefficients(fit_poly_x, fit_poly_y): """ The fit polynomials must be recombined to align with the SIP decomposition The result is the f(u,v) and g(u,v) polynomials, and the CD matrix. """ # Extract values for CD matrix and recombining c11 = fit_poly_x.c1_0.value c12 = fit_poly_x.c0_1.value c21 = fit_poly_y.c1_0.value c22 = fit_poly_y.c0_1.value sip_poly_x = fit_poly_x.copy() sip_poly_y = fit_poly_y.copy() # Force low order coefficients to be 0 as defined in SIP sip_poly_x.c0_0 = 0 sip_poly_y.c0_0 = 0 sip_poly_x.c1_0 = 0 sip_poly_x.c0_1 = 0 sip_poly_y.c1_0 = 0 sip_poly_y.c0_1 = 0 cdmat = ((c11, c12), (c21, c22)) invcdmat = npla.inv(np.array(cdmat)) degree = fit_poly_x.degree # Now loop through all remaining coefficients for i in range(0, degree + 1): for j in range(0, degree + 1): if (i + j > 1) and (i + j < degree + 1): old_x = getattr(fit_poly_x, f'c{i}_{j}').value old_y = getattr(fit_poly_y, f'c{i}_{j}').value newcoeff = np.dot(invcdmat, np.array([[old_x], [old_y]])) setattr(sip_poly_x, f'c{i}_{j}', newcoeff[0, 0]) setattr(sip_poly_y, f'c{i}_{j}', newcoeff[1, 0]) return cdmat, sip_poly_x, sip_poly_y def _store_2D_coefficients(hdr, poly_model, coeff_prefix, keeplinear=False): """ Write the polynomial model coefficients to the header. """ mindeg = int(not keeplinear) degree = poly_model.degree for i in range(0, degree + 1): for j in range(0, degree + 1): if (i + j) > mindeg and (i + j < degree + 1): hdr[f'{coeff_prefix}_{i}_{j}'] = getattr(poly_model, f'c{i}_{j}').value def _fix_transform_inputs(transform, inputs): # This is a workaround to the bug in https://github.com/astropy/astropy/issues/11360 # Once that bug is fixed, the code below can be replaced with fix_inputs if not inputs: return transform c = None mapping = [] for k in range(transform.n_inputs): if k in inputs: mapping.append(0) else: # this assumes that n_inputs > 0 and that axis 0 always exist c = 0 if c is None else (c + 1) mapping.append(c) in_selector = Mapping( mapping, n_inputs = transform.n_inputs - len(inputs) ) input_fixer = Const1D(inputs[0]) if 0 in inputs else Identity(1) for k in range(1, transform.n_inputs): input_fixer &= Const1D(inputs[k]) if k in inputs else Identity(1) transform = in_selector | input_fixer | transform return transform class Step: """ Represents a ``step`` in the WCS pipeline. Parameters ---------- frame : `~gwcs.coordinate_frames.CoordinateFrame` A gwcs coordinate frame object. transform : `~astropy.modeling.Model` or None A transform from this step's frame to next step's frame. The transform of the last step should be `None`. """ def __init__(self, frame, transform=None): self.frame = frame self.transform = transform @property def frame(self): return self._frame @frame.setter def frame(self, val): if not isinstance(val, (cf.CoordinateFrame, str)): raise TypeError('"frame" should be an instance of CoordinateFrame or a string.') self._frame = val @property def transform(self): return self._transform @transform.setter def transform(self, val): if val is not None and not isinstance(val, (Model)): raise TypeError('"transform" should be an instance of astropy.modeling.Model.') self._transform = val @property def frame_name(self): if isinstance(self.frame, str): return self.frame return self.frame.name def __getitem__(self, ind): warnings.warn("Indexing a WCS.pipeline step is deprecated. " "Use the `frame` and `transform` attributes instead.", DeprecationWarning) if ind not in (0, 1): raise IndexError("Allowed inices are 0 (frame) and 1 (transform).") if ind == 0: return self.frame return self.transform def __str__(self): return f"{self.frame_name}\t {getattr(self.transform, 'name', 'None') or self.transform.__class__.__name__}" def __repr__(self): return f"Step(frame={self.frame_name}, \ transform={getattr(self.transform, 'name', 'None') or self.transform.__class__.__name__})" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/gwcs/wcstools.py0000644000175100001770000003150414573367100015345 0ustar00runnerdocker# 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 .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, input_frame=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 The bounding box over which the WCS is valid. It is a tuple of tuples of size 2 where each tuple represents a range of (low, high) values. The ``bounding_box`` is in the order of the axes, `~gwcs.coordinate_frames.CoordinateFrame.axes_order`. For two inputs and axes_order(0, 1) the bounding box is ((xlow, xhigh), (ylow, yhigh)). input_frame : ~gwcs.coordinate_frames.CoordinateFrame` The input coordinate frame. """ from .wcs import WCS 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] if input_frame is None: input_frame = 'detector' return WCS(output_frame=coordinate_frame, input_frame=input_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_coords, proj_point='center', projection=projections.Sky2Pix_TAN(), poly_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_coords`` 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_coords : `~astropy.coordinates.SkyCoord` Points in the output coordinate frame. The order matches the order of ``xy``. proj_point : `~astropy.coordinates.SkyCoord` A fiducial point in the output coordinate frame. If set to 'center' (default), the geometric center of input world coordinates will be used as the projection point. To specify an exact point for the projection, a Skycoord object with a coordinate pair can be passed in. projection : `~astropy.modeling.projections.Projection` A projection type. One of the projections in `~astropy.modeling.projections.projcodes`. Defaults to TAN projection (`astropy.modeling.projections.Sky2Pix_TAN`). poly_degree : int Degree of polynomial model to be fit to data. Defaults to 4. polynomial_type : str one of "polynomial", "chebyshev", "legendre". Defaults to "polynomial". Returns ------- wcsobj : `~gwcs.wcs.WCS` a WCS object for this observation. """ from .wcs import WCS supported_poly_types = {"polynomial": models.Polynomial2D, "chebyshev": models.Chebyshev2D, "legendre": models.Legendre2D } x, y = xy if not isinstance(world_coords, coord.SkyCoord): raise TypeError('`world_coords` must be an `~astropy.coordinates.SkyCoord`') try: lon, lat = world_coords.data.lon.deg, world_coords.data.lat.deg except AttributeError: unit_sph = world_coords.unit_spherical lon, lat = unit_sph.lon.deg, unit_sph.lat.deg if isinstance(proj_point, coord.SkyCoord): assert proj_point.size == 1 proj_point.transform_to(world_coords) crval = (proj_point.data.lon, proj_point.data.lat) frame = proj_point.frame elif proj_point == 'center': # use center of input points sc1 = coord.SkyCoord(lon.min()*u.deg, lat.max()*u.deg) sc2 = coord.SkyCoord(lon.max()*u.deg, lat.min()*u.deg) pa = sc1.position_angle(sc2) sep = sc1.separation(sc2) midpoint_sc = sc1.directional_offset_by(pa, sep/2) crval = (midpoint_sc.data.lon, midpoint_sc.data.lat) frame = sc1.frame else: raise ValueError("`proj_point` must be set to 'center', or an" + "`~astropy.coordinates.SkyCoord` object with " + "a pair of points.") 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(crval[0], crval[1], 180*u.deg) trans = (skyrot | projection) projection_x, projection_y = trans(lon, lat) poly = supported_poly_types[polynomial_type](poly_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) distortion = models.Mapping((0, 1, 0, 1)) | poly_x & poly_y poly_x_inverse = fitter(poly, projection_x, projection_y, x) poly_y_inverse = fitter(poly, projection_x, projection_y, y) distortion.inverse = models.Mapping((0, 1, 0, 1)) | poly_x_inverse & poly_y_inverse transform = distortion | projection.inverse | skyrot.inverse skyframe = CelestialFrame(reference_frame=frame) detector = Frame2D(name="detector") pipeline = [(detector, transform), (skyframe, None)] return WCS(pipeline) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1710091850.355165 gwcs-0.21.0/gwcs.egg-info/0000755000175100001770000000000014573367112014610 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091850.0 gwcs-0.21.0/gwcs.egg-info/PKG-INFO0000644000175100001770000001376714573367112015723 0ustar00runnerdockerMetadata-Version: 2.1 Name: gwcs Version: 0.21.0 Summary: Generalized World Coordinate System Author-email: gwcs developers License: Copyright (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. Project-URL: Homepage, https://github.com/spacetelescope/gwcs Project-URL: Tracker, https://github.com/spacetelescope/gwcs/issues Project-URL: Documentation, https://gwcs.readthedocs.io/en/stable/ Project-URL: Source Code, https://github.com/spacetelescope/jwst Requires-Python: >=3.9 Description-Content-Type: text/x-rst Requires-Dist: asdf>=2.8.1 Requires-Dist: astropy>=5.3 Requires-Dist: numpy Requires-Dist: scipy Requires-Dist: asdf_wcs_schemas>=0.4.0 Requires-Dist: asdf-astropy>=0.2.0 Provides-Extra: docs Requires-Dist: sphinx; extra == "docs" Requires-Dist: sphinx-automodapi; extra == "docs" Requires-Dist: sphinx-rtd-theme; extra == "docs" Requires-Dist: stsci-rtd-theme; extra == "docs" Requires-Dist: sphinx-astropy; extra == "docs" Requires-Dist: sphinx-asdf; extra == "docs" Requires-Dist: tomli; python_version < "3.11" and extra == "docs" Provides-Extra: test Requires-Dist: ci-watson>=0.3.0; extra == "test" Requires-Dist: pytest>=4.6.0; extra == "test" Requires-Dist: pytest-astropy; extra == "test" GWCS - Generalized World Coordinate System ========================================== .. image:: https://github.com/spacetelescope/gwcs/actions/workflows/ci.yml/badge.svg :target: https://github.com/spacetelescope/gwcs/actions :alt: CI Status .. image:: https://readthedocs.org/projects/docs/badge/?version=latest :target: https://gwcs.readthedocs.io/en/latest/ :alt: Documentation Status .. image:: https://codecov.io/gh/spacetelescope/gwcs/branch/master/graph/badge.svg?token=JtHal6Jbta :target: https://codecov.io/gh/spacetelescope/gwcs :alt: Code coverage .. image:: http://img.shields.io/badge/powered%20by-AstroPy-orange.svg?style=flat :target: http://www.astropy.org :alt: Powered by Astropy Badge .. image:: https://img.shields.io/badge/powered%20by-STScI-blue.svg?colorA=707170&colorB=3e8ddd&style=flat :target: http://www.stsci.edu :alt: Powered by STScI Badge 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. 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 --------------------------------------------- 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/ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091850.0 gwcs-0.21.0/gwcs.egg-info/SOURCES.txt0000644000175100001770000000370114573367112016475 0ustar00runnerdocker.bandit.yaml .flake8 .gitignore .readthedocs.yaml CHANGES.rst CODE_OF_CONDUCT.md CONTRIBUTING.md MANIFEST.in README.rst convert_schemas.py pyproject.toml requirements-dev.txt tox.ini .github/CODEOWNERS .github/workflows/build.yml .github/workflows/changelog.yml .github/workflows/ci.yml docs/Makefile docs/conf.py docs/index.rst docs/make.bat docs/rtd_environment.yaml docs/_templates/autosummary/base.rst docs/_templates/autosummary/class.rst docs/_templates/autosummary/module.rst docs/gwcs/fits_analog.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 gwcs/__init__.py gwcs/api.py gwcs/coordinate_frames.py gwcs/extension.py gwcs/geometry.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/converters/__init__.py gwcs/converters/geometry.py gwcs/converters/selector.py gwcs/converters/spectroscopy.py gwcs/converters/wcs.py gwcs/converters/tests/__init__.py gwcs/converters/tests/test_selector.py gwcs/converters/tests/test_transforms.py gwcs/converters/tests/test_wcs.py gwcs/tests/__init__.py gwcs/tests/conftest.py gwcs/tests/test_api.py gwcs/tests/test_api_slicing.py gwcs/tests/test_coordinate_systems.py gwcs/tests/test_extension.py gwcs/tests/test_geometry.py gwcs/tests/test_region.py gwcs/tests/test_spectroscopy_models.py gwcs/tests/test_utils.py gwcs/tests/test_wcs.py gwcs/tests/utils.py gwcs/tests/data/__init__.py gwcs/tests/data/acs.hdr gwcs/tests/data/acs_wfc.hdr gwcs/tests/data/miri_lrs_wcs.asdf gwcs/tests/data/miriwcs.asdf gwcs/tests/data/nircamwcs.asdf gwcs/tests/data/simple_wcs2.hdr gwcs/tests/data/stokes.txt licenses/LICENSE.rst licenses/README.rst././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091850.0 gwcs-0.21.0/gwcs.egg-info/dependency_links.txt0000644000175100001770000000000114573367112020656 0ustar00runnerdocker ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091850.0 gwcs-0.21.0/gwcs.egg-info/entry_points.txt0000644000175100001770000000006714573367112020111 0ustar00runnerdocker[asdf.extensions] gwcs = gwcs.extension:get_extensions ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091850.0 gwcs-0.21.0/gwcs.egg-info/requires.txt0000644000175100001770000000041214573367112017205 0ustar00runnerdockerasdf>=2.8.1 astropy>=5.3 numpy scipy asdf_wcs_schemas>=0.4.0 asdf-astropy>=0.2.0 [docs] sphinx sphinx-automodapi sphinx-rtd-theme stsci-rtd-theme sphinx-astropy sphinx-asdf [docs:python_version < "3.11"] tomli [test] ci-watson>=0.3.0 pytest>=4.6.0 pytest-astropy ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091850.0 gwcs-0.21.0/gwcs.egg-info/top_level.txt0000644000175100001770000000000514573367112017335 0ustar00runnerdockergwcs ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1710091850.355165 gwcs-0.21.0/licenses/0000755000175100001770000000000014573367112013760 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/licenses/LICENSE.rst0000644000175100001770000000274114573367100015575 0ustar00runnerdockerCopyright (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. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/licenses/README.rst0000644000175100001770000000024214573367100015442 0ustar00runnerdockerLicenses ======== This directory holds license and credit information for the affiliated package, works the affiliated package is derived from, and/or datasets. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/pyproject.toml0000644000175100001770000000440214573367100015064 0ustar00runnerdocker[project] name = "gwcs" description = "Generalized World Coordinate System" requires-python = ">=3.9" authors = [ { name = "gwcs developers", email = "help@stsci.edu" }, ] dependencies = [ "asdf >= 2.8.1", "astropy >= 5.3", "numpy", "scipy", "asdf_wcs_schemas >= 0.4.0", "asdf-astropy >= 0.2.0", ] dynamic = [ "version", ] [project.readme] file = "README.rst" content-type = "text/x-rst" [project.license] file = "licenses/LICENSE.rst" content-type = "text/x-rst" [project.urls] Homepage = "https://github.com/spacetelescope/gwcs" Tracker = "https://github.com/spacetelescope/gwcs/issues" Documentation = "https://gwcs.readthedocs.io/en/stable/" "Source Code" = "https://github.com/spacetelescope/jwst" [project.entry-points."asdf.extensions"] gwcs = "gwcs.extension:get_extensions" [project.optional-dependencies] docs = [ "sphinx", "sphinx-automodapi", "sphinx-rtd-theme", "stsci-rtd-theme", "sphinx-astropy", "sphinx-asdf", "tomli; python_version <'3.11'", ] test = [ "ci-watson>=0.3.0", "pytest>=4.6.0", "pytest-astropy", ] [build-system] requires = [ "setuptools>=61.2", "setuptools_scm[toml]>=3.4", "wheel", ] build-backend = "setuptools.build_meta" [tool.setuptools.package-data] 'gwcs.tests.data' = ["*"] [tool.setuptools.packages.find] namespaces = false [tool.build_sphinx] source-dir = "docs" build-dir = "docs/_build" all_files = "1" [tool.distutils.upload_docs] upload-dir = "docs/_build/html" show-response = 1 [tool.pytest.ini_options] minversion = "4.6" norecursedirs = [ "build", "docs/_build", ".tox", ] doctest_plus = "enabled" addopts = "--doctest-rst" filterwarnings = [ "ignore:Models in math_functions:astropy.utils.exceptions.AstropyUserWarning", # importing astropy code causes this warning to be issued, so ignore it "ignore:numpy.ndarray size changed:RuntimeWarning", ] [tool.coverage.run] omit = [ "gwcs/tests/test_*", "gwcs/tags/tests/test_*", "*/gwcs/tests/test_*", "*/gwcs/tags/tests/test_*", ] [tool.coverage.report] exclude_lines = [ "pragma: no cover", "except ImportError", "raise AssertionError", "raise NotImplementedError", "def main\\(.*\\):", "pragma: py{ignore_python_version}", ] [tool.setuptools_scm] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/requirements-dev.txt0000644000175100001770000000106514573367100016212 0ustar00runnerdockergit+https://github.com/asdf-format/asdf git+https://github.com/asdf-format/asdf-standard # Use weekly astropy dev build --extra-index-url https://pypi.anaconda.org/astropy/simple astropy --pre git+https://github.com/astropy/asdf-astropy git+https://github.com/asdf-format/asdf-transform-schemas git+https://github.com/asdf-format/asdf-coordinates-schemas git+https://github.com/asdf-format/asdf-wcs-schemas # Use Bi-weekly numpy/scipy dev builds --extra-index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple numpy>=0.0.dev0 scipy>=0.0.dev0 ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1710091850.359165 gwcs-0.21.0/setup.cfg0000644000175100001770000000004614573367112013774 0ustar00runnerdocker[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1710091840.0 gwcs-0.21.0/tox.ini0000644000175100001770000000520314573367100013463 0ustar00runnerdocker[tox] envlist = check-{style,security,build} test{,-dev}{,-pyargs,-cov} test-numpy{125,122} build-{docs,dist} # tox environments are constructed with so-called 'factors' (or terms) # separated by hyphens, e.g. test-devdeps-cov. Lines below starting with factor: # will only take effect if that factor is included in the environment name. To # see a list of example environments that can be run, along with a description, # run: # # tox -l -v # [testenv:check-style] description = check code style, e.g. with flake8 skip_install = true deps = flake8 commands = flake8 . {posargs} [testenv:check-security] description = run bandit to check security compliance skip_install = true deps = bandit>=1.7 commands = bandit -r -ll -c .bandit.yaml gwcs [testenv:check-build] description = check build sdist/wheel and a strict twine check for metadata skip_install = true deps = twine>=3.3 build commands = python -m build . twine check --strict dist/* [testenv] description = run tests jwst: of JWST pipeline romancal: of Romancal pipeline dev: with the latest developer version of key dependencies pyargs: with --pyargs on installed package warnings: treating warnings as errors cov: with coverage xdist: using parallel processing passenv = HOME GITHUB_* TOXENV CI CODECOV_* DISPLAY set_env = dev: PIP_EXTRA_INDEX_URL = https://pypi.anaconda.org/astropy/simple https://pypi.anaconda.org/liberfa/simple https://pypi.anaconda.org/scientific-python-nightly-wheels/simple args_are_paths = false change_dir = pyargs: {env:HOME} extras = test alldeps: all deps = xdist: pytest-xdist cov: pytest-cov jwst: jwst[test] @ git+https://github.com/spacetelescope/jwst.git romancal: romancal[test] @ git+https://github.com/spacetelescope/romancal.git numpy123: numpy==1.23.* numpy125: numpy==1.25.* pass_env = jwst,romancal: CRDS_* commands_pre = dev: pip install -r requirements-dev.txt -U --upgrade-strategy eager pip freeze commands = pytest \ warnings: -W error \ xdist: -n auto \ pyargs: {toxinidir}/docs --pyargs gwcs \ jwst: --pyargs jwst --ignore-glob=timeconversion --ignore-glob=associations --ignore-glob=scripts\ romancal: --pyargs romancal \ cov: --cov=. --cov-config=pyproject.toml --cov-report=term-missing --cov-report=xml \ {posargs} [testenv:build-docs] description = invoke sphinx-build to build the HTML docs extras = docs commands = sphinx-build -W docs docs/_build [testenv:build-dist] description = build wheel and sdist skip_install = true deps = build commands = python -m build .