diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 2d4d2bdb88..9c73b7145f 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,11 +1,11 @@ version: 2 build: - os: ubuntu-20.04 + os: ubuntu-22.04 apt_packages: - graphviz tools: - python: "3.9" + python: "3.11" jobs: post_checkout: # Use `git log` to check if the latest commit contains "skip rtd" or "rtd skip", diff --git a/CHANGES.rst b/CHANGES.rst index 0ec2aeff96..1865c17c79 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,12 +4,22 @@ New Features ------------ +- Introduce jdaviz.open to automatically detect the appropriate config and load data [#2221] + +- Add Simplify button to subset plugin to make composite spectral subsets more user + friendly. [#2237] + Cubeviz ^^^^^^^ Imviz ^^^^^ +- Added the ability to load DS9 region files (``.reg``) using the ``IMPORT DATA`` + button. However, this only works after loading at least one image into Imviz. [#2201] + +- Added support for new ``CircularAnnulusROI`` subset from glue. [#2201] + Mosviz ^^^^^^ @@ -22,6 +32,8 @@ Specviz2d API Changes ----------- +- Subset Plugin now respects the chosen display unit after using Unit Conversion. [#2195] + Cubeviz ^^^^^^^ @@ -34,12 +46,16 @@ Mosviz Specviz ^^^^^^^ +* Re-enabled unit conversion support. [#2127] + Specviz2d ^^^^^^^^^ Bug Fixes --------- +- Fixed wrong elliptical region translation in ``app.get_subsets()``. [#2244] + Cubeviz ^^^^^^^ @@ -58,6 +74,9 @@ Specviz2d Other Changes and Additions --------------------------- +- Gaussian smooth plugin excludes results from the gaussian smooth plugin from the input + dataset dropdown. [#2239] + 3.5.1 (unreleased) ================== diff --git a/docs/_static/jdaviz.css b/docs/_static/jdaviz.css new file mode 100644 index 0000000000..94ff946b6f --- /dev/null +++ b/docs/_static/jdaviz.css @@ -0,0 +1,74 @@ +/* Main page overview cards */ + +.sd-card { + background: #fff; + border-radius: 0; + padding: 30px 10px 20px 10px; + margin: 10px 0px; +} + +.sd-card .sd-card-header { + text-align: center; +} + +.sd-card .sd-card-header .sd-card-text { + margin: 0px; +} + +.sd-card .sd-card-img-top { + height: 52px; + width: 52px; + margin-left: auto; + margin-right: auto; +} + +.sd-card .sd-card-header { + border: none; + background-color: white; + color: #150458 !important; + font-size: var(--pst-font-size-h5); + font-weight: bold; + padding: 2.5rem 0rem 0.5rem 0rem; +} + +.sd-card .sd-card-footer { + border: none; + background-color: white; +} + +.sd-card .sd-card-footer .sd-card-text { + max-width: 220px; + margin-left: auto; + margin-right: auto; +} + +/* Dark theme tweaking */ +html[data-theme=dark] .sd-card img[src*='.svg'] { + filter: invert(0.82) brightness(0.8) contrast(1.2); +} + +/* Main index page overview cards */ +html[data-theme=dark] .sd-card { + background-color:var(--pst-color-background); +} + +html[data-theme=dark] .sd-shadow-sm { + box-shadow: 0 .1rem 1rem rgba(250, 250, 250, .6) !important +} + +html[data-theme=dark] .sd-card .sd-card-header { + background-color:var(--pst-color-background); + color: #150458 !important; +} + +html[data-theme=dark] .sd-card .sd-card-footer { + background-color:var(--pst-color-background); +} + +html[data-theme=dark] h1 { + color: var(--pst-color-primary); +} + +html[data-theme=dark] h3 { + color: #0a6774; +} diff --git a/docs/conf.py b/docs/conf.py index b450ac9fd3..8f06459b9a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -25,34 +25,24 @@ # Thus, any C-extensions that are needed to build the documentation will *not* # be accessible, and the documentation will not build correctly. -import os import sys import datetime -import importlib.metadata as importlib_metadata -from pathlib import Path - -if sys.version_info < (3, 11): - import tomli as tomllib -else: - import tomllib +from jdaviz import __version__ try: - from sphinx_astropy.conf.v1 import * # noqa + from sphinx_astropy.conf.v2 import * # noqa except ImportError: print('ERROR: the documentation requires the sphinx-astropy package to be installed') sys.exit(1) -with open(Path(__file__).parent.parent / "pyproject.toml", "rb") as configuration_file: - metadata = tomllib.load(configuration_file) - # -- General configuration ---------------------------------------------------- # By default, highlight as Python 3. highlight_language = 'python3' # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.2' +# 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. @@ -60,7 +50,7 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns.append('_templates') +exclude_patterns.append('_templates') # noqa: F405 # This is added to the end of RST files - a good place to put substitutions to # be used globally. @@ -134,13 +124,13 @@ .. |icon-viewer-data-select| image:: /img/icons/viewer_data_select.png :scale: 30 :alt: data select icon -""" +""" # noqa: F405 # -- Project information ------------------------------------------------------ # This does not *have* to match the package name, but typically does -project = metadata['project']['name'] -author = metadata['project']['authors'][0]['name'] +project = "jdaviz" +author = "JDADF Developers" copyright = '{0}, {1}'.format( datetime.datetime.now().year, author) @@ -149,15 +139,15 @@ # built documents. # The full version, including alpha/beta/rc tags. -release = importlib_metadata.version(project) +release = __version__ +dev = "dev" in release # The short X.Y version. version = '.'.join(release.split('.')[:2]) -extensions += ['sphinx.ext.extlinks'] +extensions += ['sphinx.ext.extlinks', 'sphinx_design'] # noqa: F405 gh_tag = f'v{release}' if '.dev' not in release else 'main' extlinks = {'gh-tree': (f'https://github.com/spacetelescope/jdaviz/tree/{gh_tag}/%s', '%s'), - 'gh-notebook': (f'https://github.com/spacetelescope/jdaviz/blob/{gh_tag}/notebooks/%s.ipynb', - '%s notebook')} + 'gh-notebook': (f'https://github.com/spacetelescope/jdaviz/blob/{gh_tag}/notebooks/%s.ipynb', '%s notebook')} # noqa: E501 # -- Options for HTML output -------------------------------------------------- @@ -168,33 +158,54 @@ # variables set in the global configuration. The variables set in the # global configuration are listed below, commented out. +html_css_files = ["jdaviz.css"] +html_copy_source = False + +html_theme_options.update( # noqa: F405 + { + "github_url": "https://github.com/spacetelescope/jdaviz", + "external_links": [ + {"name": "Help Desk", "url": "http://jwsthelp.stsci.edu/"}, + ], + "use_edit_page_button": True, + } +) + +html_context = { + "default_mode": "light", + "to_be_indexed": ["stable", "latest"], + "is_development": dev, + "github_user": "spacetelescope", + "github_repo": "jdaviz", + "github_version": "main", + "doc_path": "docs", +} # 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 = [] +# 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 -html_theme = "sphinx_rtd_theme" +# html_theme = "sphinx_rtd_theme" # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = '' +html_logo = 'logos/jdaviz.svg' # 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 = '' +html_favicon = 'logos/specviz2d.ico' # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '' +# html_last_updated_fmt = '' # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". @@ -220,21 +231,6 @@ [author], 1)] -# -- Options for the edit_on_github extension --------------------------------- - -if metadata['tool']['build_sphinx']['edit_on_github']: - - extensions += ['sphinx_astropy.ext.edit_on_github'] - - edit_on_github_project = metadata['tool']['build_sphinx']['github_project'] - edit_on_github_branch = "main" - - edit_on_github_source_root = "" - edit_on_github_doc_root = "docs" - -# -- Resolving issue number to links in changelog ----------------------------- -github_issues_url = 'https://github.com/{0}/issues/'.format(metadata['tool']['build_sphinx']['github_project']) - # -- Turn on nitpicky mode for sphinx (to warn about references not found) ---- nitpicky = True @@ -263,15 +259,16 @@ nitpick_ignore.append((dtype, target)) # Extra intersphinx in addition to what is already in sphinx-astropy -intersphinx_mapping['glue'] = ('http://docs.glueviz.org/en/stable/', None) -intersphinx_mapping['glue_jupyter'] = ('https://glue-jupyter.readthedocs.io/en/stable/', None) -intersphinx_mapping['regions'] = ('https://astropy-regions.readthedocs.io/en/stable/', None) -intersphinx_mapping['skimage'] = ('https://scikit-image.org/docs/stable/', None) -intersphinx_mapping['specutils'] = ('https://specutils.readthedocs.io/en/stable/', None) -intersphinx_mapping['specreduce'] = ('https://specreduce.readthedocs.io/en/stable/', None) -intersphinx_mapping['photutils'] = ('https://photutils.readthedocs.io/en/stable/', None) -intersphinx_mapping['traitlets'] = ('https://traitlets.readthedocs.io/en/stable/', None) -intersphinx_mapping['roman_datamodels'] = ('https://roman-datamodels.readthedocs.io/en/stable/', None) +intersphinx_mapping.update({ # noqa: F405 + 'glue': ('http://docs.glueviz.org/en/stable/', None), + 'glue_jupyter': ('https://glue-jupyter.readthedocs.io/en/stable/', None), + 'photutils': ('https://photutils.readthedocs.io/en/stable/', None), + 'regions': ('https://astropy-regions.readthedocs.io/en/stable/', None), + 'roman_datamodels': ('https://roman-datamodels.readthedocs.io/en/stable/', None), + 'skimage': ('https://scikit-image.org/docs/stable/', None), + 'specreduce': ('https://specreduce.readthedocs.io/en/stable/', None), + 'specutils': ('https://specutils.readthedocs.io/en/stable/', None), + 'traitlets': ('https://traitlets.readthedocs.io/en/stable/', None)}) # Options for linkcheck linkcheck_ignore = ['https://github.com/spacetelescope/jdaviz/settings/branches'] diff --git a/docs/create_products.rst b/docs/create_products.rst new file mode 100644 index 0000000000..3a78bd6991 --- /dev/null +++ b/docs/create_products.rst @@ -0,0 +1,71 @@ +.. _create_products: + +Creating Jdaviz-readable Products +================================= + +Spectroscopic data products (1D, 2D, and 3D) can be loaded +in the different ``jdaviz`` configurations using +essentially two methods, i.e., loading :class:`~specutils.Spectrum1D` objects or +from FITS files. Here, we list a few ways in which data can be packaged to be easily loaded +into a ``jdaviz`` configuration. + +Data in a database +------------------ + +If the data are stored in a database, we recommend storing a :class:`~specutils.Spectrum1D` object +per entry. This would allow the user to query the data and visualize it in +``jdaviz`` with few lines of code; also see :ref:`create_product_spectrum1d_obj`. + +Data in FITS files +------------------ + +If the data are stored as FITS files, we propose three options: + +* :ref:`create_product_specutils_loader` +* :ref:`create_product_dedicated_loader` +* :ref:`create_product_spectrum1d_obj` + +.. _create_product_specutils_loader: + +Using an available specutils loader +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Available loaders can be listed with the following commands: + +.. code-block:: python + + from specutils import Spectrum1D + Spectrum1D.read.list_formats() + +The majority are fairly specific to missions and instruments. Four formats +are more generic and adaptable: ``ASCII``, ``ECSV``, ``tabular-fits``, and +``wcs1d-fits``. More information on how to create files that are readable by +these loaders can be found on the `specutils GitHub repository +`_. + +.. _create_product_dedicated_loader: + +Creating a dedicated loader +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The `specutils documentation on how to create a custom loader +`_ +is available. We are working on the necessary documentation to prompt +``jdaviz`` to recognize a custom loader developed in ``specutils``. + +.. _create_product_spectrum1d_obj: + +Providing scripts to load the data as Spectrum1D objects +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If none of the above is an acceptable option, the user can create the data +products with their custom format and provide scripts or Jupyter Notebooks +that show how to read the products and create :class:`~specutils.Spectrum1D` objects +that can be read into ``jdaviz``. More about +how to create :class:`~specutils.Spectrum1D` objects for the 1D, 2D, and 3D cases can be +found in the corresponding "Importing data" sections of the various configurations: + +* :ref:`specviz-import-data` +* :ref:`cubeviz-import-data` +* :ref:`specviz2d-import-data` +* :ref:`mosviz-import-api` diff --git a/docs/cubeviz/examples.rst b/docs/cubeviz/examples.rst index 21e9119c61..dd029a38ad 100644 --- a/docs/cubeviz/examples.rst +++ b/docs/cubeviz/examples.rst @@ -18,25 +18,25 @@ Open and Explore a Cube .. raw:: html - + Selecting Subsets ================= .. raw:: html - + Model Fitting ============= .. raw:: html - + Line Analysis ============= .. raw:: html - + diff --git a/docs/cubeviz/export_data.rst b/docs/cubeviz/export_data.rst index 70ebac4c9d..248ad932f4 100644 --- a/docs/cubeviz/export_data.rst +++ b/docs/cubeviz/export_data.rst @@ -29,7 +29,7 @@ An example without accessing Specviz: .. code-block:: python subset1_spec1d = cubeviz.get_data(data_label=flux_data_label, - subset_to_apply="Subset 1", + spatial_subset="Subset 1", function="mean") Note that in the above example, the ``function`` keyword is used to tell Cubeviz diff --git a/docs/cubeviz/import_data.rst b/docs/cubeviz/import_data.rst index dee10366da..993ad8969f 100644 --- a/docs/cubeviz/import_data.rst +++ b/docs/cubeviz/import_data.rst @@ -10,6 +10,7 @@ now supports 3D cubes and allows the Python-level interface and parsing tools to be defined in ``specutils`` instead of being duplicated in Jdaviz. :class:`~specutils.Spectrum1D` objects are very flexible in their capabilities, however, and hence should address most astronomical spectrum use cases. +If you are creating your own data products, please read the page :ref:`create_products`. Cubeviz will automatically parse the data into the multiple viewers as described in :ref:`cubeviz-display-cubes`. For the best experience, data loaded into Cubeviz should contain valid WCS diff --git a/docs/cubeviz/index.rst b/docs/cubeviz/index.rst index 3683951423..3205126deb 100644 --- a/docs/cubeviz/index.rst +++ b/docs/cubeviz/index.rst @@ -1,12 +1,11 @@ - -.. image:: ../logos/cubeviz.svg - :width: 400 +.. |cubeviz_logo| image:: ../logos/cube.svg + :height: 42px .. _cubeviz: -####### -Cubeviz -####### +###################### +|cubeviz_logo| Cubeviz +###################### .. image:: https://stsci.box.com/shared/static/esod50xtbn07wvls1ia07urnr65tv2bj.gif :alt: Introductory video tour of the Cubeviz configuration and its features diff --git a/docs/dev/index.rst b/docs/dev/index.rst index 0429567a6d..043722ab82 100644 --- a/docs/dev/index.rst +++ b/docs/dev/index.rst @@ -1,6 +1,6 @@ -########## -Developers -########## +############### +Developer Guide +############### Here is some documentation specific for developers. diff --git a/docs/imviz/examples.rst b/docs/imviz/examples.rst index 7bbdf798fd..eca2ef12f4 100644 --- a/docs/imviz/examples.rst +++ b/docs/imviz/examples.rst @@ -18,32 +18,32 @@ Open Image Data .. raw:: html - + Overplotting a Catalog ====================== .. raw:: html - + Aligning Images =============== .. raw:: html - + Exploring the Plugin Toolbar ============================ .. raw:: html - + Aperture Photometry =================== .. raw:: html - + diff --git a/docs/imviz/import_data.rst b/docs/imviz/import_data.rst index 82ff507764..6d23609109 100644 --- a/docs/imviz/import_data.rst +++ b/docs/imviz/import_data.rst @@ -43,6 +43,9 @@ application. A notification will appear to let users know if the data import was successful. Afterward, the new data set can be found in the :guilabel:`Data` tab of each viewer's options menu as described in :ref:`cubeviz-selecting-data`. +Once data is loaded, you may use the :guilabel:`Import Data` button again +to load regions from a ``.reg`` file; also see :ref:`imviz-import-regions-api`. + .. _imviz-import-api: Importing data via the API diff --git a/docs/imviz/index.rst b/docs/imviz/index.rst index 147ea78db4..5f0b030639 100644 --- a/docs/imviz/index.rst +++ b/docs/imviz/index.rst @@ -1,11 +1,11 @@ -.. image:: ../logos/imviz.svg - :width: 400 +.. |imviz_logo| image:: ../logos/imviz\ icon.svg + :height: 42px .. _imviz: -##### -Imviz -##### +################## +|imviz_logo| Imviz +################## .. image:: https://stsci.box.com/shared/static/56jhed2cqr3nr2w5a3e5gwwkvytmc00n.gif :alt: Introductory video tour of the Imviz configuration and its features diff --git a/docs/index.rst b/docs/index.rst index b87827979d..af771a7dbf 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2,8 +2,58 @@ Jdaviz ###### -.. image:: logos/jdaviz.svg - :width: 400 +.. grid:: 3 + :gutter: 1 + + .. grid-item-card:: + :img-top: logos/imviz\ icon.svg + + .. button-ref:: imviz/index + :expand: + :color: primary + :click-parent: + + Jump to Imviz + + .. grid-item-card:: + :img-top: logos/specicon.svg + + .. button-ref:: specviz/index + :expand: + :color: primary + :click-parent: + + Jump to Specviz + + .. grid-item-card:: + :img-top: logos/cube.svg + + .. button-ref:: cubeviz/index + :expand: + :color: primary + :click-parent: + + Jump to Cubeviz + + .. grid-item-card:: + :img-top: logos/specviz2d\ icon.svg + + .. button-ref:: specviz2d/index + :expand: + :color: primary + :click-parent: + + Jump to Specviz2D + + .. grid-item-card:: + :img-top: logos/mos.svg + + .. button-ref:: mosviz/index + :expand: + :color: primary + :click-parent: + + Jump to Mosviz ``jdaviz`` is a package of astronomical data analysis visualization tools based on the Jupyter platform. These GUI-based tools link data @@ -46,180 +96,38 @@ contextual information like on-sky views of the spectrograph slit. under the "Materials and Videos" expandable section. Scroll down to the bottom of that section to find materials from the most recent session (JWebbinar 24, March 2023). -.. _jdaviz_instrument_table: - -JWST Instrument Modes in Jdaviz -=============================== - -This tool is designed with instrument modes from the James Webb Space Telescope (JWST) in mind, but -the tool should be flexible enough to read in data from many astronomical telescopes. The table below -summarizes Jdaviz file support specific to JWST instrument modes. - -.. list-table:: JWST Instrument Modes in Jdaviz - :widths: 25 15 10 15 20 - :header-rows: 1 - - * - Instrument - - Template Mode - - File Type - - Pipeline Level - - Primary Configuration - * - NIRSpec - - MOS - - S2D - - 2b,3 - - Mosviz - * - - - - - X1D - - 2b,3 - - Specviz - * - - - IFU - - S3D - - 2b,3 - - Cubeviz - * - - - - - X1D - - 2b,3 - - Specviz - * - - - FS - - S2D - - 2b,3 - - Specviz2d - * - - - - - X1D - - 2b,3 - - Specviz - * - - - BOTS - - X1DINTS - - -- - - No Support - * - NIRISS - - IMAGING - - I2D - - 2b,3 - - Imviz - * - - - WFSS - - X1D - - 2b - - Specviz - * - - - AMI - - AMINORM - - -- - - No Support - * - - - SOSS - - X1DINTS - - -- - - No Support - * - NIRCam - - Imaging - - I2D - - 2b,3 - - Imviz - * - - - Coronagraphic Imaging - - I2D - - 2b,3 - - Imviz - * - - - WFSS - - X1D - - 2b - - Specviz - * - - - Grism TSO - - X1DINTS - - -- - - No Support - * - MIRI - - Imaging - - I2D - - 2b,3 - - Imviz - * - - - Coronagraphic Imaging - - I2D - - 2b,3 - - Imviz - * - - - LRS-slit - - S2D - - 2b,3 - - Specviz2d - * - - - - - X1D - - 2b,3 - - Specviz - * - - - LRS-slitless - - X1DINTS - - -- - - No Support - * - - - MRS - - S3D - - 2b,3 - - Cubeviz - * - - - - - X1D - - 2b, 3 - - Specviz - - +************ Using Jdaviz -============ +************ .. toctree:: :maxdepth: 2 - installation.rst - imviz/index.rst - specviz/index.rst - cubeviz/index.rst - specviz2d/index.rst - mosviz/index.rst - plugin_api.rst - save_state.rst - display.rst - sample_notebooks.rst + index_using_jdaviz -Reference/API -============= +******************************* +JWST Instrument Modes in Jdaviz +******************************* .. toctree:: - :maxdepth: 2 + :maxdepth: 2 - dev/index.rst - reference/api.rst + index_jwst_modes -Additional documentation -======================== +***************** +Development Guide +***************** .. toctree:: - :maxdepth: 1 + :maxdepth: 2 - known_bugs.rst + index_ref_api +********************* License & Attribution -===================== +********************* -This project is Copyright (c) JDADF Developers and licensed under -the terms of the BSD 3-Clause license. - -This package is based upon -the `Astropy package template `_ -which is licensed under the BSD 3-clause licence. See the -`licenses `_ -folder for more information. +.. toctree:: + :maxdepth: 2 -Cite ``jdaviz`` via our Zenodo record: https://doi.org/10.5281/zenodo.5513927. + index_citation diff --git a/docs/index_citation.rst b/docs/index_citation.rst new file mode 100644 index 0000000000..5a277e10d1 --- /dev/null +++ b/docs/index_citation.rst @@ -0,0 +1,25 @@ +.. _cite-jdaviz: + +******** +Citation +******** + +Cite Us +======= + +If you use ``jdaviz``, please cite us via our Zenodo record: |Zenodo| + +.. |Zenodo| image:: https://zenodo.org/badge/DOI/10.5281/zenodo.5513927.svg + :target: https://doi.org/10.5281/zenodo.5513927 + +License +======= + +This project is Copyright (c) JDADF Developers and licensed under +the terms of the BSD 3-Clause license. + +This package is based upon +the `Astropy package template `_ +which is licensed under the BSD 3-clause licence. See the +`licenses `_ +folder for more information. diff --git a/docs/index_jwst_modes.rst b/docs/index_jwst_modes.rst new file mode 100644 index 0000000000..ff1e901ba3 --- /dev/null +++ b/docs/index_jwst_modes.rst @@ -0,0 +1,145 @@ +.. _jdaviz_instrument_table: + +********** +JWST Modes +********** + +This tool is designed with instrument modes from the James Webb Space Telescope (JWST) in mind, but +the tool should be flexible enough to read in data from many astronomical telescopes. The table below +summarizes Jdaviz file support specific to JWST instrument modes. + +NIRSpec +======= + +.. list-table:: JWST/NIRSpec Instrument Modes in Jdaviz + :widths: 15 10 15 20 + :header-rows: 1 + + * - Template Mode + - File Type + - Pipeline Level + - Primary Configuration + * - MOS + - S2D + - 2b,3 + - Mosviz + * - + - X1D + - 2b,3 + - Specviz + * - IFU + - S3D + - 2b,3 + - Cubeviz + * - + - X1D + - 2b,3 + - Specviz + * - FS + - S2D + - 2b,3 + - Specviz2d + * - + - X1D + - 2b,3 + - Specviz + * - BOTS + - X1DINTS + - -- + - No Support + +NIRISS +====== + +.. list-table:: JWST/NIRISS Instrument Modes in Jdaviz + :widths: 15 10 15 20 + :header-rows: 1 + + * - Template Mode + - File Type + - Pipeline Level + - Primary Configuration + * - IMAGING + - I2D + - 2b,3 + - Imviz + * - WFSS + - X1D + - 2b + - Specviz + * - AMI + - AMINORM + - -- + - No Support + * - SOSS + - X1DINTS + - -- + - No Support + +NIRCam +====== + +.. list-table:: JWST/NIRCam Instrument Modes in Jdaviz + :widths: 15 10 15 20 + :header-rows: 1 + + * - Template Mode + - File Type + - Pipeline Level + - Primary Configuration + * - Imaging + - I2D + - 2b,3 + - Imviz + * - Coronagraphic Imaging + - I2D + - 2b,3 + - Imviz + * - WFSS + - X1D + - 2b + - Specviz + * - Grism TSO + - X1DINTS + - -- + - No Support + +MIRI +==== + +.. list-table:: JWST/MIRI Instrument Modes in Jdaviz + :widths: 15 10 15 20 + :header-rows: 1 + + * - Template Mode + - File Type + - Pipeline Level + - Primary Configuration + * - Imaging + - I2D + - 2b,3 + - Imviz + * - Coronagraphic Imaging + - I2D + - 2b,3 + - Imviz + * - LRS-slit + - S2D + - 2b,3 + - Specviz2d + * - + - X1D + - 2b,3 + - Specviz + * - LRS-slitless + - X1DINTS + - -- + - No Support + * - MRS + - S3D + - 2b,3 + - Cubeviz + * - + - X1D + - 2b, 3 + - Specviz diff --git a/docs/index_ref_api.rst b/docs/index_ref_api.rst new file mode 100644 index 0000000000..b981598b1a --- /dev/null +++ b/docs/index_ref_api.rst @@ -0,0 +1,11 @@ +.. _jdaviz-dev: + +********** +Developers +********** + +.. toctree:: + :maxdepth: 2 + + dev/index + reference/api diff --git a/docs/index_using_jdaviz.rst b/docs/index_using_jdaviz.rst new file mode 100644 index 0000000000..9aa9c9ab9f --- /dev/null +++ b/docs/index_using_jdaviz.rst @@ -0,0 +1,21 @@ +.. _using-jdaviz: + +********** +User Guide +********** + +.. toctree:: + :maxdepth: 2 + + installation + imviz/index + specviz/index + cubeviz/index + specviz2d/index + mosviz/index + plugin_api + save_state + display + sample_notebooks + create_products + known_bugs diff --git a/docs/logos/cube.ico b/docs/logos/cube.ico new file mode 100644 index 0000000000..35c7e0a46a Binary files /dev/null and b/docs/logos/cube.ico differ diff --git a/docs/logos/cube.svg b/docs/logos/cube.svg new file mode 100644 index 0000000000..cdb126860f --- /dev/null +++ b/docs/logos/cube.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/logos/imviz.ico b/docs/logos/imviz.ico new file mode 100644 index 0000000000..ec2b3ac553 Binary files /dev/null and b/docs/logos/imviz.ico differ diff --git a/docs/logos/mos.ico b/docs/logos/mos.ico new file mode 100644 index 0000000000..254ccea2a1 Binary files /dev/null and b/docs/logos/mos.ico differ diff --git a/docs/logos/mos.svg b/docs/logos/mos.svg new file mode 100644 index 0000000000..9667f16213 --- /dev/null +++ b/docs/logos/mos.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/logos/specicon.svg b/docs/logos/specicon.svg new file mode 100644 index 0000000000..2deb534e68 --- /dev/null +++ b/docs/logos/specicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/logos/specviz.ico b/docs/logos/specviz.ico new file mode 100644 index 0000000000..f051cec174 Binary files /dev/null and b/docs/logos/specviz.ico differ diff --git a/docs/logos/specviz2d.ico b/docs/logos/specviz2d.ico new file mode 100644 index 0000000000..957345236f Binary files /dev/null and b/docs/logos/specviz2d.ico differ diff --git a/docs/mosviz/import_data.rst b/docs/mosviz/import_data.rst index 74621ca579..ed0f513af2 100644 --- a/docs/mosviz/import_data.rst +++ b/docs/mosviz/import_data.rst @@ -100,7 +100,9 @@ Manual Loading If an automatic parser is not provided yet for your data, Mosviz provides manual loading by specifying which files are which, and the associations between them. This is done by generating three lists containing the filenames for the 1D spectra, -2D spectra, and images in your dataset. These three lists are taken as arguments +2D spectra, and images in your dataset (if you are creating your own data products, +please read the page :ref:`create_products`). +These three lists are taken as arguments by :py:meth:`~jdaviz.configs.mosviz.helper.Mosviz.load_data`. The association between files is assumed to be the order of each list (e.g., the first object consists of the first filename specified in each list, the second target is the second in each list, and so forth). diff --git a/docs/mosviz/index.rst b/docs/mosviz/index.rst index 211d2ff627..64adb21405 100644 --- a/docs/mosviz/index.rst +++ b/docs/mosviz/index.rst @@ -1,12 +1,11 @@ - -.. image:: ../logos/mosviz.svg - :width: 400 +.. |mosviz_logo| image:: ../logos/mos.svg + :height: 42px .. _mosviz: -###### -Mosviz -###### +#################### +|mosviz_logo| Mosviz +#################### .. image:: https://stsci.box.com/shared/static/sbstzr702zqghc40x49g6zxsik6ayg6u.gif :alt: Introductory video tour of the Mosviz configuration and its features diff --git a/docs/reference/api.rst b/docs/reference/api.rst index 2699a7ca1f..e155ee3b30 100644 --- a/docs/reference/api.rst +++ b/docs/reference/api.rst @@ -1,239 +1,45 @@ .. _jdaviz-api: -### -API -### - -.. _jdaviz-api-configs: +############# +Reference/API +############# Configurations ============== -.. automodapi:: jdaviz.configs.cubeviz.helper - :no-inheritance-diagram: - -.. automodapi:: jdaviz.configs.imviz.helper - :no-inheritance-diagram: - -.. automodapi:: jdaviz.configs.mosviz.helper - :no-inheritance-diagram: - -.. automodapi:: jdaviz.configs.specviz.helper - :no-inheritance-diagram: +.. toctree:: + :maxdepth: 2 -.. automodapi:: jdaviz.configs.specviz2d.helper - :no-inheritance-diagram: - -.. _jdaviz-api-viewers: + api_configs Viewers ======= -.. automodapi:: jdaviz.configs.default.plugins.viewers - :no-inheritance-diagram: - -.. automodapi:: jdaviz.configs.cubeviz.plugins.viewers - :no-inheritance-diagram: - -.. automodapi:: jdaviz.configs.imviz.plugins.viewers - :no-inheritance-diagram: - -.. automodapi:: jdaviz.configs.mosviz.plugins.viewers - :no-inheritance-diagram: +.. toctree:: + :maxdepth: 2 -.. automodapi:: jdaviz.configs.specviz.plugins.viewers - :no-inheritance-diagram: - -.. _jdaviz-api-parsers: + api_viewers Parsers ======= -.. automodapi:: jdaviz.configs.cubeviz.plugins.parsers - :no-inheritance-diagram: - -.. automodapi:: jdaviz.configs.imviz.plugins.parsers - :no-inheritance-diagram: - -.. automodapi:: jdaviz.configs.mosviz.plugins.parsers - :no-inheritance-diagram: - -.. automodapi:: jdaviz.configs.specviz.plugins.parsers - :no-inheritance-diagram: +.. toctree:: + :maxdepth: 2 -.. automodapi:: jdaviz.configs.specviz2d.plugins.parsers - :no-inheritance-diagram: - -.. _jdaviz-api-plugins: + api_parsers Plugins ======= -.. automodapi:: jdaviz.configs.default.plugins.collapse.collapse - :no-inheritance-diagram: - -.. automodapi:: jdaviz.configs.default.plugins.data_tools.data_tools - :no-inheritance-diagram: - -.. automodapi:: jdaviz.configs.default.plugins.export_plot.export_plot - :no-inheritance-diagram: - -.. automodapi:: jdaviz.configs.default.plugins.gaussian_smooth.gaussian_smooth - :no-inheritance-diagram: - -.. automodapi:: jdaviz.configs.default.plugins.line_lists.line_lists - :no-inheritance-diagram: - -.. automodapi:: jdaviz.configs.default.plugins.markers.markers - :no-inheritance-diagram: - -.. automodapi:: jdaviz.configs.default.plugins.metadata_viewer.metadata_viewer - :no-inheritance-diagram: - -.. automodapi:: jdaviz.configs.default.plugins.model_fitting.model_fitting - :no-inheritance-diagram: - -.. automodapi:: jdaviz.configs.default.plugins.plot_options.plot_options - :no-inheritance-diagram: - -.. automodapi:: jdaviz.configs.default.plugins.subset_plugin.subset_plugin - :no-inheritance-diagram: - -.. automodapi:: jdaviz.configs.default.plugins.subset_tools.subset_tools - :no-inheritance-diagram: - -.. automodapi:: jdaviz.configs.default.plugins.viewer_creator.viewer_creator - :no-inheritance-diagram: - -.. automodapi:: jdaviz.configs.cubeviz.plugins.moment_maps.moment_maps - :no-inheritance-diagram: - -.. automodapi:: jdaviz.configs.cubeviz.plugins.slice.slice - :no-inheritance-diagram: - -.. automodapi:: jdaviz.configs.imviz.plugins.aper_phot_simple.aper_phot_simple - :no-inheritance-diagram: - -.. automodapi:: jdaviz.configs.imviz.plugins.catalogs.catalogs - :no-inheritance-diagram: - -.. automodapi:: jdaviz.configs.imviz.plugins.compass.compass - :no-inheritance-diagram: +.. toctree:: + :maxdepth: 2 -.. automodapi:: jdaviz.configs.imviz.plugins.coords_info.coords_info - :no-inheritance-diagram: - -.. automodapi:: jdaviz.configs.imviz.plugins.image_viewer_creator.image_viewer_creator - :no-inheritance-diagram: - -.. automodapi:: jdaviz.configs.imviz.plugins.line_profile_xy.line_profile_xy - :no-inheritance-diagram: - -.. automodapi:: jdaviz.configs.imviz.plugins.links_control.links_control - :no-inheritance-diagram: - -.. automodapi:: jdaviz.configs.imviz.plugins.rotate_canvas.rotate_canvas - :no-inheritance-diagram: - -.. automodapi:: jdaviz.configs.mosviz.plugins.row_lock.row_lock - :no-inheritance-diagram: - -.. automodapi:: jdaviz.configs.mosviz.plugins.slit_overlay.slit_overlay - :no-inheritance-diagram: - -.. automodapi:: jdaviz.configs.specviz.plugins.line_analysis.line_analysis - :no-inheritance-diagram: - -.. automodapi:: jdaviz.configs.specviz.plugins.unit_conversion.unit_conversion - :no-inheritance-diagram: - -.. automodapi:: jdaviz.configs.specviz2d.plugins.spectral_extraction.spectral_extraction - :no-inheritance-diagram: - -.. _jdaviz-api-nuts-bolts: + api_plugins Nuts and Bolts ============== -.. automodapi:: jdaviz.app - :no-inheritance-diagram: - :no-inherited-members: - -.. automodapi:: jdaviz.configs.default.plugins.data_tools.file_chooser - :no-inheritance-diagram: - :no-inherited-members: - -.. automodapi:: jdaviz.configs.default.plugins.line_lists.line_list_mixin - :no-inheritance-diagram: - :no-inherited-members: - -.. automodapi:: jdaviz.configs.default.plugins.model_fitting.fitting_backend - :no-inheritance-diagram: - :no-inherited-members: - -.. automodapi:: jdaviz.configs.default.plugins.model_fitting.initializers - :no-inheritance-diagram: - :no-inherited-members: - -.. automodapi:: jdaviz.configs.imviz.wcs_utils - :no-inheritance-diagram: - :no-inherited-members: - -.. automodapi:: jdaviz.core.astrowidgets_api - :no-inheritance-diagram: - :no-inherited-members: - -.. automodapi:: jdaviz.core.config - :no-inheritance-diagram: - :no-inherited-members: - -.. automodapi:: jdaviz.core.custom_traitlets - :no-inheritance-diagram: - :no-inherited-members: - -.. automodapi:: jdaviz.core.data_formats - :no-inheritance-diagram: - :no-inherited-members: - -.. automodapi:: jdaviz.core.events - :no-inheritance-diagram: - :no-inherited-members: - -.. automodapi:: jdaviz.core.freezable_state - :no-inheritance-diagram: - :no-inherited-members: - -.. automodapi:: jdaviz.core.helpers - :no-inheritance-diagram: - :no-inherited-members: - -.. automodapi:: jdaviz.core.linelists - :no-inheritance-diagram: - :no-inherited-members: - -.. automodapi:: jdaviz.core.marks - :no-inheritance-diagram: - :no-inherited-members: - -.. automodapi:: jdaviz.core.region_translators - :no-inheritance-diagram: - :no-inherited-members: - -.. automodapi:: jdaviz.core.registries - :no-inheritance-diagram: - :no-inherited-members: - -.. automodapi:: jdaviz.core.template_mixin - :no-inheritance-diagram: - :no-inherited-members: - -.. automodapi:: jdaviz.core.validunits - :no-inheritance-diagram: - :no-inherited-members: - -.. automodapi:: jdaviz.models.physical_models - :no-inherited-members: +.. toctree:: + :maxdepth: 2 -.. automodapi:: jdaviz.utils - :no-inheritance-diagram: - :no-inherited-members: + api_nuts_bolts diff --git a/docs/reference/api_configs.rst b/docs/reference/api_configs.rst new file mode 100644 index 0000000000..c5826d2ba4 --- /dev/null +++ b/docs/reference/api_configs.rst @@ -0,0 +1,22 @@ +.. _jdaviz-api-configs: + +Helpers API +=========== + +.. toctree:: + :maxdepth: 1 + +.. automodapi:: jdaviz.configs.cubeviz.helper + :no-inheritance-diagram: + +.. automodapi:: jdaviz.configs.imviz.helper + :no-inheritance-diagram: + +.. automodapi:: jdaviz.configs.mosviz.helper + :no-inheritance-diagram: + +.. automodapi:: jdaviz.configs.specviz.helper + :no-inheritance-diagram: + +.. automodapi:: jdaviz.configs.specviz2d.helper + :no-inheritance-diagram: diff --git a/docs/reference/api_nuts_bolts.rst b/docs/reference/api_nuts_bolts.rst new file mode 100644 index 0000000000..3e376a7808 --- /dev/null +++ b/docs/reference/api_nuts_bolts.rst @@ -0,0 +1,87 @@ +.. _jdaviz-api-nuts-bolts: + +Utilities API +============= + +.. automodapi:: jdaviz.app + :no-inheritance-diagram: + :no-inherited-members: + +.. automodapi:: jdaviz.configs.default.plugins.data_tools.file_chooser + :no-inheritance-diagram: + :no-inherited-members: + +.. automodapi:: jdaviz.configs.default.plugins.line_lists.line_list_mixin + :no-inheritance-diagram: + :no-inherited-members: + +.. automodapi:: jdaviz.configs.default.plugins.model_fitting.fitting_backend + :no-inheritance-diagram: + :no-inherited-members: + +.. automodapi:: jdaviz.configs.default.plugins.model_fitting.initializers + :no-inheritance-diagram: + :no-inherited-members: + +.. automodapi:: jdaviz.configs.imviz.wcs_utils + :no-inheritance-diagram: + :no-inherited-members: + +.. automodapi:: jdaviz.core.astrowidgets_api + :no-inheritance-diagram: + :no-inherited-members: + +.. automodapi:: jdaviz.core.config + :no-inheritance-diagram: + :no-inherited-members: + +.. automodapi:: jdaviz.core.custom_traitlets + :no-inheritance-diagram: + :no-inherited-members: + +.. automodapi:: jdaviz.core.data_formats + :no-inheritance-diagram: + :no-inherited-members: + +.. automodapi:: jdaviz.core.events + :no-inheritance-diagram: + :no-inherited-members: + +.. automodapi:: jdaviz.core.freezable_state + :no-inheritance-diagram: + :no-inherited-members: + +.. automodapi:: jdaviz.core.helpers + :no-inheritance-diagram: + :no-inherited-members: + +.. automodapi:: jdaviz.core.linelists + :no-inheritance-diagram: + :no-inherited-members: + +.. automodapi:: jdaviz.core.marks + :no-inheritance-diagram: + :no-inherited-members: + +.. automodapi:: jdaviz.core.region_translators + :no-inheritance-diagram: + :no-inherited-members: + +.. automodapi:: jdaviz.core.registries + :no-inheritance-diagram: + :no-inherited-members: + +.. automodapi:: jdaviz.core.template_mixin + :no-inheritance-diagram: + :no-inherited-members: + +.. automodapi:: jdaviz.core.validunits + :no-inheritance-diagram: + :no-inherited-members: + +.. automodapi:: jdaviz.models.physical_models + :no-inherited-members: + +.. automodapi:: jdaviz.utils + :no-inheritance-diagram: + :no-inherited-members: diff --git a/docs/reference/api_parsers.rst b/docs/reference/api_parsers.rst new file mode 100644 index 0000000000..9272aa20c3 --- /dev/null +++ b/docs/reference/api_parsers.rst @@ -0,0 +1,19 @@ +.. _jdaviz-api-parsers: + +Parsers API +=========== + +.. automodapi:: jdaviz.configs.cubeviz.plugins.parsers + :no-inheritance-diagram: + +.. automodapi:: jdaviz.configs.imviz.plugins.parsers + :no-inheritance-diagram: + +.. automodapi:: jdaviz.configs.mosviz.plugins.parsers + :no-inheritance-diagram: + +.. automodapi:: jdaviz.configs.specviz.plugins.parsers + :no-inheritance-diagram: + +.. automodapi:: jdaviz.configs.specviz2d.plugins.parsers + :no-inheritance-diagram: diff --git a/docs/reference/api_plugins.rst b/docs/reference/api_plugins.rst new file mode 100644 index 0000000000..85c87cb271 --- /dev/null +++ b/docs/reference/api_plugins.rst @@ -0,0 +1,85 @@ +.. _jdaviz-api-plugins: + +Plugins API +=========== + +.. automodapi:: jdaviz.configs.default.plugins.collapse.collapse + :no-inheritance-diagram: + +.. automodapi:: jdaviz.configs.default.plugins.data_tools.data_tools + :no-inheritance-diagram: + +.. automodapi:: jdaviz.configs.default.plugins.export_plot.export_plot + :no-inheritance-diagram: + +.. automodapi:: jdaviz.configs.default.plugins.gaussian_smooth.gaussian_smooth + :no-inheritance-diagram: + +.. automodapi:: jdaviz.configs.default.plugins.line_lists.line_lists + :no-inheritance-diagram: + +.. automodapi:: jdaviz.configs.default.plugins.markers.markers + :no-inheritance-diagram: + +.. automodapi:: jdaviz.configs.default.plugins.metadata_viewer.metadata_viewer + :no-inheritance-diagram: + +.. automodapi:: jdaviz.configs.default.plugins.model_fitting.model_fitting + :no-inheritance-diagram: + +.. automodapi:: jdaviz.configs.default.plugins.plot_options.plot_options + :no-inheritance-diagram: + +.. automodapi:: jdaviz.configs.default.plugins.subset_plugin.subset_plugin + :no-inheritance-diagram: + +.. automodapi:: jdaviz.configs.default.plugins.subset_tools.subset_tools + :no-inheritance-diagram: + +.. automodapi:: jdaviz.configs.default.plugins.viewer_creator.viewer_creator + :no-inheritance-diagram: + +.. automodapi:: jdaviz.configs.cubeviz.plugins.moment_maps.moment_maps + :no-inheritance-diagram: + +.. automodapi:: jdaviz.configs.cubeviz.plugins.slice.slice + :no-inheritance-diagram: + +.. automodapi:: jdaviz.configs.imviz.plugins.aper_phot_simple.aper_phot_simple + :no-inheritance-diagram: + +.. automodapi:: jdaviz.configs.imviz.plugins.catalogs.catalogs + :no-inheritance-diagram: + +.. automodapi:: jdaviz.configs.imviz.plugins.compass.compass + :no-inheritance-diagram: + +.. automodapi:: jdaviz.configs.imviz.plugins.coords_info.coords_info + :no-inheritance-diagram: + +.. automodapi:: jdaviz.configs.imviz.plugins.image_viewer_creator.image_viewer_creator + :no-inheritance-diagram: + +.. automodapi:: jdaviz.configs.imviz.plugins.line_profile_xy.line_profile_xy + :no-inheritance-diagram: + +.. automodapi:: jdaviz.configs.imviz.plugins.links_control.links_control + :no-inheritance-diagram: + +.. automodapi:: jdaviz.configs.imviz.plugins.rotate_canvas.rotate_canvas + :no-inheritance-diagram: + +.. automodapi:: jdaviz.configs.mosviz.plugins.row_lock.row_lock + :no-inheritance-diagram: + +.. automodapi:: jdaviz.configs.mosviz.plugins.slit_overlay.slit_overlay + :no-inheritance-diagram: + +.. automodapi:: jdaviz.configs.specviz.plugins.line_analysis.line_analysis + :no-inheritance-diagram: + +.. automodapi:: jdaviz.configs.specviz.plugins.unit_conversion.unit_conversion + :no-inheritance-diagram: + +.. automodapi:: jdaviz.configs.specviz2d.plugins.spectral_extraction.spectral_extraction + :no-inheritance-diagram: diff --git a/docs/reference/api_viewers.rst b/docs/reference/api_viewers.rst new file mode 100644 index 0000000000..5908516f5c --- /dev/null +++ b/docs/reference/api_viewers.rst @@ -0,0 +1,19 @@ +.. _jdaviz-api-viewers: + +Viewers API +=========== + +.. automodapi:: jdaviz.configs.default.plugins.viewers + :no-inheritance-diagram: + +.. automodapi:: jdaviz.configs.cubeviz.plugins.viewers + :no-inheritance-diagram: + +.. automodapi:: jdaviz.configs.imviz.plugins.viewers + :no-inheritance-diagram: + +.. automodapi:: jdaviz.configs.mosviz.plugins.viewers + :no-inheritance-diagram: + +.. automodapi:: jdaviz.configs.specviz.plugins.viewers + :no-inheritance-diagram: diff --git a/docs/specviz/examples.rst b/docs/specviz/examples.rst index 5213109338..f0ac712a37 100644 --- a/docs/specviz/examples.rst +++ b/docs/specviz/examples.rst @@ -18,18 +18,18 @@ Open a Spectrum .. raw:: html - + Model Fitting ============= .. raw:: html - + Line Analysis ============= .. raw:: html - + diff --git a/docs/specviz/export_data.rst b/docs/specviz/export_data.rst index f87e74fca2..e6d322acb6 100644 --- a/docs/specviz/export_data.rst +++ b/docs/specviz/export_data.rst @@ -33,7 +33,7 @@ To extract a spectrum with a spectral subset applied: .. code-block:: python - specviz.get_data(subset_to_apply='Subset 1') + specviz.get_data(spectral_subset='Subset 1') In this case, the returned `specutils.Spectrum1D` object will have a ``mask`` attribute, where ``True`` corresponds to the region outside the selected subset @@ -42,7 +42,7 @@ spectrum containing only your subset by running: .. code-block:: python - spec = specviz.get_data(subset_to_apply='Subset 1') + spec = specviz.get_data(spectral_subset='Subset 1') subset_spec = Spectrum1D(flux=spec.flux[~spec.mask], spectral_axis=spec.spectral_axis[~spec.mask]) specviz.load_spectrum(subset_spec) diff --git a/docs/specviz/import_data.rst b/docs/specviz/import_data.rst index c5623ddfbf..e30aa19f02 100644 --- a/docs/specviz/import_data.rst +++ b/docs/specviz/import_data.rst @@ -9,6 +9,7 @@ as that allows the Python-level interface and parsing tools to be defined in ``s instead of being duplicated in Jdaviz. :class:`~specutils.Spectrum1D` objects are very flexible in their capabilities, however, and hence should address most astronomical spectrum use cases. +If you are creating your own data products, please read the page :ref:`create_products`. .. seealso:: diff --git a/docs/specviz/index.rst b/docs/specviz/index.rst index 6e6c6212fa..aa656c7b75 100644 --- a/docs/specviz/index.rst +++ b/docs/specviz/index.rst @@ -1,11 +1,11 @@ -.. image:: ../logos/specviz.svg - :width: 400 +.. |specviz_logo| image:: ../logos/specicon.svg + :height: 42px .. _specviz: -####### -Specviz -####### +###################### +|specviz_logo| Specviz +###################### .. image:: https://stsci.box.com/shared/static/qlrlsf12fl9v9wjy321hwrjk8d9jf5h8.gif :alt: Introductory video tour of the Specviz configuration and its features diff --git a/docs/specviz/plugins.rst b/docs/specviz/plugins.rst index e6ed909749..4fccafb30c 100644 --- a/docs/specviz/plugins.rst +++ b/docs/specviz/plugins.rst @@ -118,15 +118,8 @@ To export the table into the notebook via the API, call Unit Conversion =============== -.. note:: - - This plugin is temporarily disabled. Effort to improve it is being - tracked at https://github.com/spacetelescope/jdaviz/issues/1972 . - The spectral flux density and spectral axis units can be converted -using the Unit Conversion plugin. The Spectrum1D object to be -converted is the currently selected spectrum in the spectrum viewer :guilabel:`Data` -icon in the viewer toolbar. +using the Unit Conversion plugin. Select the frequency, wavelength, or energy unit in the :guilabel:`New Spectral Axis Unit` pulldown @@ -135,11 +128,8 @@ Select the frequency, wavelength, or energy unit in the Select the flux density unit in the :guilabel:`New Flux Unit` pulldown (e.g., Jansky, W/(Hz/m2), ph/(Angstrom cm2 s)). -The :guilabel:`Apply` button will convert the flux density and/or -spectral axis units and create a new Spectrum1D object that -is automatically switched to in the spectrum viewer. -The name of the new Spectrum1D object is "_units_copy_" plus -the flux and spectral units of the spectrum. +Note that this affects the default units in all viewers and plugins, where applicable, +but does not affect the underlying data. .. _line-lists: diff --git a/docs/specviz2d/import_data.rst b/docs/specviz2d/import_data.rst index 0c9fc6860a..e3501e70a3 100644 --- a/docs/specviz2d/import_data.rst +++ b/docs/specviz2d/import_data.rst @@ -9,6 +9,7 @@ as that allows the Python-level interface and parsing tools to be defined in ``s instead of being duplicated in Jdaviz. :class:`~specutils.Spectrum1D` objects are very flexible in their capabilities, however, and hence should address most astronomical spectrum use cases. +If you are creating your own data products, please read the page :ref:`create_products`. .. seealso:: @@ -56,7 +57,7 @@ notebook can access the Specviz2D helper class API. Using this API, users can load data into the application through code with the :meth:`~jdaviz.configs.specviz2d.helper.Specviz2d.load_data` method, which takes as input a :class:`~specutils.Spectrum1D` object or filename for the -2D spectrum and (optionally) the 1D spectrum. +2D spectrum and (optionally) the 1D spectrum. By default, extension 1 of the 2D file is loaded, but you can specify another extension by providing an integer @@ -66,4 +67,4 @@ the spectrum to be horizontal: .. code-block:: python - specviz2d.load_data(fn, ext=7, transpose=True) \ No newline at end of file + specviz2d.load_data(fn, ext=7, transpose=True) diff --git a/docs/specviz2d/index.rst b/docs/specviz2d/index.rst index 4d5df1f3f8..87789e915e 100644 --- a/docs/specviz2d/index.rst +++ b/docs/specviz2d/index.rst @@ -1,11 +1,11 @@ -.. image:: ../logos/specviz2d.svg - :width: 400 +.. |specviz2d_logo| image:: ../logos/specviz2d\ icon.svg + :height: 42px .. _specviz2d: -######### -Specviz2D -######### +########################## +|specviz2d_logo| Specviz2D +########################## Specviz2d is a tool for visualization and quick-look analysis of 2D astronomical spectra. It incorporates visualization tools with analysis capabilities, diff --git a/jdaviz/__init__.py b/jdaviz/__init__.py index 716678364f..fdddfd2e84 100644 --- a/jdaviz/__init__.py +++ b/jdaviz/__init__.py @@ -19,6 +19,7 @@ from jdaviz.configs.cubeviz import Cubeviz # noqa: F401 from jdaviz.configs.imviz import Imviz # noqa: F401 from jdaviz.utils import enable_hot_reloading # noqa: F401 +from jdaviz.core.data_formats import open # noqa: F401 # Clean up namespace. del os diff --git a/jdaviz/app.py b/jdaviz/app.py index d2d3d9c217..08d0e65fdf 100644 --- a/jdaviz/app.py +++ b/jdaviz/app.py @@ -9,9 +9,9 @@ from ipywidgets import widget_serialization import ipyvue +from astropy import units as u from astropy.nddata import CCDData, NDData from astropy.io import fits -from astropy import units as u from astropy.coordinates import Angle from astropy.time import Time from regions import PixCoord, CirclePixelRegion, RectanglePixelRegion, EllipsePixelRegion @@ -28,7 +28,7 @@ from glue.config import colormaps, data_translator from glue.config import settings as glue_settings from glue.core import BaseData, HubListener, Data, DataCollection -from glue.core.link_helpers import LinkSame +from glue.core.link_helpers import LinkSame, LinkSameWithUnits from glue.plugins.wcs_autolinking.wcs_autolinking import WCSLink, IncompatibleWCS from glue.core.message import (DataCollectionAddMessage, DataCollectionDeleteMessage, @@ -39,6 +39,7 @@ from glue.core.subset import (Subset, RangeSubsetState, RoiSubsetState, CompositeSubsetState, InvertState) from glue.core.roi import CircularROI, EllipticalROI, RectangularROI +from glue.core.units import unit_converter from glue_astronomy.spectral_coordinates import SpectralCoordinates from glue_jupyter.app import JupyterApplication from glue_jupyter.common.toolbar_vuetify import read_icon @@ -70,10 +71,57 @@ mask=['mask', 'dq']) +@unit_converter('custom-jdaviz') +class UnitConverterWithSpectral: + + def equivalent_units(self, data, cid, units): + if cid.label == "flux": + eqv = u.spectral_density(1 * u.m) # Value does not matter here. + list_of_units = set(list(map(str, u.Unit(units).find_equivalent_units( + include_prefix_units=True, equivalencies=eqv))) + [ + 'Jy', 'mJy', 'uJy', + 'W / (m2 Hz)', 'W / (Hz m2)', # Order is different in astropy v5.3 + 'eV / (s m2 Hz)', 'eV / (Hz s m2)', + 'erg / (s cm2)', + 'erg / (s cm2 Angstrom)', 'erg / (Angstrom s cm2)', + 'erg / (s cm2 Hz)', 'erg / (Hz s cm2)', + 'ph / (s cm2 Angstrom)', 'ph / (Angstrom s cm2)', + 'ph / (s cm2 Hz)', 'ph / (Hz s cm2)' + ]) + else: # spectral axis + # prefer Hz over Bq and um over micron + exclude = {'Bq', 'micron'} + list_of_units = set(list(map(str, u.Unit(units).find_equivalent_units( + include_prefix_units=True, equivalencies=u.spectral())))) - exclude + return list_of_units + + def to_unit(self, data, cid, values, original_units, target_units): + # Given a glue data object (data), a component ID (cid), the values + # to convert, and the original and target units of the values, this method + # should return the converted values. Note that original_units + # gives the units of the values array, which might not be the same + # as the original native units of the component in the data. + if cid.label == "flux": + spec = data.get_object(cls=Spectrum1D) + if len(values) == 2: + # Need this for setting the y-limits + spec_limits = [spec.spectral_axis[0].value, spec.spectral_axis[-1].value] + eqv = u.spectral_density(spec_limits*spec.spectral_axis.unit) + else: + eqv = u.spectral_density(spec.spectral_axis) + else: # spectral axis + eqv = u.spectral() + + return (values * u.Unit(original_units)).to_value(u.Unit(target_units), equivalencies=eqv) + + # Set default opacity for data layers to 1 instead of 0.8 in # some glue-core versions glue_settings.DATA_ALPHA = 1 +# Enable spectrum unit conversion. +glue_settings.UNIT_CONVERTER = 'custom-jdaviz' + custom_components = {'j-tooltip': 'components/tooltip.vue', 'j-external-link': 'components/external_link.vue', 'j-docs-link': 'components/docs_link.vue', @@ -466,7 +514,7 @@ def _link_new_data(self, reference_data=None, data_to_be_linked=None): if isinstance(linked_data.coords, SpectralCoordinates): wc_old = ref_data.world_component_ids[-1] wc_new = linked_data.world_component_ids[0] - self.data_collection.add_link(LinkSame(wc_old, wc_new)) + self.data_collection.add_link(LinkSameWithUnits(wc_old, wc_new)) return try: @@ -512,8 +560,8 @@ def _link_new_data(self, reference_data=None, data_to_be_linked=None): else: continue - links.append(LinkSame(ref_data.pixel_component_ids[ref_index], - linked_data.pixel_component_ids[linked_index])) + links.append(LinkSameWithUnits(ref_data.pixel_component_ids[ref_index], + linked_data.pixel_component_ids[linked_index])) dc.add_link(links) @@ -852,7 +900,8 @@ def get_subsets_from_viewer(self, viewer_reference, data_label=None, subset_type return regions def get_subsets(self, subset_name=None, spectral_only=False, - spatial_only=False, object_only=False, simplify_spectral=True): + spatial_only=False, object_only=False, simplify_spectral=True, + use_display_units=False): """ Returns all branches of glue subset tree in the form that subset plugin can recognize. @@ -869,6 +918,9 @@ def get_subsets(self, subset_name=None, spectral_only=False, leave out the region class name and glue_state. simplify_spectral : bool Return a composite spectral subset collapsed to a simplified SpectralRegion. + use_display_units: bool, optional + Whether to convert to the display units defined in the + :ref:`Unit Conversion ` plugin. Returns ------- @@ -887,7 +939,8 @@ def get_subsets(self, subset_name=None, spectral_only=False, if isinstance(subset.subset_state, CompositeSubsetState): # Region composed of multiple ROI or Range subset # objects that must be traversed - subset_region = self.get_sub_regions(subset.subset_state, simplify_spectral) + subset_region = self.get_sub_regions(subset.subset_state, + simplify_spectral, use_display_units) elif isinstance(subset.subset_state, RoiSubsetState): # 3D regions represented as a dict including an # AstropyRegion object if possible @@ -895,7 +948,8 @@ def get_subsets(self, subset_name=None, spectral_only=False, elif isinstance(subset.subset_state, RangeSubsetState): # 2D regions represented as SpectralRegion objects subset_region = self._get_range_subset_bounds(subset.subset_state, - simplify_spectral) + simplify_spectral, + use_display_units) else: # subset.subset_state can be an instance of MaskSubsetState # or something else we do not know how to handle @@ -966,19 +1020,25 @@ def _remove_duplicate_bounds(self, spec_regions): regions_no_dups += region return regions_no_dups - def _get_range_subset_bounds(self, subset_state, simplify_spectral=True): + def _get_range_subset_bounds(self, subset_state, + simplify_spectral=True, use_display_units=False): # TODO: Use global display units # units = dc[0].data.coords.spectral_axis.unit viewer = self.get_viewer(self._jdaviz_helper. _default_spectrum_viewer_reference_name) data = viewer.data() - if viewer: - units = u.Unit(viewer.state.x_display_unit) - elif data and len(data) > 0 and isinstance(data[0], Spectrum1D): + if data and len(data) > 0 and isinstance(data[0], Spectrum1D): units = data[0].spectral_axis.unit else: raise ValueError("Unable to find spectral axis units") + if use_display_units: + # converting may result in flipping order (wavelength <-> frequency) + ret_units = self._get_display_unit('spectral') + subset_bounds = [(subset_state.lo * units).to(ret_units, u.spectral()), + (subset_state.hi * units).to(ret_units, u.spectral())] + spec_region = SpectralRegion(min(subset_bounds), max(subset_bounds)) + else: + spec_region = SpectralRegion(subset_state.lo * units, subset_state.hi * units) - spec_region = SpectralRegion(subset_state.lo * units, subset_state.hi * units) if not simplify_spectral: return [{"name": subset_state.__class__.__name__, "glue_state": subset_state.__class__.__name__, @@ -1006,19 +1066,21 @@ def _get_roi_subset_definition(self, subset_state): rx = roi.radius_x ry = roi.radius_y theta = np.around(np.degrees(roi.theta), decimals=_around_decimals) - roi_as_region = EllipsePixelRegion(PixCoord(xc, yc), rx, ry, Angle(theta, "deg")) + roi_as_region = EllipsePixelRegion(PixCoord(xc, yc), rx * 2, ry * 2, Angle(theta, "deg")) # noqa: E501 return [{"name": subset_state.roi.__class__.__name__, "glue_state": subset_state.__class__.__name__, "region": roi_as_region, "subset_state": subset_state}] - def get_sub_regions(self, subset_state, simplify_spectral=True): + def get_sub_regions(self, subset_state, simplify_spectral=True, use_display_units=False): if isinstance(subset_state, CompositeSubsetState): if subset_state and hasattr(subset_state, "state2") and subset_state.state2: - one = self.get_sub_regions(subset_state.state1, simplify_spectral) - two = self.get_sub_regions(subset_state.state2, simplify_spectral) + one = self.get_sub_regions(subset_state.state1, + simplify_spectral, use_display_units) + two = self.get_sub_regions(subset_state.state2, + simplify_spectral, use_display_units) if isinstance(one, list) and "glue_state" in one[0]: one[0]["glue_state"] = subset_state.__class__.__name__ @@ -1106,18 +1168,39 @@ def get_sub_regions(self, subset_state, simplify_spectral=True): # (4.0 um, 4.5 um) (5.0 um, 6.0 um) (9.0 um, 12.0 um) if isinstance(two, SpectralRegion): - if one.lower > two.lower: - # If one.lower is less than two.lower, it will be included - # in the two.invert() call. Otherwise, we can add it like this. - return (two.invert(one.lower, one.upper) + - one.invert(two.lower, two.upper)) - return two.invert(one.lower, one.upper) + new_region = None + temp_region = None + for subregion in two: + # Add all subregions that do not intersect with XOR region + # to a new SpectralRegion object + if subregion.lower > one.upper or subregion.upper < one.lower: + if not new_region: + new_region = subregion + else: + new_region += subregion + # All other subregions are added to temp_region + else: + if not temp_region: + temp_region = subregion + else: + temp_region += subregion + # This is the main application of XOR to other regions + if not new_region: + new_region = temp_region.invert(one.lower, one.upper) + else: + new_region = new_region + temp_region.invert(one.lower, one.upper) + # This adds the edge regions that are otherwise not included + if not (one.lower == temp_region.lower and one.upper == temp_region.upper): + new_region = new_region + one.invert(temp_region.lower, + temp_region.upper) + return new_region else: return two + one else: # This gets triggered in the InvertState case where state1 # is an object and state2 is None - return self.get_sub_regions(subset_state.state1, simplify_spectral) + return self.get_sub_regions(subset_state.state1, + simplify_spectral, use_display_units) elif subset_state is not None: # This is the leaf node of the glue subset state tree where # a subset_state is either ROI or Range. @@ -1125,7 +1208,61 @@ def get_sub_regions(self, subset_state, simplify_spectral=True): return self._get_roi_subset_definition(subset_state) elif isinstance(subset_state, RangeSubsetState): - return self._get_range_subset_bounds(subset_state, simplify_spectral) + return self._get_range_subset_bounds(subset_state, + simplify_spectral, use_display_units) + + def _get_display_unit(self, axis): + if self._jdaviz_helper is None or self._jdaviz_helper.plugins.get('Unit Conversion') is None: # noqa + # fallback on native units (unit conversion is not enabled) + if axis == 'spectral': + sv = self.get_viewer(self._jdaviz_helper._default_spectrum_viewer_reference_name) + return sv.data()[0].spectral_axis.unit + elif axis == 'flux': + sv = self.get_viewer(self._jdaviz_helper._default_spectrum_viewer_reference_name) + return sv.data()[0].flux.unit + else: + raise ValueError(f"could not find units for axis='{axis}'") + try: + return getattr(self._jdaviz_helper.plugins.get('Unit Conversion')._obj, + f'{axis}_unit_selected') + except AttributeError: + raise ValueError(f"could not find display unit for axis='{axis}'") + + def simplify_spectral_subset(self, subset_name, att, overwrite=False): + """ + Convert a composite spectral subset consisting of And, AndNot, Or, Replace, and Xor + into one consisting of just Range and Or state objects. + + Parameters + ---------- + subset_name : str + Name of subset to simplify. + att : str + Attribute that the subset uses to apply to data. + overwrite : bool + Whether to update the current subset with the simplified state or apply it + to a new subset. + """ + spectral_region = self.get_subsets(subset_name, spectral_only=True) + new_state = None + # Reverse through sub regions so that they are added back + # in the order of lowest values to highest + for index in range(len(spectral_region) - 1, -1, -1): + convert_to_range = RangeSubsetState(spectral_region[index].lower.value, + spectral_region[index].upper.value, + att) + if new_state is None: + new_state = convert_to_range + else: + new_state = new_state | convert_to_range + + dc = self.data_collection + if not overwrite: + dc.new_subset_group(subset_state=new_state) + else: + old_subset = [subsets for subsets in dc.subset_groups + if subsets.label == subset_name][0] + old_subset.subset_state = new_state def add_data(self, data, data_label=None, notify_done=True): """ @@ -1307,19 +1444,6 @@ def add_data_to_viewer(self, viewer_reference, data_label, self.set_data_visibility(viewer_item, data_label, visible=visible, replace=clear_other_data) - def _set_plot_axes_labels(self, viewer_id): - """ - Sets the plot axes labels to be the units of the data to be loaded. - - Parameters - ---------- - viewer_id : str - The UUID associated with the desired viewer item. - """ - viewer = self._viewer_by_id(viewer_id) - - viewer.set_plot_axes() - def remove_data_from_viewer(self, viewer_reference, data_label): """ Removes a data set from the specified viewer. @@ -1660,6 +1784,7 @@ def set_data_visibility(self, viewer_reference, data_label, visible=True, replac [data] = [x for x in self.data_collection if x.label == data_label] viewer.add_data(data, percentile=95, color=viewer.color_cycler()) + viewer.set_plot_axes() add_data_message = AddDataMessage(data, viewer, viewer_id=viewer_id, @@ -1695,11 +1820,8 @@ def set_data_visibility(self, viewer_reference, data_label, visible=True, replac # active data. viewer_data_labels = [layer.layer.label for layer in viewer.layers] if len(viewer_data_labels) > 0 and getattr(self._jdaviz_helper, '_in_batch_load', 0) == 0: - active_data = self.data_collection[viewer_data_labels[0]] - if (hasattr(active_data, "_preferred_translation") - and active_data._preferred_translation is not None): - self._set_plot_axes_labels(viewer_id) - + # This "if" is nested on purpose to make parent "if" available + # for other configs in the future, as needed. if self.config == 'imviz': viewer.on_limits_change() # Trigger compass redraw diff --git a/jdaviz/cli.py b/jdaviz/cli.py index 2c673da2b2..4f569e7f00 100644 --- a/jdaviz/cli.py +++ b/jdaviz/cli.py @@ -14,10 +14,12 @@ __all__ = ['main'] JDAVIZ_DIR = pathlib.Path(__file__).parent.resolve() +DEFAULT_VERBOSITY = 'warning' +DEFAULT_HISTORY_VERBOSITY = 'info' def main(filepaths=None, layout='default', instrument=None, browser='default', - theme='light', verbosity='warning', history_verbosity='info', + theme='light', verbosity=DEFAULT_VERBOSITY, history_verbosity=DEFAULT_HISTORY_VERBOSITY, hotreload=False): """ Start a Jdaviz application instance with data loaded from FILENAME. diff --git a/jdaviz/components/plugin_add_results.vue b/jdaviz/components/plugin_add_results.vue index 08451089ae..2660f84d7d 100644 --- a/jdaviz/components/plugin_add_results.vue +++ b/jdaviz/components/plugin_add_results.vue @@ -12,14 +12,24 @@ :hint="label_hint ? label_hint : 'Label for the resulting data item.'" > - +
+ + + +
- - {{ item.id }} model component not in equation + + + {{ item.id }} is inconsistent with the current display units so cannot be used in the model equation. + Create a new model component or re-estimate the free parameters based on the current display units. + + + + mdi-restart + Re-estimate free parameters + + + + + + + + {{ item.id }} model component not in equation + - @@ -290,7 +314,7 @@ }, methods: { componentInEquation(componentId) { - return this.model_equation.split(/[+*\/-]/).indexOf(componentId) !== -1 + return this.model_equation.replace(/\s/g, '').split(/[+*\/-]/).indexOf(componentId) !== -1 }, roundUncertainty(uncertainty) { return uncertainty.toPrecision(2) diff --git a/jdaviz/configs/default/plugins/model_fitting/tests/test_fitting.py b/jdaviz/configs/default/plugins/model_fitting/tests/test_fitting.py index 41d418a0ec..22c059d64a 100644 --- a/jdaviz/configs/default/plugins/model_fitting/tests/test_fitting.py +++ b/jdaviz/configs/default/plugins/model_fitting/tests/test_fitting.py @@ -327,3 +327,56 @@ def test_results_table(specviz_helper, spectrum1d): 'G:mean_1:fixed', 'G:mean_1:std', 'G:stddev_1', 'G:stddev_1:unit', 'G:stddev_1:fixed', 'G:stddev_1:std'] + + +def test_equation_validation(specviz_helper, spectrum1d): + data_label = 'test' + specviz_helper.load_data(spectrum1d, data_label=data_label) + + mf = specviz_helper.plugins['Model Fitting'] + mf.create_model_component('Const1D') + mf.create_model_component('Linear1D') + + assert mf.equation == 'C+L' + assert mf._obj.model_equation_invalid_msg == '' + + mf.equation = 'L+' + assert mf._obj.model_equation_invalid_msg == 'incomplete equation.' + + mf.equation = 'L+C' + assert mf._obj.model_equation_invalid_msg == '' + + mf.equation = 'L+CC' + assert mf._obj.model_equation_invalid_msg == 'CC is not an existing model component.' + + mf.equation = 'L+CC+DD' + assert mf._obj.model_equation_invalid_msg == 'CC, DD are not existing model components.' + + mf.equation = '' + assert mf._obj.model_equation_invalid_msg == 'model equation is required.' + + +@pytest.mark.filterwarnings(r"ignore:Model is linear in parameters.*") +@pytest.mark.filterwarnings(r"ignore:The fit may be unsuccessful.*") +def test_incompatible_units(specviz_helper, spectrum1d): + data_label = 'test' + specviz_helper.load_data(spectrum1d, data_label=data_label) + + mf = specviz_helper.plugins['Model Fitting'] + mf.create_model_component('Linear1D') + + mf.add_results.label = 'model native units' + mf.calculate_fit(add_data=True) + + uc = specviz_helper.plugins['Unit Conversion'] + assert uc.spectral_unit.selected == "Angstrom" + uc.spectral_unit = u.Hz + + assert 'L is currently disabled' in mf._obj.model_equation_invalid_msg + mf.add_results.label = 'frequency units' + with pytest.raises(ValueError, match=r"model equation is invalid.*"): + mf.calculate_fit(add_data=True) + + mf.reestimate_model_parameters() + assert mf._obj.model_equation_invalid_msg == '' + mf.calculate_fit(add_data=True) diff --git a/jdaviz/configs/default/plugins/subset_plugin/subset_plugin.py b/jdaviz/configs/default/plugins/subset_plugin/subset_plugin.py index 7cae959aa7..4a505c320f 100644 --- a/jdaviz/configs/default/plugins/subset_plugin/subset_plugin.py +++ b/jdaviz/configs/default/plugins/subset_plugin/subset_plugin.py @@ -1,13 +1,19 @@ +import os + import numpy as np + +import astropy.units as u from glue.core.message import EditSubsetMessage, SubsetUpdateMessage from glue.core.edit_subset_mode import (AndMode, AndNotMode, OrMode, ReplaceMode, XorMode) from glue.core.roi import CircularROI, EllipticalROI, RectangularROI from glue.core.subset import RoiSubsetState, RangeSubsetState, CompositeSubsetState +from glue.icons import icon_path from glue_jupyter.widgets.subset_mode_vuetify import SelectionModeMenu +from glue_jupyter.common.toolbar_vuetify import read_icon from traitlets import Any, List, Unicode, Bool, observe -from jdaviz.core.events import SnackbarMessage +from jdaviz.core.events import SnackbarMessage, GlobalDisplayUnitChanged from jdaviz.core.registries import tray_registry from jdaviz.core.template_mixin import PluginTemplateMixin, DatasetSelectMixin, SubsetSelect @@ -40,6 +46,13 @@ class SubsetPlugin(PluginTemplateMixin, DatasetSelectMixin): subplugins_opened = Any().tag(sync=True) is_centerable = Bool(False).tag(sync=True) + can_simplify = Bool(False).tag(sync=True) + + icon_replace = Unicode(read_icon(os.path.join(icon_path("glue_replace", icon_format="svg")), 'svg+xml')).tag(sync=True) # noqa + icon_or = Unicode(read_icon(os.path.join(icon_path("glue_or", icon_format="svg")), 'svg+xml')).tag(sync=True) # noqa + icon_and = Unicode(read_icon(os.path.join(icon_path("glue_and", icon_format="svg")), 'svg+xml')).tag(sync=True) # noqa + icon_xor = Unicode(read_icon(os.path.join(icon_path("glue_xor", icon_format="svg")), 'svg+xml')).tag(sync=True) # noqa + icon_andnot = Unicode(read_icon(os.path.join(icon_path("glue_andnot", icon_format="svg")), 'svg+xml')).tag(sync=True) # noqa def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -52,12 +65,15 @@ def __init__(self, *args, **kwargs): handler=self._sync_selected_from_state) self.session.hub.subscribe(self, SubsetUpdateMessage, handler=self._on_subset_update) + self.session.hub.subscribe(self, GlobalDisplayUnitChanged, + handler=self._on_display_unit_changed) self.subset_select = SubsetSelect(self, 'subset_items', 'subset_selected', default_text="Create New") self.subset_states = [] + self.spectral_display_unit = None def _sync_selected_from_state(self, *args): if not hasattr(self, 'subset_select'): @@ -127,7 +143,8 @@ def _unpack_get_subsets_for_ui(self): ------- """ - subset_information = self.app.get_subsets(self.subset_selected, simplify_spectral=False) + subset_information = self.app.get_subsets(self.subset_selected, simplify_spectral=False, + use_display_units=True) _around_decimals = 6 # Avoid 30 degrees from coming back as 29.999999999999996 if not subset_information: return @@ -175,10 +192,12 @@ def _unpack_get_subsets_for_ui(self): subset_type = subset_state.roi.__class__.__name__ elif isinstance(subset_state, RangeSubsetState): - lo = subset_state.lo - hi = subset_state.hi - subset_definition = [{"name": "Lower bound", "att": "lo", "value": lo, "orig": lo}, - {"name": "Upper bound", "att": "hi", "value": hi, "orig": hi}] + lo = spec['region'].lower + hi = spec['region'].upper + subset_definition = [{"name": "Lower bound", "att": "lo", "value": lo.value, + "orig": lo.value, "unit": str(lo.unit)}, + {"name": "Upper bound", "att": "hi", "value": hi.value, + "orig": hi.value, "unit": str(hi.unit)}] subset_type = "Range" if len(subset_definition) > 0: # Note: .append() does not work for List traitlet. @@ -187,6 +206,15 @@ def _unpack_get_subsets_for_ui(self): self.glue_state_types = self.glue_state_types + [glue_state] self.subset_states = self.subset_states + [subset_state] + simplifiable_states = set(['AndState', 'XorState', 'AndNotState']) + # Check if the subset has more than one subregion, is a range subset type, and + # uses one of the states that can be simplified. + if (len(self.subset_states) > 1 and isinstance(self.subset_states[0], RangeSubsetState) + and len(simplifiable_states - set(self.glue_state_types)) < 3): + self.can_simplify = True + else: + self.can_simplify = False + def _get_subset_definition(self, *args): """ Retrieve the parameters defining the selected subset, for example the @@ -199,6 +227,23 @@ def _get_subset_definition(self, *args): self._unpack_get_subsets_for_ui() + def vue_simplify_subset(self, *args): + if len(self.subset_states) < 2: + self.hub.broadcast(SnackbarMessage("Cannot simplify spectral subset " + "of length less than 2", color='warning', + sender=self)) + return + att = self.subset_states[0].att + self.app.simplify_spectral_subset(subset_name=self.subset_selected, att=att, + overwrite=True) + + def _on_display_unit_changed(self, msg): + # We only care about the spectral units, since flux units don't affect spectral subsets + if msg.axis == "spectral": + self.spectral_display_unit = msg.unit + if self.subset_selected != self.subset_select.default_text: + self._get_subset_definition(self.subset_selected) + def vue_update_subset(self, *args): status, reason = self._check_input() if not status: @@ -215,6 +260,16 @@ def vue_update_subset(self, *args): else: d_val = float(d_att["value"]) + # Convert from display unit to original unit if necessary + if self.subset_types[index] == "Range": + if self.spectral_display_unit is not None: + x_att = sub_states.att + base_units = self.app.data_collection[0].get_component(x_att).units + if self.spectral_display_unit != base_units: + d_val = d_val*u.Unit(self.spectral_display_unit) + d_val = d_val.to(u.Unit(base_units)) + d_val = d_val.value + if float(d_att["orig"]) != d_val: if self.subset_types[index] == "Range": setattr(sub_states, d_att["att"], d_val) diff --git a/jdaviz/configs/default/plugins/subset_plugin/subset_plugin.vue b/jdaviz/configs/default/plugins/subset_plugin/subset_plugin.vue index 286adbce32..515a40723b 100644 --- a/jdaviz/configs/default/plugins/subset_plugin/subset_plugin.vue +++ b/jdaviz/configs/default/plugins/subset_plugin/subset_plugin.vue @@ -47,22 +47,51 @@
- - Subset type: {{ subset_types[index] }} + + Subregion {{ index }} + +
+ {{ subset_types[index] }} applied with +
+
+
+ + replace mode +
+
+ + and mode +
+
+ + add mode +
+
+ + remove mode +
+
+ + xor mode +
+
- - Glue state: {{ glue_state_types[index] }} - - + +
+ + Simplify + Update diff --git a/jdaviz/configs/default/plugins/viewers.py b/jdaviz/configs/default/plugins/viewers.py index de2660590f..04148dcacd 100644 --- a/jdaviz/configs/default/plugins/viewers.py +++ b/jdaviz/configs/default/plugins/viewers.py @@ -22,6 +22,7 @@ class JdavizViewerMixin: toolbar = None tools_nested = [] _prev_limits = None + _native_mark_classnames = ('Lines', 'LinesGL') def __init__(self, *args, **kwargs): # NOTE: anything here most likely won't be called by viewers because of inheritance order @@ -35,14 +36,16 @@ def native_marks(self): """ Return all marks that are Lines/LinesGL objects (and not subclasses) """ - return [m for m in self.figure.marks if m.__class__.__name__ in ['Lines', 'LinesGL']] + return [m for m in self.figure.marks + if m.__class__.__name__ in self._native_mark_classnames] @property def custom_marks(self): """ Return all marks that are not Lines/LinesGL objects (but can be subclasses) """ - return [m for m in self.figure.marks if m.__class__.__name__ not in ['Lines', 'LinesGL']] + return [m for m in self.figure.marks + if m.__class__.__name__ not in self._native_mark_classnames] def _subscribe_to_layers_update(self): # subscribe to new layers @@ -115,8 +118,6 @@ def _get_layer_info(layer): return "mdi-chart-bell-curve", "" return "", suffix - return '', '' - visible_layers = {} for layer in self.state.layers[::-1]: if layer.visible: @@ -218,6 +219,10 @@ def jdaviz_helper(self): """The Jdaviz configuration helper tied to the viewer.""" return self.jdaviz_app._jdaviz_helper + @property + def hub(self): + return self.session.hub + @property def reference_id(self): return self._reference_id @@ -225,3 +230,7 @@ def reference_id(self): @property def reference(self): return self.jdaviz_app._viewer_item_by_id(self.reference_id).get('reference') + + def set_plot_axes(self): + # individual viewers can override to set custom axes labels/ticks/styling + return diff --git a/jdaviz/configs/imviz/plugins/coords_info/coords_info.py b/jdaviz/configs/imviz/plugins/coords_info/coords_info.py index 28f7b7fa63..5cc0ecdbeb 100644 --- a/jdaviz/configs/imviz/plugins/coords_info/coords_info.py +++ b/jdaviz/configs/imviz/plugins/coords_info/coords_info.py @@ -399,13 +399,10 @@ def _image_viewer_update(self, viewer, x, y): def _spectrum_viewer_update(self, viewer, x, y): def _cursor_fallback(): - statistic = getattr(viewer.state, 'function', None) - cache_key = (viewer.state.layers[0].layer.label, statistic) - sp = self.app._get_object_cache.get(cache_key, viewer.data()[0]) self._dict['axes_x'] = x - self._dict['axes_x:unit'] = sp.spectral_axis.unit.to_string() + self._dict['axes_x:unit'] = viewer.state.x_display_unit self._dict['axes_y'] = y - self._dict['axes_y:unit'] = sp.flux.unit.to_string() + self._dict['axes_y:unit'] = viewer.state.y_display_unit self._dict['data_label'] = '' def _copy_axes_to_spectral(): @@ -428,8 +425,6 @@ def _copy_axes_to_spectral(): self.row3_text = '' self.icon = 'mdi-cursor-default' self.marks[viewer._reference_id].visible = False - # get the units from the first layer - # TODO: replace with display units once implemented _cursor_fallback() _copy_axes_to_spectral() return @@ -469,20 +464,25 @@ def _copy_axes_to_spectral(): sp = self.app._get_object_cache[cache_key] else: sp = self._specviz_helper.get_data(data_label=data_label, - subset_to_apply=subset_label) + spectral_subset=subset_label) self.app._get_object_cache[cache_key] = sp + # Calculations have to happen in the frame of viewer display units. + disp_wave = sp.spectral_axis.to_value(viewer.state.x_display_unit, u.spectral()) + disp_flux = sp.flux.to_value(viewer.state.y_display_unit, + u.spectral_density(sp.spectral_axis)) + # Out of range in spectral axis. if (self.dataset.selected != lyr.layer.label and - (x < sp.spectral_axis.value.min() or x > sp.spectral_axis.value.max())): + (x < disp_wave.min() or x > disp_wave.max())): continue - cur_i = np.argmin(abs(sp.spectral_axis.value - x)) - cur_wave = sp.spectral_axis[cur_i] - cur_flux = sp.flux[cur_i] + cur_i = np.argmin(abs(disp_wave - x)) + cur_wave = disp_wave[cur_i] + cur_flux = disp_flux[cur_i] - dx = cur_wave.value - x - dy = cur_flux.value - y + dx = cur_wave - x + dy = cur_flux - y cur_distance = math.sqrt(dx * dx + dy * dy) if (closest_distance is None) or (cur_distance < closest_distance): closest_distance = cur_distance @@ -507,27 +507,34 @@ def _copy_axes_to_spectral(): return self.row2_title = 'Wave' - self.row2_text = f'{closest_wave.value:10.5e} {closest_wave.unit.to_string()}' - self._dict['axes_x'] = closest_wave.value - self._dict['axes_x:unit'] = closest_wave.unit.to_string() - if closest_wave.unit != u.pix: + self.row2_text = f'{closest_wave:10.5e} {viewer.state.x_display_unit}' + self._dict['axes_x'] = closest_wave + self._dict['axes_x:unit'] = viewer.state.x_display_unit + if viewer.state.x_display_unit != u.pix: self.row2_text += f' ({int(closest_i)} pix)' if self.app.config == 'cubeviz': # float to be compatible with nan self._dict['slice'] = float(closest_i) - self._dict['spectral_axis'] = closest_wave.value - self._dict['spectral_axis:unit'] = closest_wave.unit.to_string() + self._dict['spectral_axis'] = closest_wave + self._dict['spectral_axis:unit'] = viewer.state.x_display_unit else: # float to be compatible with nan self._dict['index'] = float(closest_i) + if viewer.state.y_display_unit is None: + flux_unit = "" + else: + flux_unit = viewer.state.y_display_unit self.row3_title = 'Flux' - self.row3_text = f'{closest_flux.value:10.5e} {closest_flux.unit.to_string()}' - self._dict['axes_y'] = closest_flux.value - self._dict['axes_y:unit'] = closest_flux.unit.to_string() + self.row3_text = f'{closest_flux:10.5e} {flux_unit}' + self._dict['axes_y'] = closest_flux + self._dict['axes_y:unit'] = viewer.state.y_display_unit - self.icon = closest_icon + if closest_icon is not None: + self.icon = closest_icon + else: + self.icon = "" - self.marks[viewer._reference_id].update_xy([closest_wave.value], [closest_flux.value]) # noqa + self.marks[viewer._reference_id].update_xy([closest_wave], [closest_flux]) self.marks[viewer._reference_id].visible = True _copy_axes_to_spectral() diff --git a/jdaviz/configs/imviz/plugins/parsers.py b/jdaviz/configs/imviz/plugins/parsers.py index 4789830bc1..39b1068528 100644 --- a/jdaviz/configs/imviz/plugins/parsers.py +++ b/jdaviz/configs/imviz/plugins/parsers.py @@ -82,6 +82,11 @@ def parse_data(app, file_obj, ext=None, data_label=None): ) with rdd.open(file_obj) as pf: _parse_image(app, pf, data_label, ext=ext) + + elif file_obj_lower.endswith('.reg'): + # This will load DS9 regions as Subset but only if there is already data. + app._jdaviz_helper.load_regions_from_file(file_obj) + else: # Assume FITS with fits.open(file_obj) as pf: _parse_image(app, pf, data_label, ext=ext) diff --git a/jdaviz/configs/imviz/tests/data/ds9_annulus_01.reg b/jdaviz/configs/imviz/tests/data/ds9_annulus_01.reg new file mode 100644 index 0000000000..b4d5144aa2 --- /dev/null +++ b/jdaviz/configs/imviz/tests/data/ds9_annulus_01.reg @@ -0,0 +1,4 @@ +# Region file format: DS9 version 4.1 +global color=green dashlist=8 3 width=1 font="helvetica 10 normal roman" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1 +icrs +annulus(197.8929,-1.36599,1.9820003",3.9640007",5.946001") # color=magenta font="helvetica 10 bold roman" text={Annulus} diff --git a/jdaviz/configs/imviz/tests/test_regions.py b/jdaviz/configs/imviz/tests/test_regions.py index 38a60fbc78..145ed2f1ad 100644 --- a/jdaviz/configs/imviz/tests/test_regions.py +++ b/jdaviz/configs/imviz/tests/test_regions.py @@ -1,9 +1,8 @@ -import glue_astronomy import numpy as np +import pytest from astropy import units as u from astropy.coordinates import SkyCoord, Angle from astropy.utils.data import get_pkg_data_filename -from packaging.version import Version from photutils.aperture import CircularAperture, SkyCircularAperture from regions import (PixCoord, CircleSkyRegion, RectanglePixelRegion, CirclePixelRegion, EllipsePixelRegion, PointSkyRegion, PolygonPixelRegion, @@ -11,8 +10,6 @@ from jdaviz.configs.imviz.tests.utils import BaseImviz_WCS_NoWCS -GLUE_ASTRONOMY_LT_0_7_1 = not (Version(glue_astronomy.__version__) >= Version("0.7.1.dev")) - class BaseRegionHandler: """Test to see if region is loaded. @@ -122,13 +119,15 @@ def test_regions_sky_has_wcs(self): self.imviz._apply_interactive_region('bqplot:circle', (1.5, 2.5), (3.6, 4.6)) sky = SkyCoord(ra=337.5202808, dec=-20.833333059999998, unit='deg') - # This will become indistinguishable from normal Subset. + # These will become indistinguishable from normal Subset. my_reg_sky_1 = CircleSkyRegion(sky, Angle(0.5, u.arcsec)) - # Masked subset. my_reg_sky_2 = CircleAnnulusSkyRegion(center=sky, inner_radius=0.0004 * u.deg, outer_radius=0.0005 * u.deg) - # Add them both. - bad_regions = self.imviz.load_regions([my_reg_sky_1, my_reg_sky_2], return_bad_regions=True) + # Masked subset. + my_reg_sky_3 = PolygonPixelRegion(vertices=PixCoord(x=[1, 1, 3, 3, 1], y=[1, 3, 3, 1, 1])) + # Add them all. + bad_regions = self.imviz.load_regions([my_reg_sky_1, my_reg_sky_2, my_reg_sky_3], + return_bad_regions=True) assert len(bad_regions) == 0 # Mimic interactive regions (after) @@ -139,15 +138,28 @@ def test_regions_sky_has_wcs(self): # that check hopefully is already done in glue-astronomy. # Apparently, static region ate up one number... subsets = self.imviz.get_interactive_regions() - assert list(subsets.keys()) == ['Subset 1', 'Subset 2', 'Subset 4', 'Subset 5'], subsets + assert list(subsets.keys()) == ['Subset 1', 'Subset 2', 'Subset 3', 'Subset 5', 'Subset 6'], subsets # noqa: E501 assert isinstance(subsets['Subset 1'], CirclePixelRegion) assert isinstance(subsets['Subset 2'], CirclePixelRegion) - assert isinstance(subsets['Subset 4'], EllipsePixelRegion) - assert isinstance(subsets['Subset 5'], RectanglePixelRegion) + assert isinstance(subsets['Subset 3'], CircleAnnulusPixelRegion) + assert isinstance(subsets['Subset 5'], EllipsePixelRegion) + assert isinstance(subsets['Subset 6'], RectanglePixelRegion) # Check static region self.verify_region_loaded('MaskedSubset 1') + def test_regions_annulus_from_load_data(self): + # This file actually will load 2 annuli + regfile = get_pkg_data_filename('data/ds9_annulus_01.reg') + self.imviz.load_data(regfile) + assert len(self.imviz.app.data_collection) == 2 # Make sure not loaded as data + + subsets = self.imviz.get_interactive_regions() + subset_names = list(subsets.keys()) + assert subset_names == ['Subset 1', 'Subset 2'] + for n in subset_names: + assert isinstance(subsets[n], CircleAnnulusPixelRegion) + def test_photutils_pixel(self): my_aper = CircularAperture((5, 5), r=2) bad_regions = self.imviz.load_regions([my_aper], return_bad_regions=True) @@ -173,18 +185,21 @@ def setup_class(self): self.raw_regions = Regions.read(self.region_file, format='ds9') def test_ds9_load_all(self, imviz_helper): + with pytest.raises(ValueError, match="Cannot load regions without data"): + imviz_helper.load_data(self.region_file) + self.viewer = imviz_helper.default_viewer imviz_helper.load_data(self.arr, data_label='my_image') bad_regions = imviz_helper.load_regions_from_file(self.region_file, return_bad_regions=True) assert len(bad_regions) == 1 - # Will load 8/9 and 6 of that become ROIs. + # Will load 8/9 and 7 of that become ROIs. subsets = imviz_helper.get_interactive_regions() assert list(subsets.keys()) == ['Subset 1', 'Subset 2', 'Subset 3', - 'Subset 4', 'Subset 5', 'Subset 6'], subsets + 'Subset 4', 'Subset 5', 'Subset 6', 'Subset 7'], subsets - for i in (1, 2): # The other 2 are MaskedSubset - self.verify_region_loaded(f'MaskedSubset {i}', count=1) + # The other 1 is MaskedSubset + self.verify_region_loaded('MaskedSubset 1', count=1) def test_ds9_load_two_good(self, imviz_helper): self.viewer = imviz_helper.default_viewer @@ -234,18 +249,12 @@ def test_annulus(self): new_subset = subset_groups[0].subset_state & ~subset_groups[1].subset_state self.viewer.apply_subset_state(new_subset) - # In older glue-astronomy, annulus is no longer accessible by API - # but also should not crash Imviz. subsets = self.imviz.get_interactive_regions() assert len(self.imviz.app.data_collection.subset_groups) == 3 - if GLUE_ASTRONOMY_LT_0_7_1: - expected_subset_keys = ['Subset 1', 'Subset 2'] - else: - expected_subset_keys = ['Subset 1', 'Subset 2', 'Subset 3'] - assert isinstance(subsets['Subset 3'], CircleAnnulusPixelRegion) - assert list(subsets.keys()) == expected_subset_keys, subsets + assert list(subsets.keys()) == ['Subset 1', 'Subset 2', 'Subset 3'], subsets assert isinstance(subsets['Subset 1'], CirclePixelRegion) assert isinstance(subsets['Subset 2'], CirclePixelRegion) + assert isinstance(subsets['Subset 3'], CircleAnnulusPixelRegion) # Clear the regions for next test. self.imviz._delete_all_regions() diff --git a/jdaviz/configs/mosviz/plugins/parsers.py b/jdaviz/configs/mosviz/plugins/parsers.py index 6b6259cea7..6566de033f 100644 --- a/jdaviz/configs/mosviz/plugins/parsers.py +++ b/jdaviz/configs/mosviz/plugins/parsers.py @@ -9,7 +9,7 @@ from astropy.io.registry import IORegistryError from astropy.wcs import WCS from glue.core.data import Data -from glue.core.link_helpers import LinkSame +from glue.core.link_helpers import LinkSameWithUnits from specutils import Spectrum1D, SpectrumList, SpectrumCollection from specutils.io.default_loaders.jwst_reader import identify_jwst_s2d_multi_fits @@ -140,7 +140,7 @@ def link_data_in_table(app, data_obj=None): wc_spec_1d = app.session.data_collection[spec_1d].world_component_ids wc_spec_2d = app.session.data_collection[spec_2d].world_component_ids - wc_spec_ids.append(LinkSame(wc_spec_1d[0], wc_spec_2d[1])) + wc_spec_ids.append(LinkSameWithUnits(wc_spec_1d[0], wc_spec_2d[1])) app.session.data_collection.add_link(wc_spec_ids) diff --git a/jdaviz/configs/specviz/helper.py b/jdaviz/configs/specviz/helper.py index c2a918b681..e0969e7cdd 100644 --- a/jdaviz/configs/specviz/helper.py +++ b/jdaviz/configs/specviz/helper.py @@ -138,18 +138,24 @@ def get_spectra(self, data_label=None, subset_to_apply=None, apply_slider_redshi return output_spectra - def get_spectral_regions(self): + def get_spectral_regions(self, use_display_units=False): """ A simple wrapper around the app-level call to retrieve only spectral subsets, which are now returned as SpectralRegions by default. + Parameters + ---------- + use_display_units : bool, optional + Whether to convert to the display units defined in the + :ref:`Unit Conversion ` plugin. + Returns ------- spec_regs : dict Mapping from the names of the subsets to the subsets expressed as `specutils.SpectralRegion` objects. """ - return self.app.get_subsets(spectral_only=True) + return self.app.get_subsets(spectral_only=True, use_display_units=use_display_units) def x_limits(self, x_min=None, x_max=None): """Sets the limits of the x-axis @@ -281,7 +287,8 @@ def set_spectrum_tick_format(self, fmt, axis=None): self._default_spectrum_viewer_reference_name ).figure.axes[axis].tick_format = fmt - def get_data(self, data_label=None, spectral_subset=None, cls=None, **kwargs): + def get_data(self, data_label=None, spectral_subset=None, cls=None, + use_display_units=False, **kwargs): """ Returns data with name equal to data_label of type cls with subsets applied from subset_to_apply. @@ -294,6 +301,8 @@ def get_data(self, data_label=None, spectral_subset=None, cls=None, **kwargs): Spectral subset applied to data. cls : `~specutils.Spectrum1D`, optional The type that data will be returned as. + use_display_units: bool, optional + Whether to convert to the display units defined in the plugin. Returns ------- @@ -322,4 +331,5 @@ def get_data(self, data_label=None, spectral_subset=None, cls=None, **kwargs): function = None return self._get_data(data_label=data_label, spatial_subset=spatial_subset, - spectral_subset=spectral_subset, function=function, cls=cls) + spectral_subset=spectral_subset, function=function, + cls=cls, use_display_units=use_display_units) diff --git a/jdaviz/configs/specviz/plugins/line_analysis/line_analysis.py b/jdaviz/configs/specviz/plugins/line_analysis/line_analysis.py index e19b115b1d..5365e7cb4a 100644 --- a/jdaviz/configs/specviz/plugins/line_analysis/line_analysis.py +++ b/jdaviz/configs/specviz/plugins/line_analysis/line_analysis.py @@ -15,7 +15,8 @@ RemoveDataMessage, SpectralMarksChangedMessage, LineIdentifyMessage, - RedshiftMessage) + RedshiftMessage, + GlobalDisplayUnitChanged) from jdaviz.core.marks import (LineAnalysisContinuum, LineAnalysisContinuumCenter, LineAnalysisContinuumLeft, @@ -158,6 +159,8 @@ def __init__(self, *args, **kwargs): handler=self._on_plotted_lines_changed) self.hub.subscribe(self, LineIdentifyMessage, handler=self._on_identified_line_changed) + self.hub.subscribe(self, GlobalDisplayUnitChanged, + handler=self._on_global_display_unit_changed) @property def user_api(self): @@ -205,6 +208,10 @@ def _on_viewer_subsets_changed(self, msg): and self.plugin_opened): self._calculate_statistics() + def _on_global_display_unit_changed(self, msg): + if self.plugin_opened: + self._calculate_statistics() + @observe('plugin_opened') def _on_plugin_opened_changed(self, *args): if self.disabled_msg: @@ -314,8 +321,8 @@ def _calculate_statistics(self, *args, **kwargs): # show spinner with overlay self.results_computing = True - - full_spectrum = self.dataset.selected_spectrum_for_spatial_subset(self.spatial_subset_selected) # noqa + full_spectrum = self.dataset.selected_spectrum_for_spatial_subset(self.spatial_subset_selected, # noqa + use_display_units=True) if (full_spectrum is None or self.width == "" or (not self.plugin_opened and not kwargs.get('ignore_plugin_closed'))): @@ -330,13 +337,14 @@ def _calculate_statistics(self, *args, **kwargs): self.update_results(None) return - sr = self.app.get_subsets().get(self.spectral_subset_selected) if self.spectral_subset_selected == "Entire Spectrum": spectrum = full_spectrum else: + sr = self.app.get_subsets(self.spectral_subset_selected, + simplify_spectral=True, use_display_units=True) spectrum = extract_region(full_spectrum, sr, return_single_spectrum=True) - sr_lower = spectrum.spectral_axis[spectrum.spectral_axis.value >= sr.lower.value][0] - sr_upper = spectrum.spectral_axis[spectrum.spectral_axis.value <= sr.upper.value][-1] + sr_lower = np.nanmin(spectrum.spectral_axis[spectrum.spectral_axis.value >= sr.lower.value]) # noqa + sr_upper = np.nanmax(spectrum.spectral_axis[spectrum.spectral_axis.value <= sr.upper.value]) # noqa # compute continuum if self.continuum_subset_selected == "Surrounding" and self.spectral_subset_selected == "Entire Spectrum": # noqa @@ -383,7 +391,8 @@ def _calculate_statistics(self, *args, **kwargs): # cube, but still apply that to the spatially-collapsed spectrum. continuum_mask = ~self._specviz_helper.get_data( self.dataset.selected, - spectral_subset=self.continuum_subset_selected).mask + spectral_subset=self.continuum_subset_selected, + use_display_units=False).mask spectral_axis_nanmasked = spectral_axis.value.copy() spectral_axis_nanmasked[~continuum_mask] = np.nan if self.spectral_subset_selected == "Entire Spectrum": @@ -521,8 +530,8 @@ def _uncertainty(result): def _compute_redshift_for_selected_line(self): index = self.line_items.index(self.selected_line) line_mark = self.line_marks[index] - rest_value = (line_mark.rest_value * line_mark._x_unit).to_value(u.AA, - equivalencies=u.spectral()) + rest_value = (line_mark.rest_value * line_mark.xunit).to_value(u.AA, + equivalencies=u.spectral()) return (self.results_centroid - rest_value) / rest_value @observe('sync_identify') diff --git a/jdaviz/configs/specviz/plugins/parsers.py b/jdaviz/configs/specviz/plugins/parsers.py index a567e65ddc..73c019060b 100644 --- a/jdaviz/configs/specviz/plugins/parsers.py +++ b/jdaviz/configs/specviz/plugins/parsers.py @@ -76,14 +76,6 @@ def specviz_spectrum1d_parser(app, data, data_label=None, format=None, show_in_v raise ValueError(f"Length of data labels list ({len(data_label)}) is different" f" than length of list of data ({len(data)})") - # If there's already visible data in the viewer, convert units if needed - current_unit = None - if spectrum_viewer_reference_name in app.get_viewer_reference_names(): - sv = app.get_viewer(spectrum_viewer_reference_name) - for layer_state in sv.state.layers: - if layer_state.visible: - current_unit = sv.state.x_display_unit - with app.data_collection.delay_link_manager_update(): # these are used to build a combined spectrum with all # input spectra included (taken from https://github.com/spacetelescope/ @@ -98,10 +90,6 @@ def specviz_spectrum1d_parser(app, data, data_label=None, format=None, show_in_v wave_units = spec.spectral_axis.unit flux_units = spec.flux.unit - if current_unit is not None and spec.spectral_axis.unit != current_unit: - spec = Spectrum1D(flux=spec.flux, - spectral_axis=spec.spectral_axis.to(current_unit)) - # Make metadata layout conform with other viz. spec.meta = standardize_metadata(spec.meta) diff --git a/jdaviz/configs/specviz/plugins/unit_conversion/tests/test_unit_conversion.py b/jdaviz/configs/specviz/plugins/unit_conversion/tests/test_unit_conversion.py index cf1af35805..245c19c124 100644 --- a/jdaviz/configs/specviz/plugins/unit_conversion/tests/test_unit_conversion.py +++ b/jdaviz/configs/specviz/plugins/unit_conversion/tests/test_unit_conversion.py @@ -1,272 +1,91 @@ -import numpy as np import pytest from astropy import units as u -from astropy.nddata import UnknownUncertainty -from astropy.tests.helper import assert_quantity_allclose -from glue.core.roi import XRangeROI - -from jdaviz.configs.specviz.plugins.unit_conversion import unit_conversion as uc - -RESULT_SPECTRAL_AXIS = [0.6, 0.62222222, 0.64444444, 0.66666667, - 0.68888889, 0.71111111, 0.73333333, - 0.75555556, 0.77777778, 0.8] * u.micron - -RESULT_FLUX = [1.04067240e-07, 9.52912307e-08, 9.77144651e-08, - 1.00212528e-07, 8.55573341e-08, 8.29285448e-08, - 9.05651431e-08, 8.33870526e-08, 7.47628902e-08, - 7.74896053e-08] * u.Unit("erg / (s cm2 um)") - -RESULT_UNCERTAINTY = [3.85914248e-09, 3.60631495e-09, 1.74661581e-09, - 1.29057072e-08, 1.08965936e-08, 3.33352891e-09, - 5.64618219e-09, 1.65028707e-09, 4.49994292e-09, - 6.61559372e-09] +# On failure, should not crash; essentially a no-op. @pytest.mark.parametrize( - ('new_spectral_axis', 'new_flux'), - [("fail", "erg / (s cm2 um)"), - ("None", "fail"), - ("micron", "fail")]) -def test_value_error_exception(specviz_helper, spectrum1d, new_spectral_axis, new_flux): - label = "Test 1D Spectrum" - specviz_helper.load_spectrum(spectrum1d, data_label=label) - - conv_func = uc.UnitConversion.process_unit_conversion - converted_spectrum = conv_func(specviz_helper.app, spectrum=spectrum1d, - new_flux=new_flux, - new_spectral_axis=new_spectral_axis) - - assert converted_spectrum is None - - -def test_no_spec_no_flux_no_uncert(specviz_helper, spectrum1d): - label = "Test 1D Spectrum" - specviz_helper.load_spectrum(spectrum1d, data_label=label) - - spectrum1d.uncertainty = None - - conv_func = uc.UnitConversion.process_unit_conversion - converted_spectrum = conv_func(specviz_helper.app, spectrum=spectrum1d) - - assert converted_spectrum.flux.unit == spectrum1d.flux.unit - assert converted_spectrum.spectral_axis.unit == spectrum1d.spectral_axis.unit - assert converted_spectrum.uncertainty is None - - # Test that applying and removing Subset disables and enables it, respectively. - conv_plugin = specviz_helper.app.get_tray_item_from_name('g-unit-conversion') - specviz_helper.app.get_viewer("spectrum-viewer").apply_roi(XRangeROI(6000, 6500)) - assert conv_plugin.disabled_msg == 'Please create Subsets only after unit conversion' - specviz_helper.app.data_collection.remove_subset_group( - specviz_helper.app.data_collection.subset_groups[0]) - assert conv_plugin.disabled_msg == '' - - -def test_spec_no_flux_no_uncert(specviz_helper, spectrum1d): - label = "Test 1D Spectrum" - specviz_helper.load_spectrum(spectrum1d, data_label=label) - + ('new_spectral_axis', 'new_flux', 'expected_spectral_axis', 'expected_flux'), + [("fail", "erg / (s cm2 Angstrom)", "Angstrom", "erg / (s cm2 Angstrom)"), + ("None", "fail", "Angstrom", "Jy"), + ("micron", "fail", "micron", "Jy")]) +def test_value_error_exception(specviz_helper, spectrum1d, new_spectral_axis, new_flux, + expected_spectral_axis, expected_flux): + specviz_helper.load_spectrum(spectrum1d, data_label="Test 1D Spectrum") + viewer = specviz_helper.app.get_viewer("spectrum-viewer") + plg = specviz_helper.plugins["Unit Conversion"] + + try: + plg.spectral_unit = new_spectral_axis + except ValueError as e: + if "reverting selection to" not in repr(e): + raise + try: + plg.flux_unit = new_flux + except ValueError as e: + if "reverting selection to" not in repr(e): + raise + + assert len(specviz_helper.app.data_collection) == 1 + assert u.Unit(viewer.state.x_display_unit) == u.Unit(expected_spectral_axis) + assert u.Unit(viewer.state.y_display_unit) == u.Unit(expected_flux) + + +@pytest.mark.parametrize('uncert', (False, True)) +def test_conv_wave_only(specviz_helper, spectrum1d, uncert): + if uncert is False: + spectrum1d.uncertainty = None + specviz_helper.load_spectrum(spectrum1d, data_label="Test 1D Spectrum") + + viewer = specviz_helper.app.get_viewer("spectrum-viewer") + plg = specviz_helper.plugins["Unit Conversion"] new_spectral_axis = "micron" - spectrum1d.uncertainty = None - - conv_func = uc.UnitConversion.process_unit_conversion - converted_spectrum = conv_func(specviz_helper.app, spectrum=spectrum1d, - new_spectral_axis=new_spectral_axis) - - assert_quantity_allclose(converted_spectrum.spectral_axis, - RESULT_SPECTRAL_AXIS, atol=1e-5*u.um) - assert converted_spectrum.flux.unit == spectrum1d.flux.unit - assert converted_spectrum.spectral_axis.unit == new_spectral_axis - assert converted_spectrum.uncertainty is None - + plg.spectral_unit = new_spectral_axis -def test_no_spec_no_flux_uncert(specviz_helper, spectrum1d): - label = "Test 1D Spectrum" - specviz_helper.load_spectrum(spectrum1d, data_label=label) + assert len(specviz_helper.app.data_collection) == 1 + assert u.Unit(viewer.state.x_display_unit) == u.Unit(new_spectral_axis) + assert u.Unit(viewer.state.y_display_unit) == u.Unit('Jy') - conv_func = uc.UnitConversion.process_unit_conversion - converted_spectrum = conv_func(specviz_helper.app, spectrum=spectrum1d) - assert converted_spectrum.flux.unit == spectrum1d.flux.unit +@pytest.mark.parametrize('uncert', (False, True)) +def test_conv_flux_only(specviz_helper, spectrum1d, uncert): + if uncert is False: + spectrum1d.uncertainty = None + specviz_helper.load_spectrum(spectrum1d, data_label="Test 1D Spectrum") + viewer = specviz_helper.app.get_viewer("spectrum-viewer") + plg = specviz_helper.plugins["Unit Conversion"] + new_flux = "erg / (s cm2 Angstrom)" + plg.flux_unit = new_flux -def test_no_spec_no_flux_uncert_unit_exp_none(specviz_helper, spectrum1d): - np.random.seed(42) - label = "Test 1D Spectrum" - specviz_helper.load_spectrum(spectrum1d, data_label=label) + assert len(specviz_helper.app.data_collection) == 1 + assert u.Unit(viewer.state.x_display_unit) == u.Unit('Angstrom') + assert u.Unit(viewer.state.y_display_unit) == u.Unit(new_flux) - spectrum1d.uncertainty = UnknownUncertainty(np.abs( - np.random.randn(len(spectrum1d.spectral_axis)))) - conv_func = uc.UnitConversion.process_unit_conversion - converted_spectrum = conv_func(specviz_helper.app, spectrum=spectrum1d) - - assert converted_spectrum.flux.unit == spectrum1d.flux.unit - assert converted_spectrum.spectral_axis.unit == spectrum1d.spectral_axis.unit - assert converted_spectrum.uncertainty is None - - -def test_no_spec_flux_no_uncert(specviz_helper, spectrum1d): - label = "Test 1D Spectrum" - specviz_helper.load_spectrum(spectrum1d, data_label=label) - - spectrum1d.uncertainty = None - new_flux = "erg / (s cm2 um)" - - conv_func = uc.UnitConversion.process_unit_conversion - converted_spectrum = conv_func(specviz_helper.app, spectrum=spectrum1d, - new_flux=new_flux) - - assert_quantity_allclose(converted_spectrum.flux, - RESULT_FLUX, atol=1e-5*u.Unit(new_flux)) - assert converted_spectrum.spectral_axis.unit == spectrum1d.spectral_axis.unit - assert converted_spectrum.uncertainty is None - - -def test_no_spec_flux_unit_exp_not_none(specviz_helper, spectrum1d): - label = "Test 1D Spectrum" - specviz_helper.load_spectrum(spectrum1d, data_label=label) - - new_flux = "erg / (s cm2 um)" - - conv_func = uc.UnitConversion.process_unit_conversion - converted_spectrum = conv_func(specviz_helper.app, spectrum=spectrum1d, - new_flux=new_flux) - - assert_quantity_allclose(converted_spectrum.flux, - RESULT_FLUX, atol=1e-5*u.Unit(new_flux)) - assert_quantity_allclose(converted_spectrum.uncertainty.quantity.value, - RESULT_UNCERTAINTY, atol=1e-11) - - -def test_spec_flux_no_uncert(specviz_helper, spectrum1d): - label = "Test 1D Spectrum" - specviz_helper.load_spectrum(spectrum1d, data_label=label) +@pytest.mark.parametrize('uncert', (False, True)) +def test_conv_wave_flux(specviz_helper, spectrum1d, uncert): + if uncert is False: + spectrum1d.uncertainty = None + specviz_helper.load_spectrum(spectrum1d, data_label="Test 1D Spectrum") + viewer = specviz_helper.app.get_viewer("spectrum-viewer") + plg = specviz_helper.plugins["Unit Conversion"] new_spectral_axis = "micron" - new_flux = "erg / (s cm2 um)" - - spectrum1d.uncertainty = None - - conv_func = uc.UnitConversion.process_unit_conversion - converted_spectrum = conv_func(specviz_helper.app, spectrum=spectrum1d, - new_flux=new_flux, - new_spectral_axis=new_spectral_axis) - - assert_quantity_allclose(converted_spectrum.flux, - RESULT_FLUX, atol=1e-5*u.Unit(new_flux)) - assert_quantity_allclose(converted_spectrum.spectral_axis, - RESULT_SPECTRAL_AXIS, atol=1e-5*u.um) - assert converted_spectrum.uncertainty is None - - -def test_spec_no_flux_uncert_no_unit_exp(specviz_helper, spectrum1d): - np.random.seed(42) - label = "Test 1D Spectrum" - specviz_helper.load_spectrum(spectrum1d, data_label=label) - - new_spectral_axis = "micron" - - spectrum1d.uncertainty = UnknownUncertainty(np.abs( - np.random.randn(len(spectrum1d.spectral_axis)))) - - conv_func = uc.UnitConversion.process_unit_conversion - converted_spectrum = conv_func(specviz_helper.app, spectrum=spectrum1d, - new_spectral_axis=new_spectral_axis) - - assert converted_spectrum.flux.unit == spectrum1d.flux.unit - assert_quantity_allclose(converted_spectrum.spectral_axis, - RESULT_SPECTRAL_AXIS, atol=1e-5*u.um) - assert converted_spectrum.uncertainty is None - - -def test_no_spec_flux_uncert_no_unit_exp(specviz_helper, spectrum1d): - np.random.seed(42) - label = "Test 1D Spectrum" - specviz_helper.load_spectrum(spectrum1d, data_label=label) - - new_flux = "erg / (s cm2 um)" - - spectrum1d.uncertainty = UnknownUncertainty(np.abs( - np.random.randn(len(spectrum1d.spectral_axis)))) - - conv_func = uc.UnitConversion.process_unit_conversion - converted_spectrum = conv_func(specviz_helper.app, spectrum=spectrum1d, - new_flux=new_flux) - - assert converted_spectrum.spectral_axis.unit == spectrum1d.spectral_axis.unit - assert_quantity_allclose(converted_spectrum.flux, - RESULT_FLUX, atol=1e-5*u.Unit(new_flux)) - assert converted_spectrum.uncertainty is None - - -def test_spec_no_flux_uncert_unit_exp(specviz_helper, spectrum1d): - label = "Test 1D Spectrum" - specviz_helper.load_spectrum(spectrum1d, data_label=label) - - new_spectral_axis = "micron" - - conv_func = uc.UnitConversion.process_unit_conversion - converted_spectrum = conv_func(specviz_helper.app, spectrum=spectrum1d, - new_spectral_axis=new_spectral_axis) - - assert_quantity_allclose(converted_spectrum.spectral_axis, - RESULT_SPECTRAL_AXIS, atol=1e-5*u.um) - - -def test_spec_flux_uncert_no_unit_exp(specviz_helper, spectrum1d): - np.random.seed(42) - label = "Test 1D Spectrum" - specviz_helper.load_spectrum(spectrum1d, data_label=label) - - new_spectral_axis = "micron" - new_flux = "erg / (s cm2 um)" - - spectrum1d.uncertainty = UnknownUncertainty(np.abs( - np.random.randn(len(spectrum1d.spectral_axis)))) - - conv_func = uc.UnitConversion.process_unit_conversion - converted_spectrum = conv_func(specviz_helper.app, spectrum=spectrum1d, - new_flux=new_flux, - new_spectral_axis=new_spectral_axis) - - assert converted_spectrum.uncertainty is None - assert_quantity_allclose(converted_spectrum.spectral_axis, - RESULT_SPECTRAL_AXIS, atol=1e-5*u.um) - assert_quantity_allclose(converted_spectrum.flux, - RESULT_FLUX, atol=1e-5*u.Unit(new_flux)) - - -def test_spec_flux_uncert_unit_exp(specviz_helper, spectrum1d): - label = "Test 1D Spectrum" - specviz_helper.load_spectrum(spectrum1d, data_label=label) - - new_spectral_axis = "micron" - new_flux = "erg / (s cm2 um)" - - conv_func = uc.UnitConversion.process_unit_conversion - converted_spectrum = conv_func(specviz_helper.app, spectrum=spectrum1d, - new_flux=new_flux, - new_spectral_axis=new_spectral_axis) - - assert_quantity_allclose(converted_spectrum.spectral_axis, - RESULT_SPECTRAL_AXIS, atol=1e-5*u.um) - assert_quantity_allclose(converted_spectrum.flux, - RESULT_FLUX, atol=1e-5*u.Unit(new_flux)) - assert_quantity_allclose(converted_spectrum.uncertainty.quantity.value, - RESULT_UNCERTAINTY, atol=1e-11) - - -def test_converted_spec_is_none(specviz_helper, spectrum1d): - label = "Test 1D Spectrum" - specviz_helper.load_spectrum(spectrum1d, data_label=label) - - new_spectral_axis = "feet" - - unit_conversion = specviz_helper.app.get_tray_item_from_name("g-unit-conversion") - unit_conversion.new_spectral_axis_unit = new_spectral_axis - converted_spectrum = unit_conversion.vue_unit_conversion(specviz_helper.app, - spectrum=spectrum1d, - new_flux=None, - new_spectral_axis=new_spectral_axis) - - assert converted_spectrum is None + new_flux = "erg / (s cm2 Angstrom)" + plg.spectral_unit = new_spectral_axis + plg.flux_unit = new_flux + + assert len(specviz_helper.app.data_collection) == 1 + assert u.Unit(viewer.state.x_display_unit) == u.Unit(new_spectral_axis) + assert u.Unit(viewer.state.y_display_unit) == u.Unit(new_flux) + + +def test_conv_no_data(specviz_helper): + """plugin unit selections won't have valid choices yet, preventing + attempting to set display units.""" + plg = specviz_helper.plugins["Unit Conversion"] + with pytest.raises(ValueError, match="no valid unit choices"): + plg.spectral_unit = "micron" + with pytest.raises(ValueError, match="no valid unit choices"): + plg.flux_unit = "erg / (s cm2 Angstrom)" + assert len(specviz_helper.app.data_collection) == 0 diff --git a/jdaviz/configs/specviz/plugins/unit_conversion/unit_conversion.py b/jdaviz/configs/specviz/plugins/unit_conversion/unit_conversion.py index acae7e3f26..6e10407ea5 100644 --- a/jdaviz/configs/specviz/plugins/unit_conversion/unit_conversion.py +++ b/jdaviz/configs/specviz/plugins/unit_conversion/unit_conversion.py @@ -1,274 +1,143 @@ -from packaging.version import Version - -import specutils +import numpy as np from astropy import units as u -from astropy.nddata import VarianceUncertainty, StdDevUncertainty, InverseVariance -from glue.core.message import SubsetCreateMessage, SubsetDeleteMessage -from traitlets import List, Unicode, Any, observe +from traitlets import List, Unicode, observe -from jdaviz.core.events import SnackbarMessage, RedshiftMessage +from jdaviz.core.events import GlobalDisplayUnitChanged from jdaviz.core.registries import tray_registry -from jdaviz.core.template_mixin import PluginTemplateMixin, DatasetSelectMixin +from jdaviz.core.template_mixin import PluginTemplateMixin, UnitSelectPluginComponent, PluginUserApi from jdaviz.core.validunits import (create_spectral_equivalencies_list, create_flux_equivalencies_list) -from jdaviz.configs.specviz.helper import _apply_redshift_to_spectra __all__ = ['UnitConversion'] -unit_exponents = {StdDevUncertainty: 1, - InverseVariance: -2, - VarianceUncertainty: 2} -SPECUTILS_GT_1_7_0 = Version(specutils.__version__) > Version('1.7.0') + +def _valid_glue_display_unit(unit_str, sv, axis='x'): + # need to make sure the unit string is formatted according to the list of valid choices + # that glue will accept (may not be the same as the defaults of the installed version of + # astropy) + if not unit_str: + return unit_str + unit_u = u.Unit(unit_str) + choices_str = getattr(sv.state.__class__, f'{axis}_display_unit').get_choices(sv.state) + choices_str = [choice for choice in choices_str if choice is not None] + choices_u = [u.Unit(choice) for choice in choices_str] + if unit_u not in choices_u: + raise ValueError(f"{unit_str} could not find match in valid {axis} display units {choices_str}") # noqa + ind = choices_u.index(unit_u) + return choices_str[ind] @tray_registry('g-unit-conversion', label="Unit Conversion", viewer_requirements='spectrum') -class UnitConversion(PluginTemplateMixin, DatasetSelectMixin): - +class UnitConversion(PluginTemplateMixin): + """ + The Unit Conversion plugin handles global app-wide unit-conversion. + See the :ref:`Unit Conversion Plugin Documentation ` for more details. + + Only the following attributes and methods are available through the + :ref:`public plugin API `: + + * :meth:`~jdaviz.core.template_mixin.PluginTemplateMixin.show` + * :meth:`~jdaviz.core.template_mixin.PluginTemplateMixin.open_in_tray` + * ``spectral_unit`` (:class:`~jdaviz.core.template_mixin.SelectPluginComponent`): + Global unit to use for all spectral axes. + * ``flux_unit`` (:class:`~jdaviz.core.template_mixin.SelectPluginComponent`): + Global unit to use for all flux axes. + """ template_file = __file__, "unit_conversion.vue" - current_flux_unit = Unicode().tag(sync=True) - current_spectral_axis_unit = Unicode().tag(sync=True) - new_flux_unit = Any().tag(sync=True) - new_spectral_axis_unit = Any().tag(sync=True) - - spectral_axis_unit_equivalencies = List([]).tag(sync=True) - flux_unit_equivalencies = List([]).tag(sync=True) + spectral_unit_items = List().tag(sync=True) + spectral_unit_selected = Unicode().tag(sync=True) + flux_unit_items = List().tag(sync=True) + flux_unit_selected = Unicode().tag(sync=True) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.hub.subscribe(self, SubsetCreateMessage, handler=self._on_viewer_subset_changed) - self.hub.subscribe(self, SubsetDeleteMessage, handler=self._on_viewer_subset_changed) - - self._redshift = None - self.app.hub.subscribe(self, RedshiftMessage, handler=self._redshift_listener) - - self._default_spectrum_viewer_reference_name = kwargs.get( - "spectrum_viewer_reference_name", "spectrum-viewer" - ) - - # when accessing the selected data, access the spectrum-viewer version - # TODO: we'll probably want to update unit-conversion to be able to act on cubes directly - # in the future - self.dataset._viewers = [self._default_spectrum_viewer_reference_name] - # require entries to be in spectrum-viewer (not other cubeviz images, etc) - self.dataset.add_filter('layer_in_spectrum_viewer') - - def _on_viewer_subset_changed(self, *args): - if len(self.app.data_collection.subset_groups) == 0: - self.disabled_msg = '' - else: - self.disabled_msg = 'Please create Subsets only after unit conversion' - - def _redshift_listener(self, msg): - '''Save new redshifts (including from the helper itself)''' - if msg.param == "redshift": - self._redshift = msg.value - - @observe('dataset_selected') - def update_ui(self, event=None): - """ - Set up UI to have all values of currently visible spectra. - """ - spectrum = self.dataset.selected_obj - if spectrum is None: + if self.config not in ['specviz', 'cubeviz']: + # TODO [specviz2d, mosviz] x_display_unit is not implemented in glue for image viewer + # used by spectrum-2d-viewer + # TODO [mosviz]: add to yaml file + # TODO [cubeviz, slice]: slice indicator broken after changing spectral_unit + # TODO: support for multiple viewers and handling of mixed state from glue (or does + # this force all to sync?) + self.disabled_msg = f'This plugin is temporarily disabled in {self.config}. Effort to improve it is being tracked at GitHub Issue 1972.' # noqa + + # TODO [markers]: existing markers need converting + self.spectrum_viewer.state.add_callback('x_display_unit', + self._on_glue_x_display_unit_changed) + self.spectrum_viewer.state.add_callback('y_display_unit', + self._on_glue_y_display_unit_changed) + + self.spectral_unit = UnitSelectPluginComponent(self, + items='spectral_unit_items', + selected='spectral_unit_selected') + self.flux_unit = UnitSelectPluginComponent(self, + items='flux_unit_items', + selected='flux_unit_selected') + + @property + def user_api(self): + return PluginUserApi(self, expose=('spectral_unit', 'flux_unit')) + + def _on_glue_x_display_unit_changed(self, x_unit): + if x_unit is None: return - - # Set UI label to show current flux and spectral axis units. - self.current_flux_unit = spectrum.flux.unit.to_string() - self.current_spectral_axis_unit = spectrum.spectral_axis.unit.to_string() - - # Populate drop down with all valid options for unit conversion. - self.spectral_axis_unit_equivalencies = create_spectral_equivalencies_list(spectrum) - self.flux_unit_equivalencies = create_flux_equivalencies_list(spectrum) - - def vue_unit_conversion(self, *args, **kwargs): - """ - Runs when the ``apply`` button is hit. Tries to change units if ``new`` units are set - and are valid. - """ - if self._redshift is not None: - # apply the global redshift to the new spectrum - spectrum = _apply_redshift_to_spectra(self.dataset.selected_obj, self._redshift) - else: - spectrum = self.dataset.selected_obj - - converted_spec = self.process_unit_conversion(spectrum, - self.new_flux_unit, - self.new_spectral_axis_unit) - if converted_spec is None: + if x_unit == 'deg' and self.app.config == 'cubeviz': + # original unit during init can be deg (before axis is set correctly) return - - label = f"_units_copy_Flux:{converted_spec.flux.unit}_" +\ - f"SpectralAxis:{converted_spec.spectral_axis.unit}" - new_label = "" - - # Finds the '_units_copy_' spectrum and does unit conversions in that copy. - if "_units_copy_" in self.dataset_selected: - - selected_data_label = self.dataset_selected - selected_data_label_split = selected_data_label.split("_units_copy_") - - new_label = selected_data_label_split[0] + label - - original_spectrum = self.data_collection[selected_data_label_split[0]] - original_flux = original_spectrum.get_object().flux.unit - original_spectral_axis = original_spectrum.get_object().spectral_axis.unit - - if new_label in self.data_collection: - # Spectrum with these converted units already exists. - msg = SnackbarMessage( - "Spectrum with these units already exists, please check the data drop down.", - color="warning", - sender=self) - self.hub.broadcast(msg) - return - - elif converted_spec.flux.unit == original_flux and \ - converted_spec.spectral_axis.unit == original_spectral_axis: - # Check if converted units already exist in the original spectrum. - msg = SnackbarMessage( - "These are the units of the original spectrum, please use " - "that spectrum instead.", - color="warning", - sender=self) - self.hub.broadcast(msg) - return - - else: - # Add spectrum with converted units to app. - self.app.add_data(converted_spec, new_label) - self.app.add_data_to_viewer( - self._default_spectrum_viewer_reference_name, - new_label, clear_other_data=True - ) - - else: - new_label = self.dataset_selected + label - - if new_label in self.data_collection: - # Spectrum with these converted units already exists. - msg = SnackbarMessage( - "Spectrum with these units already exists, please check the data drop down.", - color="warning", - sender=self) - self.hub.broadcast(msg) - - return - else: - - # Replace old spectrum with new one with updated units. - self.app.add_data(converted_spec, new_label) - - self.app.add_data_to_viewer( - self._default_spectrum_viewer_reference_name, - new_label, clear_other_data=True - ) - snackbar_message = SnackbarMessage( - f"Data set '{label}' units converted successfully.", - color="success", - sender=self) - self.hub.broadcast(snackbar_message) - - def process_unit_conversion(self, spectrum, new_flux=None, new_spectral_axis=None): - """ - - Parameters - ---------- - spectrum : `specutils.Spectrum1D` - The spectrum that will have its units converted. - new_flux - The flux of spectrum will be converted to these units if they are provided. - new_spectral_axis - The spectral_axis of spectrum will be converted to these units if they are provided. - - Returns - ------- - converted_spectrum : `specutils.Spectrum1D` - A new spectrum with converted units. - """ - set_spectral_axis_unit = spectrum.spectral_axis - set_flux_unit = spectrum.flux - - current_flux_unit = spectrum.flux.unit.to_string() - current_spectral_axis_unit = spectrum.spectral_axis.unit.to_string() - - # Try to set new units if set and are valid. - if new_spectral_axis is not None \ - and new_spectral_axis != "" \ - and new_spectral_axis != current_spectral_axis_unit: - try: - set_spectral_axis_unit = spectrum.spectral_axis.to(u.Unit(new_spectral_axis)) - except ValueError as e: - snackbar_message = SnackbarMessage( - "Unable to convert spectral axis units for selected data. " - f"Try different units: {repr(e)}", - color="error", - sender=self) - self.hub.broadcast(snackbar_message) - - return - - # Try to set new units if set and are valid. - if new_flux is not None \ - and new_flux != "" \ - and new_flux != current_flux_unit: - try: - equivalencies = u.spectral_density(set_spectral_axis_unit) - set_flux_unit = spectrum.flux.to(u.Unit(new_flux), - equivalencies=equivalencies) - except ValueError as e: - snackbar_message = SnackbarMessage( - "Unable to convert flux units for selected data. " - f"Try different units: {repr(e)}", - color="error", - sender=self) - self.hub.broadcast(snackbar_message) - - return - - # Uncertainty converted to new flux units - if spectrum.uncertainty is not None: - unit_exp = unit_exponents.get(spectrum.uncertainty.__class__) - # If uncertainty type not in our lookup, drop the uncertainty - if unit_exp is None: - msg = SnackbarMessage( - "Warning: Unrecognized uncertainty type, cannot guarantee " - "conversion so dropping uncertainty in resulting data", - color="warning", - sender=self) - self.hub.broadcast(msg) - temp_uncertainty = None - else: - try: - # Catch and handle error trying to convert variance uncertainties - # between frequency and wavelength space. - # TODO: simplify this when astropy handles it - temp_uncertainty = spectrum.uncertainty.quantity**(1/unit_exp) - temp_uncertainty = temp_uncertainty.to(u.Unit(set_flux_unit.unit), - equivalencies=u.spectral_density(set_spectral_axis_unit)) # noqa - temp_uncertainty **= unit_exp - temp_uncertainty = spectrum.uncertainty.__class__(temp_uncertainty.value) - except u.UnitConversionError: - msg = SnackbarMessage( - "Warning: Could not convert uncertainty, setting to " - "None in converted data", - color="warning", - sender=self) - self.hub.broadcast(msg) - temp_uncertainty = None - else: - temp_uncertainty = None - - # Create new spectrum with new units. - converted_spectrum = spectrum._copy(flux=set_flux_unit, - wcs=None, - spectral_axis=set_spectral_axis_unit, - unit=set_flux_unit.unit, - uncertainty=temp_uncertainty) - if SPECUTILS_GT_1_7_0: - converted_spectrum.shift_spectrum_to(redshift=spectrum.redshift) - else: - converted_spectrum.redshift = spectrum.redshift - return converted_spectrum + self.spectrum_viewer.set_plot_axes() + if x_unit != self.spectral_unit.selected: + x_unit = _valid_glue_display_unit(x_unit, self.spectrum_viewer, 'x') + x_u = u.Unit(x_unit) + choices = create_spectral_equivalencies_list(x_u) + # ensure that original entry is in the list of choices + if not np.any([x_u == u.Unit(choice) for choice in choices]): + choices = [x_unit] + choices + self.spectral_unit.choices = choices + # in addition to the jdaviz options, allow the user to set any glue-valid unit + # which would then be appended on to the list of choices going forward + self.spectral_unit._addl_unit_strings = self.spectrum_viewer.state.__class__.x_display_unit.get_choices(self.spectrum_viewer.state) # noqa + self.spectral_unit.selected = x_unit + if not len(self.flux_unit.choices): + # in case flux_unit was triggered first (but could not be set because there + # as no spectral_unit to determine valid equivalencies) + self._on_glue_y_display_unit_changed(self.spectrum_viewer.state.y_display_unit) + + def _on_glue_y_display_unit_changed(self, y_unit): + if y_unit is None: + return + if self.spectral_unit.selected == "": + # no spectral unit set yet, cannot determine equivalencies + # setting the spectral unit will check len(flux_unit.choices) and call this manually + # in the case that that is triggered second. + return + self.spectrum_viewer.set_plot_axes() + if y_unit != self.flux_unit.selected: + x_u = u.Unit(self.spectral_unit.selected) + y_unit = _valid_glue_display_unit(y_unit, self.spectrum_viewer, 'y') + y_u = u.Unit(y_unit) + choices = create_flux_equivalencies_list(y_u, x_u) + # ensure that original entry is in the list of choices + if not np.any([y_u == u.Unit(choice) for choice in choices]): + choices = [y_unit] + choices + self.flux_unit.choices = choices + self.flux_unit.selected = y_unit + + @observe('spectral_unit_selected') + def _on_spectral_unit_changed(self, *args): + self.hub.broadcast(GlobalDisplayUnitChanged('spectral', + self.spectral_unit.selected, + sender=self)) + xunit = _valid_glue_display_unit(self.spectral_unit.selected, self.spectrum_viewer, 'x') + if self.spectrum_viewer.state.x_display_unit != xunit: + self.spectrum_viewer.state.x_display_unit = xunit + + @observe('flux_unit_selected') + def _on_flux_unit_changed(self, *args): + self.hub.broadcast(GlobalDisplayUnitChanged('flux', + self.flux_unit.selected, + sender=self)) + yunit = _valid_glue_display_unit(self.flux_unit.selected, self.spectrum_viewer, 'y') + if self.spectrum_viewer.state.y_display_unit != yunit: + self.spectrum_viewer.state.y_display_unit = yunit diff --git a/jdaviz/configs/specviz/plugins/unit_conversion/unit_conversion.vue b/jdaviz/configs/specviz/plugins/unit_conversion/unit_conversion.vue index fac0a6d9e8..f544f5733f 100644 --- a/jdaviz/configs/specviz/plugins/unit_conversion/unit_conversion.vue +++ b/jdaviz/configs/specviz/plugins/unit_conversion/unit_conversion.vue @@ -3,72 +3,31 @@ - - - - + > - - - - - - - - - + > - - - - Apply - - - diff --git a/jdaviz/configs/specviz/plugins/viewers.py b/jdaviz/configs/specviz/plugins/viewers.py index 51f9e64ef1..2d5dc13b23 100644 --- a/jdaviz/configs/specviz/plugins/viewers.py +++ b/jdaviz/configs/specviz/plugins/viewers.py @@ -351,10 +351,23 @@ def add_data(self, data, color=None, alpha=None, **layer_state): result : bool `True` if successful, `False` otherwise. """ + # If this is the first loaded data, set things up for unit conversion. + if len(self.layers) == 0: + reset_plot_axes = True + else: + reset_plot_axes = False + # The base class handles the plotting of the main # trace representing the spectrum itself. result = super().add_data(data, color, alpha, **layer_state) + if reset_plot_axes: + x_units = data.get_component(self.state.x_att.label).units + y_units = data.get_component("flux").units + self.state.x_display_unit = x_units if len(x_units) else None + self.state.y_display_unit = y_units if len(y_units) else None + self.set_plot_axes() + self._plot_uncertainties() self._plot_mask() @@ -477,22 +490,21 @@ def _plot_uncertainties(self): self.figure.marks = list(self.figure.marks) + [error_line_mark] def set_plot_axes(self): - # Get data to be used for axes labels - data = self.data()[0] - # Set axes labels for the spectrum viewer - spectral_axis_unit_type = str(data.spectral_axis.unit.physical_type).title() - # flux_unit_type = data.flux.unit.physical_type.title() flux_unit_type = "Flux density" - - if data.spectral_axis.unit.is_equivalent(u.m): + x_disp_unit = self.state.x_display_unit + x_unit = u.Unit(x_disp_unit) if x_disp_unit else u.dimensionless_unscaled + if x_unit.is_equivalent(u.m): spectral_axis_unit_type = "Wavelength" - elif data.spectral_axis.unit.is_equivalent(u.pixel): - spectral_axis_unit_type = "pixel" + elif x_unit.is_equivalent(u.Hz): + spectral_axis_unit_type = "Frequency" + elif x_unit.is_equivalent(u.pixel): + spectral_axis_unit_type = "Pixel" + else: + spectral_axis_unit_type = str(x_unit.physical_type).title() - label_0 = f"{spectral_axis_unit_type} [{data.spectral_axis.unit.to_string()}]" - self.figure.axes[0].label = label_0 - self.figure.axes[1].label = f"{flux_unit_type} [{data.flux.unit.to_string()}]" + self.figure.axes[0].label = f"{spectral_axis_unit_type} [{self.state.x_display_unit}]" + self.figure.axes[1].label = f"{flux_unit_type} [{self.state.y_display_unit}]" # Make it so y axis label is not covering tick numbers. self.figure.axes[1].label_offset = "-50" diff --git a/jdaviz/configs/specviz/tests/test_helper.py b/jdaviz/configs/specviz/tests/test_helper.py index 12603091ff..0f47db0d02 100644 --- a/jdaviz/configs/specviz/tests/test_helper.py +++ b/jdaviz/configs/specviz/tests/test_helper.py @@ -10,7 +10,6 @@ from astropy.utils.data import download_file from jdaviz.app import Application -from jdaviz.configs.specviz.plugins.unit_conversion import unit_conversion as uc from jdaviz.core.marks import LineUncertainties from jdaviz import Specviz @@ -277,7 +276,7 @@ def test_get_spectral_regions_unit_conversion(specviz_helper, spectrum1d): # If the reference (visible) data changes via unit conversion, # check that the region's units convert too - specviz_helper.load_spectrum(spectrum1d) + specviz_helper.load_spectrum(spectrum1d) # Originally Angstrom # Also check coordinates info panel. # x=0 -> 6000 A, x=1 -> 6222.222 A @@ -293,27 +292,22 @@ def test_get_spectral_regions_unit_conversion(specviz_helper, spectrum1d): assert label_mouseover.as_text() == ('', '', '') assert label_mouseover.icon == '' - # Convert the wavelength axis to microns - new_spectral_axis = "micron" - conv_func = uc.UnitConversion.process_unit_conversion - converted_spectrum = conv_func(specviz_helper.app, spectrum=spectrum1d, - new_spectral_axis=new_spectral_axis) - - # Add this new data and clear the other, making the converted spectrum our reference - specviz_helper.app.add_data(converted_spectrum, "Converted Spectrum") - specviz_helper.app.add_data_to_viewer("spectrum-viewer", - "Converted Spectrum", - clear_other_data=True) + # Convert the wavelength axis to micron + new_spectral_axis = "um" + specviz_helper.plugins['Unit Conversion'].spectral_unit = new_spectral_axis - specviz_helper.app.get_viewer("spectrum-viewer").apply_roi(XRangeROI(0.6, 0.7)) + spec_viewer.apply_roi(XRangeROI(0.6, 0.7)) - # TODO: Is this test still relevant with the upcoming glue unit conversion changes? # Retrieve the Subset - # subsets = specviz_helper.get_spectral_regions() - # reg = subsets.get('Subset 1') - # - # assert reg.lower.unit == u.Unit(new_spectral_axis) - # assert reg.upper.unit == u.Unit(new_spectral_axis) + subsets = specviz_helper.get_spectral_regions(use_display_units=False) + reg = subsets.get('Subset 1') + assert reg.lower.unit == u.Angstrom + assert reg.upper.unit == u.Angstrom + + subsets = specviz_helper.get_spectral_regions(use_display_units=True) + reg = subsets.get('Subset 1') + assert reg.lower.unit == u.um + assert reg.upper.unit == u.um # Coordinates info panel should show new unit label_mouseover._viewer_mouse_event(spec_viewer, @@ -321,7 +315,7 @@ def test_get_spectral_regions_unit_conversion(specviz_helper, spectrum1d): label_mouseover.as_text() == ('Cursor 6.10000e-01, 1.25000e+01', 'Wave 6.00000e-01 micron (0 pix)', 'Flux 1.24967e+01 Jy') - assert label_mouseover.icon == 'b' + assert label_mouseover.icon == 'a' label_mouseover._viewer_mouse_event(spec_viewer, {'event': 'mouseleave'}) assert label_mouseover.as_text() == ('', '', '') diff --git a/jdaviz/configs/specviz2d/specviz2d.yaml b/jdaviz/configs/specviz2d/specviz2d.yaml index d62dac9ed9..6126211888 100644 --- a/jdaviz/configs/specviz2d/specviz2d.yaml +++ b/jdaviz/configs/specviz2d/specviz2d.yaml @@ -21,7 +21,6 @@ tray: - spectral-extraction - g-gaussian-smooth - g-model-fitting - - g-unit-conversion - g-line-list - specviz-line-analysis - g-export-plot diff --git a/jdaviz/configs/specviz2d/tests/test_parsers.py b/jdaviz/configs/specviz2d/tests/test_parsers.py index 1e4fea74b8..78a4096068 100644 --- a/jdaviz/configs/specviz2d/tests/test_parsers.py +++ b/jdaviz/configs/specviz2d/tests/test_parsers.py @@ -81,7 +81,7 @@ def test_2d_parser_no_unit(specviz2d_helper, mos_spectrum2d): label_mouseover._viewer_mouse_event(viewer_1d, {'event': 'mousemove', 'domain': {'x': 6.5, 'y': 3}}) assert label_mouseover.as_text() == ('Cursor 6.50000e+00, 3.00000e+00', - 'Wave 6.00000e+00 pix', + 'Wave 6.00000e+00 pixel', 'Flux -3.59571e+00') assert label_mouseover.icon == 'b' diff --git a/jdaviz/core/data_formats.py b/jdaviz/core/data_formats.py index 7caa29253b..c26104c663 100644 --- a/jdaviz/core/data_formats.py +++ b/jdaviz/core/data_formats.py @@ -11,6 +11,8 @@ from stdatamodels import asdf_in_fits from jdaviz.core.config import list_configurations +from jdaviz import configs as jdaviz_configs +from jdaviz.cli import DEFAULT_VERBOSITY, DEFAULT_HISTORY_VERBOSITY __all__ = [ 'guess_dimensionality', @@ -156,6 +158,9 @@ def identify_helper(filename, ext=1): ------- helper_name : str Name of the best-guess helper for ``filename``. + + Fits HDUList : astropy.io.fits.HDUList + The HDUList of the file opened to identify the helper """ supported_dtypes = [ Spectrum1D, @@ -167,10 +172,12 @@ def identify_helper(filename, ext=1): if filename.lower().endswith('asdf'): # ASDF files are only supported in jdaviz for # Roman WFI 2D images, so suggest imviz: - return 'imviz' + return ('imviz', None) - header = fits.getheader(filename, ext=ext) - data = fits.getdata(filename, ext=ext) + # Must use memmap=False to force close all handles and allow file overwrite + hdul = fits.open(filename, memmap=False) + data = hdul[ext] + header = data.header wcs = _get_wcs(filename, header) has_spectral_axis = 'spectral' in wcs.world_axis_object_classes @@ -201,10 +208,10 @@ def identify_helper(filename, ext=1): # could be 2D spectrum or 2D image. break tie with WCS: if has_spectral_axis: if n_axes > 1: - return 'specviz2d' - return 'specviz' + return ('specviz2d', hdul) + return ('specviz', hdul) elif not isinstance(data, fits.BinTableHDU): - return 'imviz' + return ('imviz', hdul) # Ensure specviz is chosen when ``data`` is a table or recarray # and there's a "known" spectral column name: @@ -230,7 +237,7 @@ def identify_helper(filename, ext=1): # if at least one spectral column is found: if sum(found_spectral_columns): - return 'specviz' + return ('specviz', hdul) # If the data could be spectral: for cls in [Spectrum1D, SpectrumList]: @@ -240,10 +247,10 @@ def identify_helper(filename, ext=1): # first catch known JWST spectrum types: if (n_axes == 3 and recognized_spectrum_format.find('s3d') > -1): - return 'cubeviz' + return ('cubeviz', hdul) elif (n_axes == 2 and recognized_spectrum_format.find('x1d') > -1): - return 'specviz' + return ('specviz', hdul) # we intentionally don't choose specviz2d for # data recognized as 's2d' as we did with the cases above, @@ -253,22 +260,65 @@ def identify_helper(filename, ext=1): # Use WCS to break the tie below: elif n_axes == 2: if has_spectral_axis: - return 'specviz2d' - return 'imviz' + return ('specviz2d', hdul) + return ('imviz', hdul) elif n_axes == 1: - return 'specviz' + return ('specviz', hdul) try: # try using the specutils registry: valid_format, config = identify_data(filename) - return config + return (config, hdul) except ValueError: # if file type not recognized: pass if n_axes == 2 and not has_spectral_axis: # at this point, non-spectral 2D data are likely images: - return 'imviz' + return ('imviz', hdul) raise ValueError(f"No helper could be auto-identified for {filename}.") + + +def open(filename, show=True, **kwargs): + ''' + Automatically detect the correct configuration based on a given file, + load the data, and display the configuration + + Parameters + ---------- + filename : str (path-like) + Name for a local data file. + show : bool + Determines whether to immediately show the application + + All other arguments are interpreted as load_data/load_spectrum arguments for + the autoidentified configuration class + + Returns + ------- + Jdaviz ConfigHelper : jdaviz.core.helpers.ConfigHelper + The autoidentified ConfigHelper for the given data + ''' + # Identify the correct config + helper_str, hdul = identify_helper(filename) + viz_class = getattr(jdaviz_configs, helper_str.capitalize()) + + # Create config instance + verbosity = kwargs.pop('verbosity', DEFAULT_VERBOSITY) + history_verbosity = kwargs.pop('history_verbosity', DEFAULT_HISTORY_VERBOSITY) + viz_helper = viz_class(verbosity=verbosity, history_verbosity=history_verbosity) + + # Load data + data = hdul if (hdul is not None) else filename + if helper_str == "specviz": + viz_helper.load_spectrum(data, **kwargs) + else: + viz_helper.load_data(data, **kwargs) + + # Display app + if show: + viz_helper.show() + + return viz_helper diff --git a/jdaviz/core/events.py b/jdaviz/core/events.py index 46126bb65e..1bfbb9d9b6 100644 --- a/jdaviz/core/events.py +++ b/jdaviz/core/events.py @@ -1,3 +1,4 @@ +import astropy.units as u from glue.core.message import Message __all__ = ['NewViewerMessage', 'ViewerAddedMessage', 'ViewerRemovedMessage', 'LoadDataMessage', @@ -6,7 +7,8 @@ 'SliceSelectSliceMessage', 'SliceToolStateMessage', 'TableClickMessage', 'LinkUpdatedMessage', 'ExitBatchLoadMessage', - 'MarkersChangedMessage', 'CanvasRotationChangedMessage'] + 'MarkersChangedMessage', 'CanvasRotationChangedMessage', + 'GlobalDisplayUnitChanged'] class NewViewerMessage(Message): @@ -337,3 +339,19 @@ def angle(self): @property def flip_horizontal(self): return self._flip_horizontal + + +class GlobalDisplayUnitChanged(Message): + '''Message generated when the global app-wide display unit is changed''' + def __init__(self, axis, unit, *args, **kwargs): + super().__init__(*args, **kwargs) + self._axis = axis + self._unit = unit + + @property + def axis(self): + return self._axis + + @property + def unit(self): + return u.Unit(self._unit) diff --git a/jdaviz/core/freezable_state.py b/jdaviz/core/freezable_state.py index 0d1a14600f..6405bf035b 100644 --- a/jdaviz/core/freezable_state.py +++ b/jdaviz/core/freezable_state.py @@ -8,7 +8,7 @@ __all__ = ['FreezableState', 'FreezableProfileViewerState', 'FreezableBqplotImageViewerState'] -class FreezableState(): +class FreezableState: _frozen_state = [] def __setattr__(self, k, v): diff --git a/jdaviz/core/helpers.py b/jdaviz/core/helpers.py index 7a4c171fcd..986547f28b 100644 --- a/jdaviz/core/helpers.py +++ b/jdaviz/core/helpers.py @@ -412,8 +412,32 @@ def show_in_new_tab(self, title=None): # pragma: no cover return self.show(loc="sidecar:tab-after", title=title) def _get_data(self, data_label=None, spatial_subset=None, spectral_subset=None, - mask_subset=None, function=None, cls=None): - # Start validity checks + mask_subset=None, function=None, cls=None, use_display_units=False): + def _handle_display_units(data, use_display_units): + if use_display_units: + if isinstance(data, Spectrum1D): + spectral_unit = self.app._get_display_unit('spectral') + if not spectral_unit: + return data + if self.app.config == 'cubeviz' and spectral_unit == 'deg': + # this happens before the correct axis is set for the spectrum-viewer + # and would result in a unit-conversion error if attempting to convert + # to the display units. This should only ever be temporary during + # app intialization. + return data + flux_unit = self.app._get_display_unit('flux') + # TODO: any other attributes (meta, wcs, etc)? + # TODO: implement uncertainty.to upstream + new_uncert = data.uncertainty.__class__(data.uncertainty.quantity.to(flux_unit)) if data.uncertainty is not None else None # noqa + data = Spectrum1D(spectral_axis=data.spectral_axis.to(spectral_unit, + u.spectral()), + flux=data.flux.to(flux_unit, + u.spectral_density(data.spectral_axis)), + uncertainty=new_uncert) + else: # pragma: nocover + raise NotImplementedError(f"converting {data.__class__.__name__} to display units is not supported") # noqa + return data + list_of_valid_function_values = ('minimum', 'maximum', 'mean', 'median', 'sum') if function and function not in list_of_valid_function_values: @@ -471,7 +495,7 @@ def _get_data(self, data_label=None, spatial_subset=None, spectral_subset=None, else: data = data.get_object(cls=cls, **object_kwargs) - return data + return _handle_display_units(data, use_display_units) if not cls and spatial_subset: raise AttributeError(f"A valid cls must be provided to" @@ -528,9 +552,9 @@ def _get_data(self, data_label=None, spatial_subset=None, spectral_subset=None, else: data = spec_subset - return data + return _handle_display_units(data, use_display_units) - def get_data(self, data_label=None, cls=None): + def get_data(self, data_label=None, cls=None, use_display_units=False): """ Returns data with name equal to data_label of type cls. @@ -540,6 +564,8 @@ def get_data(self, data_label=None, cls=None): Provide a label to retrieve a specific data set from data_collection. cls : `~specutils.Spectrum1D`, `~astropy.nddata.CCDData`, optional The type that data will be returned as. + use_display_units: bool, optional + Whether to convert to the display units defined in the plugin. Returns ------- @@ -548,7 +574,8 @@ def get_data(self, data_label=None, cls=None): """ return self._get_data(data_label=data_label, spatial_subset=None, - spectral_subset=None, function=None, cls=None) + spectral_subset=None, function=None, + cls=None, use_display_units=use_display_units) class ImageConfigHelper(ConfigHelper): @@ -658,12 +685,17 @@ def load_regions(self, regions, max_num_regions=None, refdata_label=None, If not requested, return `None`. """ + if len(self.app.data_collection) == 0: + raise ValueError('Cannot load regions without data.') + from photutils.aperture import (CircularAperture, SkyCircularAperture, EllipticalAperture, SkyEllipticalAperture, - RectangularAperture, SkyRectangularAperture) + RectangularAperture, SkyRectangularAperture, + CircularAnnulus, SkyCircularAnnulus) from regions import (Regions, CirclePixelRegion, CircleSkyRegion, EllipsePixelRegion, EllipseSkyRegion, - RectanglePixelRegion, RectangleSkyRegion) + RectanglePixelRegion, RectangleSkyRegion, + CircleAnnulusPixelRegion, CircleAnnulusSkyRegion) from jdaviz.core.region_translators import regions2roi, aperture2regions # If user passes in one region obj instead of list, try to be smart. @@ -688,23 +720,27 @@ def load_regions(self, regions, max_num_regions=None, refdata_label=None, has_wcs = data_has_valid_wcs(data, ndim=2) for region in regions: - if isinstance(region, (SkyCircularAperture, SkyEllipticalAperture, - SkyRectangularAperture, CircleSkyRegion, - EllipseSkyRegion, RectangleSkyRegion)) and not has_wcs: + if (isinstance(region, (SkyCircularAperture, SkyEllipticalAperture, + SkyRectangularAperture, SkyCircularAnnulus, + CircleSkyRegion, EllipseSkyRegion, + RectangleSkyRegion, CircleAnnulusSkyRegion)) + and not has_wcs): bad_regions.append((region, 'Sky region provided but data has no valid WCS')) continue # photutils: Convert to regions shape first if isinstance(region, (CircularAperture, SkyCircularAperture, EllipticalAperture, SkyEllipticalAperture, - RectangularAperture, SkyRectangularAperture)): + RectangularAperture, SkyRectangularAperture, + CircularAnnulus, SkyCircularAnnulus)): region = aperture2regions(region) # regions: Convert to ROI. # NOTE: Out-of-bounds ROI will succeed; this is native glue behavior. if isinstance(region, (CirclePixelRegion, CircleSkyRegion, EllipsePixelRegion, EllipseSkyRegion, - RectanglePixelRegion, RectangleSkyRegion)): + RectanglePixelRegion, RectangleSkyRegion, + CircleAnnulusPixelRegion, CircleAnnulusSkyRegion)): state = regions2roi(region, wcs=data.coords) # TODO: Do we want user to specify viewer? Does it matter? diff --git a/jdaviz/core/marks.py b/jdaviz/core/marks.py index 2a6590d711..ae2307ec13 100644 --- a/jdaviz/core/marks.py +++ b/jdaviz/core/marks.py @@ -7,13 +7,14 @@ from glue.core import HubListener from specutils import Spectrum1D +from jdaviz.core.events import GlobalDisplayUnitChanged from jdaviz.core.events import (SliceToolStateMessage, LineIdentifyMessage, SpectralMarksChangedMessage, RedshiftMessage) __all__ = ['OffscreenLinesMarks', 'BaseSpectrumVerticalLine', 'SpectralLine', 'SliceIndicatorMarks', 'ShadowMixin', 'ShadowLine', 'ShadowLabelFixedY', - 'PluginMark', 'PluginLine', 'PluginScatter', + 'PluginMark', 'LinesAutoUnit', 'PluginLine', 'PluginScatter', 'LineAnalysisContinuum', 'LineAnalysisContinuumCenter', 'LineAnalysisContinuumLeft', 'LineAnalysisContinuumRight', 'LineUncertainties', 'ScatterMask', 'SelectedSpaxel', 'MarkersMark'] @@ -58,11 +59,85 @@ def _update_counts(self, *args): self.right.text = [f'{oob_right} \u25b6' if oob_right > 0 else ''] -class BaseSpectrumVerticalLine(Lines, HubListener): +class PluginMark(): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.xunit = None + self.yunit = None + # whether to update existing marks when global display units are changed + self.auto_update_units = True + self.hub.subscribe(self, GlobalDisplayUnitChanged, + handler=self._on_global_display_unit_changed) + + if self.xunit is None: + self.set_x_unit() + if self.yunit is None: + self.set_y_unit() + + @property + def hub(self): + return self.viewer.hub + + def update_xy(self, x, y): + self.x = np.asarray(x) + self.y = np.asarray(y) + + def append_xy(self, x, y): + self.x = np.append(self.x, x) + self.y = np.append(self.y, y) + + def set_x_unit(self, unit=None): + if unit is None: + if not hasattr(self.viewer.state, 'x_display_unit'): + return + unit = self.viewer.state.x_display_unit + unit = u.Unit(unit) + + if self.xunit is not None and not np.all([s == 0 for s in self.x.shape]): + x = (self.x * self.xunit).to_value(unit, u.spectral()) + self.xunit = unit + self.x = x + self.xunit = unit + + def set_y_unit(self, unit=None): + if unit is None: + if not hasattr(self.viewer.state, 'y_display_unit'): + return + unit = self.viewer.state.y_display_unit + unit = u.Unit(unit) + + if self.yunit is not None and not np.all([s == 0 for s in self.y.shape]): + if self.viewer.default_class is Spectrum1D: + spec = self.viewer.state.reference_data.get_object(cls=Spectrum1D) + eqv = u.spectral_density(spec.spectral_axis) + y = (self.y * self.yunit).to_value(unit, equivalencies=eqv) + else: + y = (self.y * self.yunit).to_value(unit) + self.yunit = unit + self.y = y + + self.yunit = unit + + def _on_global_display_unit_changed(self, msg): + if not self.auto_update_units: + return + if self.viewer.__class__.__name__ in ['SpecvizProfileView', 'CubevizProfileView']: + axis_map = {'spectral': 'x', 'flux': 'y'} + elif self.viewer.__class__.__name__ == 'MosvizProfile2DView': + axis_map = {'spectral': 'x'} + else: + return + axis = axis_map.get(msg.axis, None) + if axis is not None: + getattr(self, f'set_{axis}_unit')(msg.unit) + + def clear(self): + self.update_xy([], []) + + +class BaseSpectrumVerticalLine(Lines, PluginMark, HubListener): def __init__(self, viewer, x, **kwargs): - # we'll store the current units so that we can automatically update the - # positioning on a change to the x-units - self._x_unit = viewer.state.reference_data.get_object(cls=Spectrum1D).spectral_axis.unit + self.viewer = viewer # the location of the marker will need to update automatically if the # underlying data changes (through a unit conversion, for example) @@ -83,14 +158,14 @@ def _update_reference_data(self, reference_data): def _update_data(self, x_all): # the x-units may have changed. We want to convert the internal self.x - # from self._x_unit to the new units (x_all.unit) + # from self.xunit to the new units (x_all.unit) new_unit = x_all.unit - if new_unit == self._x_unit: + if new_unit == self.xunit: return - old_quant = self.x[0]*self._x_unit + old_quant = self.x[0]*self.xunit x = old_quant.to_value(x_all.unit, equivalencies=u.spectral()) self.x = [x, x] - self._x_unit = new_unit + self.xunit = new_unit class SpectralLine(BaseSpectrumVerticalLine): @@ -110,7 +185,7 @@ def __init__(self, viewer, rest_value, redshift=0, name=None, **kwargs): # setting redshift will set self.x and enable the obs_value property, # but to do that we need x_unit set first (would normally be assigned # in the super init) - self._x_unit = viewer.state.reference_data.get_object(cls=Spectrum1D).spectral_axis.unit + self.xunit = u.Unit(viewer.state.x_display_unit) self.redshift = redshift viewer.session.hub.subscribe(self, LineIdentifyMessage, @@ -131,6 +206,11 @@ def rest_value(self): def obs_value(self): return self.x[0] + def set_x_unit(self, unit=None): + prev_unit = self.xunit + super().set_x_unit(unit=unit) + self._rest_value = (self._rest_value * prev_unit).to_value(unit, u.spectral()) + @property def redshift(self): return self._redshift @@ -138,16 +218,16 @@ def redshift(self): @redshift.setter def redshift(self, redshift): self._redshift = redshift - if str(self._x_unit.physical_type) == 'length': + if str(self.xunit.physical_type) == 'length': obs_value = self._rest_value*(1+redshift) - elif str(self._x_unit.physical_type) == 'frequency': + elif str(self.xunit.physical_type) == 'frequency': obs_value = self._rest_value/(1+redshift) else: # catch all for anything else (wavenumber, energy, etc) - rest_angstrom = (self._rest_value*self._x_unit).to_value(u.Angstrom, - equivalencies=u.spectral()) + rest_angstrom = (self._rest_value*self.xunit).to_value(u.Angstrom, + equivalencies=u.spectral()) obs_angstrom = rest_angstrom*(1+redshift) - obs_value = (obs_angstrom*u.Angstrom).to_value(self._x_unit, + obs_value = (obs_angstrom*u.Angstrom).to_value(self.xunit, equivalencies=u.spectral()) self.x = [obs_value, obs_value] @@ -168,14 +248,14 @@ def _process_identify_change(self, msg): def _update_data(self, x_all): new_unit = x_all.unit - if new_unit == self._x_unit: + if new_unit == self.xunit: return - old_quant = self._rest_value*self._x_unit + old_quant = self._rest_value*self.xunit self._rest_value = old_quant.to_value(new_unit, equivalencies=u.spectral()) # re-compute self.x from current redshift (instead of converting that as well) self.redshift = self._redshift - self._x_unit = new_unit + self.xunit = new_unit class SliceIndicatorMarks(BaseSpectrumVerticalLine, HubListener): @@ -268,7 +348,7 @@ def _update_colors_opacities(self): self.colors = ["#c75109" if self._active else "#007BA1"] self.opacities = [1.0 if self._active else 0.9] - def _on_change_state(self, msg): + def _on_change_state(self, msg={}): if isinstance(msg, dict): changes = msg else: @@ -484,33 +564,32 @@ def _on_shadowing_changed(self, change): self._update_align() -class PluginMark(): - def update_xy(self, x, y): - self.x = np.asarray(x) - self.y = np.asarray(y) - - def append_xy(self, x, y): - self.x = np.append(self.x, x) - self.y = np.append(self.y, y) - - def clear(self): - self.update_xy([], []) +class LinesAutoUnit(PluginMark, Lines, HubListener): + def __init__(self, viewer, *args, **kwargs): + self.viewer = viewer + super().__init__(*args, **kwargs) class PluginLine(Lines, PluginMark, HubListener): def __init__(self, viewer, x=[], y=[], **kwargs): + self.viewer = viewer # color is same blue as import button super().__init__(x=x, y=y, colors=["#007BA1"], scales=viewer.scales, **kwargs) class PluginScatter(Scatter, PluginMark, HubListener): def __init__(self, viewer, x=[], y=[], **kwargs): + self.viewer = viewer # color is same blue as import button super().__init__(x=x, y=y, colors=["#007BA1"], scales=viewer.scales, **kwargs) class LineAnalysisContinuum(PluginLine): - pass + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # units do not need to be updated because the plugin itself reruns + # the computation and automatically changes the arrays themselves + self.auto_update_units = False class LineAnalysisContinuumCenter(LineAnalysisContinuum): @@ -529,9 +608,9 @@ class LineAnalysisContinuumRight(LineAnalysisContinuumLeft): pass -class LineUncertainties(Lines): - def __init__(self, **kwargs): - super().__init__(**kwargs) +class LineUncertainties(LinesAutoUnit): + def __init__(self, viewer, *args, **kwargs): + super().__init__(viewer, *args, **kwargs) class ScatterMask(Scatter): diff --git a/jdaviz/core/region_translators.py b/jdaviz/core/region_translators.py index 17e6d3defe..df1d277fb0 100644 --- a/jdaviz/core/region_translators.py +++ b/jdaviz/core/region_translators.py @@ -3,7 +3,7 @@ """ from astropy import units as u from astropy.coordinates import Angle -from glue.core.roi import CircularROI, EllipticalROI, RectangularROI +from glue.core.roi import CircularROI, EllipticalROI, RectangularROI, CircularAnnulusROI from photutils.aperture import (CircularAperture, SkyCircularAperture, EllipticalAperture, SkyEllipticalAperture, RectangularAperture, SkyRectangularAperture, @@ -115,7 +115,8 @@ def regions2roi(region_shape, wcs=None): """ - if isinstance(region_shape, (CircleSkyRegion, EllipseSkyRegion, RectangleSkyRegion)): + if isinstance(region_shape, (CircleSkyRegion, EllipseSkyRegion, RectangleSkyRegion, + CircleAnnulusSkyRegion)): if wcs is None: raise ValueError(f'WCS must be provided for {region_shape}') @@ -140,6 +141,10 @@ def regions2roi(region_shape, wcs=None): roi = RectangularROI( xmin=xmin, xmax=xmax, ymin=ymin, ymax=ymax, theta=region_shape.angle.to_value(u.radian)) + elif isinstance(region_shape, CircleAnnulusPixelRegion): + roi = CircularAnnulusROI( + xc=region_shape.center.x, yc=region_shape.center.y, + inner_radius=region_shape.inner_radius, outer_radius=region_shape.outer_radius) else: raise NotImplementedError(f'{region_shape.__class__.__name__} is not supported') diff --git a/jdaviz/core/template_mixin.py b/jdaviz/core/template_mixin.py index da06034c0e..a8bb410257 100644 --- a/jdaviz/core/template_mixin.py +++ b/jdaviz/core/template_mixin.py @@ -14,6 +14,7 @@ SubsetDeleteMessage, SubsetUpdateMessage) from glue.core.subset import RoiSubsetState +from glue_jupyter.bqplot.image import BqplotImageView from glue_jupyter.widgets.linked_dropdown import get_choices as _get_glue_choices from specutils import Spectrum1D from traitlets import Any, Bool, HasTraits, List, Unicode, observe @@ -28,7 +29,8 @@ __all__ = ['show_widget', 'TemplateMixin', 'PluginTemplateMixin', - 'BasePluginComponent', 'SelectPluginComponent', + 'ViewerPropertiesMixin', + 'BasePluginComponent', 'SelectPluginComponent', 'UnitSelectPluginComponent', 'PluginSubcomponent', 'SubsetSelect', 'SpatialSubsetSelectMixin', 'SpectralSubsetSelectMixin', 'DatasetSpectralSubsetValidMixin', @@ -102,7 +104,32 @@ def _subset_type(subset): return 'spectral' -class TemplateMixin(VuetifyTemplate, HubListener): +class ViewerPropertiesMixin: + # assumes that self.app is defined by the class + @cached_property + def spectrum_viewer(self): + if hasattr(self, '_default_spectrum_viewer_reference_name'): + viewer_reference = self._default_spectrum_viewer_reference_name + else: + viewer_reference = self.app._get_first_viewer_reference_name( + require_spectrum_viewer=True + ) + + return self.app.get_viewer(viewer_reference) + + @cached_property + def spectrum_2d_viewer(self): + if hasattr(self, '_default_spectrum_2d_viewer_reference_name'): + viewer_reference = self._default_spectrum_2d_viewer_reference_name + else: + viewer_reference = self.app._get_first_viewer_reference_name( + require_spectrum_2d_viewer=True + ) + + return self.app.get_viewer(viewer_reference) + + +class TemplateMixin(VuetifyTemplate, HubListener, ViewerPropertiesMixin): config = Unicode("").tag(sync=True) vdocs = Unicode("").tag(sync=True) popout_button = Any().tag(sync=True, **widget_serialization) @@ -294,7 +321,7 @@ def show(self, loc="inline", title=None): # pragma: no cover show_widget(self, loc=loc, title=title) -class BasePluginComponent(HubListener): +class BasePluginComponent(HubListener, ViewerPropertiesMixin): """ This base class handles attaching traitlets from the plugin itself to logic handled within the component, support for caching and clearing caches on properties, @@ -376,28 +403,6 @@ def _dict_from_viewer(viewer, viewer_item): for vid, viewer in self.app._viewer_store.items() if viewer.__class__.__name__ != 'MosvizTableViewer'] - @cached_property - def spectrum_viewer(self): - if hasattr(self, '_default_spectrum_viewer_reference_name'): - viewer_reference = self._default_spectrum_viewer_reference_name - else: - viewer_reference = self.app._get_first_viewer_reference_name( - require_spectrum_viewer=True - ) - - return self._plugin.app.get_viewer(viewer_reference) - - @cached_property - def spectrum_2d_viewer(self): - if hasattr(self, '_default_spectrum_2d_viewer_reference_name'): - viewer_reference = self._default_spectrum_2d_viewer_reference_name - else: - viewer_reference = self.app._get_first_viewer_reference_name( - require_spectrum_2d_viewer=True - ) - - return self._plugin.app.get_viewer(viewer_reference) - class SelectPluginComponent(BasePluginComponent, HasTraits): """ @@ -474,6 +479,10 @@ def __hash__(self): def choices(self): return self.labels + @choices.setter + def choices(self, choices=[]): + self.items = [{'label': choice} for choice in choices] + @property def is_multiselect(self): if not hasattr(self, 'multiselect'): @@ -642,6 +651,48 @@ def _selected_changed(self, event): raise ValueError(f"{event['new']} not one of {self.labels}, reverting selection to {event['old']}") # noqa +class UnitSelectPluginComponent(SelectPluginComponent): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.add_observe('items', lambda _: self._clear_cache('unit_choices')) + self._addl_unit_strings = [] + + @cached_property + def unit_choices(self): + return [u.Unit(lbl) for lbl in self.labels] + + @property + def addl_unit_choices(self): + return [u.Unit(choice) for choice in self._addl_unit_strings] + + def _selected_changed(self, event): + self._clear_cache() + if event['new'] in self.labels + ['']: + # the string is an exact match, no converting necessary + return + elif not len(self.labels): + raise ValueError("no valid unit choices") + try: + new_u = u.Unit(event['new']) + except ValueError: + self.selected = event['old'] + raise ValueError(f"{event['new']} could not be converted to a valid unit, reverting selection to {event['old']}") # noqa + if new_u not in self.unit_choices: + if new_u in self.addl_unit_choices: + # append this one (as the valid string representation) to the list of user-choices + addl_index = self.addl_unit_choices.index(new_u) + self.choices = self.choices + [self._addl_unit_strings[addl_index]] + # clear the cache so we can find the appropriate entry in unit_choices + self._clear_cache('unit_choices') + else: + self.selected = event['old'] + raise ValueError(f"{event['new']} not one of {self.labels}, reverting selection to {event['old']}") # noqa + + # convert to default string representation from the valid choices + ind = self.unit_choices.index(new_u) + self.selected = self.labels[ind] + + class LayerSelect(SelectPluginComponent): """ Plugin select for layers, with support for single or multi-selection. @@ -1485,16 +1536,22 @@ def selected_obj(self): # from the data collection return self.get_object(cls=self.default_data_cls) - def selected_spectrum_for_spatial_subset(self, spatial_subset=SPATIAL_DEFAULT_TEXT): + def selected_spectrum_for_spatial_subset(self, + spatial_subset=SPATIAL_DEFAULT_TEXT, + use_display_units=True): if spatial_subset == SPATIAL_DEFAULT_TEXT: spatial_subset = None return self.plugin._specviz_helper.get_data(data_label=self.selected, - spatial_subset=spatial_subset) + spatial_subset=spatial_subset, + use_display_units=use_display_units) def _is_valid_item(self, data): def not_from_plugin(data): return data.meta.get('Plugin', None) is None + def not_from_this_plugin(data): + return data.meta.get('Plugin', None) != self.plugin.__class__.__name__ + def not_from_plugin_model_fitting(data): return data.meta.get('Plugin', None) != 'ModelFitting' @@ -1678,9 +1735,9 @@ class AutoTextFieldMixin(VuetifyTemplate, HubListener): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.auto_label = AddResults(self, 'label', - 'label_default', 'label_auto', - 'label_invalid_msg') + self.auto_label = AutoTextField(self, 'label', + 'label_default', 'label_auto', + 'label_invalid_msg') class AddResults(BasePluginComponent): @@ -1837,25 +1894,31 @@ def add_results_from_plugin(self, data_item, replace=None, label=None): if label is None: label = self.label - if replace is None: - replace = self.viewer.selected_reference != 'spectrum-viewer' - - if self.label_overwrite and len(self.add_to_viewer_items) <= 2: + if self.label_overwrite: # the switch for add_to_viewer is hidden, and so the loaded state of the overwritten # entry should be the same as the original entry (to avoid deleting reference data) - viewer_reference = self.add_to_viewer_items[-1]['reference'] - viewer_item = self.app._viewer_item_by_reference(viewer_reference) - viewer = self.app.get_viewer(viewer_reference) - viewer_loaded_labels = [layer.layer.label for layer in viewer.layers] - add_to_viewer_selected = viewer_reference if label in viewer_loaded_labels else 'None' # noqa - visible = label in viewer_item['visible_layers'] + add_to_viewer_refs = [] + add_to_viewer_vis = [] + for viewer_select_item in self.add_to_viewer_items[1:]: + # index 0 is for "None" + viewer_ref = viewer_select_item['reference'] + viewer_item = self.app._viewer_item_by_reference(viewer_ref) + viewer = self.app.get_viewer(viewer_ref) + viewer_loaded_labels = [layer.layer.label for layer in viewer.layers] + if label in viewer_loaded_labels: + add_to_viewer_refs.append(viewer_ref) + add_to_viewer_vis.append(label in viewer_item['visible_layers']) else: - add_to_viewer_selected = self.add_to_viewer_selected - visible = True + if self.add_to_viewer_selected == 'None': + add_to_viewer_refs = [] + add_to_viewer_vis = [] + else: + add_to_viewer_refs = [self.add_to_viewer_selected] + add_to_viewer_vis = [True] if label in self.app.data_collection: - if add_to_viewer_selected != 'None': - self.app.remove_data_from_viewer(self.viewer.selected_reference, label) + for viewer_ref in add_to_viewer_refs: + self.app.remove_data_from_viewer(viewer_ref, label) self.app.data_collection.remove(self.app.data_collection[label]) if not hasattr(data_item, 'meta'): @@ -1865,12 +1928,17 @@ def add_results_from_plugin(self, data_item, replace=None, label=None): data_item.meta['mosviz_row'] = self.app.state.settings['mosviz_row'] self.app.add_data(data_item, label) - if add_to_viewer_selected != 'None': + for viewer_ref, visible in zip(add_to_viewer_refs, add_to_viewer_vis): # replace the contents in the selected viewer with the results from this plugin - # TODO: switch to an instance/classname check? - self.app.add_data_to_viewer(self.viewer.selected_id, + if replace is not None: + this_replace = replace + else: + this_viewer = self.app.get_viewer(viewer_ref) + this_replace = isinstance(this_viewer, BqplotImageView) + + self.app.add_data_to_viewer(viewer_ref, label, - visible=visible, clear_other_data=replace) + visible=visible, clear_other_data=this_replace) # update overwrite warnings, etc self._on_label_changed() diff --git a/jdaviz/core/tests/test_autoconfig.py b/jdaviz/core/tests/test_autoconfig.py new file mode 100644 index 0000000000..94ac011e6e --- /dev/null +++ b/jdaviz/core/tests/test_autoconfig.py @@ -0,0 +1,41 @@ +# Tests automatic config detection against our example notebook data + +from pathlib import Path +from tempfile import TemporaryDirectory + +import pytest +from astroquery.mast import Observations +from astropy.utils.data import download_file + +from jdaviz import open as jdaviz_open +from jdaviz.configs import Specviz2d, Cubeviz, Imviz # , Specviz + + +@pytest.mark.remote_data +@pytest.mark.filterwarnings('ignore') +@pytest.mark.parametrize('uris', ( + # ("mast:JWST/product/jw02732-o004_t004_miri_ch1-shortmediumlong_x1d.fits", Specviz), + # Specviz check disabled due to https://github.com/spacetelescope/jdaviz/issues/2229 + ("mast:JWST/product/jw01538-o160_s00004_nirspec_f170lp-g235h-s1600a1-sub2048_s2d.fits", Specviz2d), # noqa + ("mast:JWST/product/jw02727-o002_t062_nircam_clear-f090w_i2d.fits", Imviz), + ("mast:JWST/product/jw02732-o004_t004_miri_ch1-shortmediumlong_s3d.fits", Cubeviz), + ("https://stsci.box.com/shared/static/28a88k1qfipo4yxc4p4d40v4axtlal8y.fits", Cubeviz) + # Check that MaNGA cubes go to cubeviz. This file is originally from: + # https://data.sdss.org/sas/dr14/manga/spectro/redux/v2_1_2/7495/stack/manga-7495-12704-LOGCUBE.fits.gz) +)) +def test_autoconfig(uris): + # Setup temporary directory + with TemporaryDirectory(ignore_cleanup_errors=True) as tempdir: + uri = uris[0] + helper_class = uris[1] + + if uri.startswith("mast:"): + download_path = str(Path(tempdir) / Path(uri).name) + Observations.download_file(uri, local_path=download_path) + elif uri.startswith("http"): + download_path = download_file(uri, cache=True, timeout=100) + + viz_helper = jdaviz_open(download_path, show=False) + + assert type(viz_helper) == helper_class + assert len(viz_helper.app.data_collection) > 0 diff --git a/jdaviz/core/tests/test_data_menu.py b/jdaviz/core/tests/test_data_menu.py index 9c18584630..f092c2994c 100644 --- a/jdaviz/core/tests/test_data_menu.py +++ b/jdaviz/core/tests/test_data_menu.py @@ -110,17 +110,6 @@ def test_visibility_toggle(imviz_helper): def test_auto_config_detection(uri, expected_helper): url = f'https://mast.stsci.edu/api/v0.1/Download/file/?uri={uri}' fn = download_file(url, timeout=100) - helper_name = identify_helper(fn) + helper_name, hdul = identify_helper(fn) + hdul.close() assert helper_name == expected_helper - - -@pytest.mark.remote_data -@pytest.mark.filterwarnings(r"ignore::astropy.wcs.wcs.FITSFixedWarning") -def test_auto_config_manga(): - # Check that MaNGA cubes go to cubeviz. This file is - # originally from - # https://data.sdss.org/sas/dr14/manga/spectro/redux/v2_1_2/7495/stack/manga-7495-12704-LOGCUBE.fits.gz - URL = 'https://stsci.box.com/shared/static/28a88k1qfipo4yxc4p4d40v4axtlal8y.fits' - fn = download_file(URL, cache=True, timeout=100) - helper_name = identify_helper(fn) - assert helper_name == 'cubeviz' diff --git a/jdaviz/core/tests/test_tools.py b/jdaviz/core/tests/test_tools.py index b281486965..81f4400276 100644 --- a/jdaviz/core/tests/test_tools.py +++ b/jdaviz/core/tests/test_tools.py @@ -1,28 +1,22 @@ -import numpy as np from numpy.testing import assert_allclose -from glue.core import Data -def test_boxzoom(cubeviz_helper, spectral_cube_wcs): - data = Data(flux=np.ones((128, 128, 256)), label='Test Flux', coords=spectral_cube_wcs) - cubeviz_helper.app.data_collection.append(data) - - cubeviz_helper.app.add_data_to_viewer('spectrum-viewer', 'Test Flux') - cubeviz_helper.app.add_data_to_viewer('flux-viewer', 'Test Flux') +def test_boxzoom(cubeviz_helper, image_cube_hdu_obj_microns): + cubeviz_helper.load_data(image_cube_hdu_obj_microns, data_label="Test Flux") flux_viewer = cubeviz_helper.app.get_viewer('flux-viewer') assert flux_viewer.state.y_min == -0.5 - assert flux_viewer.state.y_max == 127.5 + assert flux_viewer.state.y_max == 8.5 assert flux_viewer.state.x_min == -0.5 - assert flux_viewer.state.x_max == 127.5 + assert flux_viewer.state.x_max == 9.5 t = flux_viewer.toolbar.tools['jdaviz:boxzoom'] t.activate() - t.interact.selected_x = [10, 20] - t.interact.selected_y = [20, 60] + t.interact.selected_x = [1, 4] + t.interact.selected_y = [2, 6] - assert t.get_x_axis_with_aspect_ratio() == (-5., 35.) + assert_allclose(t.get_x_axis_with_aspect_ratio(), [0.277778, 4.722222], rtol=1e-6) def _get_lims(viewer): diff --git a/jdaviz/core/user_api.py b/jdaviz/core/user_api.py index 9ef69ed4f6..4a31da9197 100644 --- a/jdaviz/core/user_api.py +++ b/jdaviz/core/user_api.py @@ -1,3 +1,5 @@ +import astropy.units as u + __all__ = ['UserApiWrapper', 'PluginUserApi'] @@ -38,12 +40,15 @@ def __setattr__(self, attr, value): exp_obj = getattr(self._obj, attr) from jdaviz.core.template_mixin import (SelectPluginComponent, + UnitSelectPluginComponent, PlotOptionsSyncState, AddResults, AutoTextField) if isinstance(exp_obj, SelectPluginComponent): # this allows setting the selection directly without needing to access the underlying # .selected traitlet + if isinstance(exp_obj, UnitSelectPluginComponent) and isinstance(value, u.Unit): + value = value.to_string() exp_obj.selected = value return elif isinstance(exp_obj, AddResults): diff --git a/jdaviz/core/validunits.py b/jdaviz/core/validunits.py index 01a939ead0..372a5f2904 100644 --- a/jdaviz/core/validunits.py +++ b/jdaviz/core/validunits.py @@ -1,5 +1,4 @@ -import astropy.units as u -import numpy as np +from astropy import units as u __all__ = ['units_to_strings', 'create_spectral_equivalencies_list', 'create_flux_equivalencies_list'] @@ -19,68 +18,61 @@ def units_to_strings(unit_list): result : list A list of the units with their best (i.e., most readable) string version. """ - return [u.Unit(unit).name - if u.Unit(unit) == u.Unit("Angstrom") - else u.Unit(unit).long_names[0] if ( - hasattr(u.Unit(unit), "long_names") and len(u.Unit(unit).long_names) > 0) - else u.Unit(unit).to_string() - for unit in unit_list] + return [u.Unit(unit).to_string() for unit in unit_list] -def create_spectral_equivalencies_list(spectrum, +def create_spectral_equivalencies_list(spectral_axis_unit, exclude=[u.jupiterRad, u.earthRad, u.solRad, - u.lyr, u.AU, u.pc]): - """Get all possible conversions from current spectral_axis_unit. - """ - if spectrum.spectral_axis.unit == u.pix: + u.lyr, u.AU, u.pc, u.Bq, u.micron, u.lsec]): + """Get all possible conversions from current spectral_axis_unit.""" + if spectral_axis_unit in (u.pix, u.dimensionless_unscaled): return [] # Get unit equivalencies. - curr_spectral_axis_unit_equivalencies = u.Unit( - spectrum.spectral_axis.unit).find_equivalent_units( - equivalencies=u.spectral()) + try: + curr_spectral_axis_unit_equivalencies = spectral_axis_unit.find_equivalent_units( + equivalencies=u.spectral()) + except u.core.UnitConversionError: + return [] # Get local units. - locally_defined_spectral_axis_units = ['angstrom', 'nanometer', - 'micron', 'hertz', 'erg'] + locally_defined_spectral_axis_units = ['Angstrom', 'nm', + 'um', 'Hz', 'erg'] local_units = [u.Unit(unit) for unit in locally_defined_spectral_axis_units] # Remove overlap units. curr_spectral_axis_unit_equivalencies = list(set(curr_spectral_axis_unit_equivalencies) - - set(local_units+exclude)) + - set(local_units + exclude)) # Convert equivalencies into readable versions of the units and sorted alphabetically. spectral_axis_unit_equivalencies_titles = sorted(units_to_strings( curr_spectral_axis_unit_equivalencies)) # Concatenate both lists with the local units coming first. - spectral_axis_unit_equivalencies_titles = sorted(units_to_strings( - local_units)) + spectral_axis_unit_equivalencies_titles - - return spectral_axis_unit_equivalencies_titles + return sorted(units_to_strings(local_units)) + spectral_axis_unit_equivalencies_titles -def create_flux_equivalencies_list(spectrum): - """Get all possible conversions for flux from current flux units. - """ - if ((spectrum.flux.unit == u.count) or (spectrum.spectral_axis.unit == u.pix)): +def create_flux_equivalencies_list(flux_unit, spectral_axis_unit): + """Get all possible conversions for flux from current flux units.""" + if ((flux_unit in (u.count, (u.MJy / u.sr), u.dimensionless_unscaled)) + or (spectral_axis_unit in (u.pix, u.dimensionless_unscaled))): return [] - # Get unit equivalencies. - curr_flux_unit_equivalencies = u.Unit( - spectrum.flux.unit).find_equivalent_units( - equivalencies=u.spectral_density(np.sum(spectrum.spectral_axis)), + # Get unit equivalencies. Value passed into u.spectral_density() is irrelevant. + try: + curr_flux_unit_equivalencies = flux_unit.find_equivalent_units( + equivalencies=u.spectral_density(1 * spectral_axis_unit), include_prefix_units=False) + except u.core.UnitConversionError: + return [] # Get local units. locally_defined_flux_units = ['Jy', 'mJy', 'uJy', 'W / (m2 Hz)', 'eV / (s m2 Hz)', 'erg / (s cm2)', - 'erg / (s cm2 um)', 'erg / (s cm2 Angstrom)', 'erg / (s cm2 Hz)', - 'ph / (s cm2 um)', 'ph / (s cm2 Angstrom)', 'ph / (s cm2 Hz)'] local_units = [u.Unit(unit) for unit in locally_defined_flux_units] @@ -93,7 +85,4 @@ def create_flux_equivalencies_list(spectrum): flux_unit_equivalencies_titles = sorted(units_to_strings(curr_flux_unit_equivalencies)) # Concatenate both lists with the local units coming first. - flux_unit_equivalencies_titles = (sorted(units_to_strings(local_units)) + - flux_unit_equivalencies_titles) - - return flux_unit_equivalencies_titles + return sorted(units_to_strings(local_units)) + flux_unit_equivalencies_titles diff --git a/jdaviz/tests/test_subsets.py b/jdaviz/tests/test_subsets.py index 76e8ef3b1d..f6c08ec574 100644 --- a/jdaviz/tests/test_subsets.py +++ b/jdaviz/tests/test_subsets.py @@ -4,12 +4,10 @@ from astropy.tests.helper import assert_quantity_allclose from glue.core import Data from glue.core.roi import CircularROI, EllipticalROI, RectangularROI, XRangeROI - from glue.core.edit_subset_mode import AndMode, AndNotMode, OrMode, XorMode from regions import PixCoord, CirclePixelRegion, RectanglePixelRegion, EllipsePixelRegion - from numpy.testing import assert_allclose -from specutils import SpectralRegion +from specutils import SpectralRegion, Spectrum1D from jdaviz.core.marks import ShadowSpatialSpectral @@ -135,12 +133,10 @@ def test_region_from_subset_3d(cubeviz_helper): def test_region_from_subset_profile(cubeviz_helper, spectral_cube_wcs): - data = Data(flux=np.ones((128, 128, 256)), label='Test 1D Flux', coords=spectral_cube_wcs) + data = Spectrum1D(flux=np.ones((128, 128, 256)) * u.nJy, wcs=spectral_cube_wcs) subset_plugin = cubeviz_helper.app.get_tray_item_from_name('g-subset-plugin') - cubeviz_helper.app.data_collection.append(data) - - cubeviz_helper.app.add_data_to_viewer('spectrum-viewer', 'Test 1D Flux') + cubeviz_helper.load_data(data, data_label='Test 1D Flux') cubeviz_helper.app.get_viewer("spectrum-viewer").apply_roi(XRangeROI(5, 15.5)) @@ -184,11 +180,8 @@ def test_region_from_subset_profile(cubeviz_helper, spectral_cube_wcs): def test_region_spectral_spatial(cubeviz_helper, spectral_cube_wcs): - data = Data(flux=np.ones((128, 128, 256)), label='Test Flux', coords=spectral_cube_wcs) - cubeviz_helper.app.data_collection.append(data) - - cubeviz_helper.app.add_data_to_viewer('spectrum-viewer', 'Test Flux') - cubeviz_helper.app.add_data_to_viewer('flux-viewer', 'Test Flux') + data = Spectrum1D(flux=np.ones((128, 128, 256)) * u.nJy, wcs=spectral_cube_wcs) + cubeviz_helper.load_data(data, data_label="Test Flux") # use gaussian smooth plugin as a regression test for # https://github.com/spacetelescope/jdaviz/issues/1853 @@ -236,11 +229,8 @@ def test_region_spectral_spatial(cubeviz_helper, spectral_cube_wcs): def test_disjoint_spatial_subset(cubeviz_helper, spectral_cube_wcs): - data = Data(flux=np.ones((128, 128, 256)), label='Test Flux', coords=spectral_cube_wcs) - cubeviz_helper.app.data_collection.append(data) - - cubeviz_helper.app.add_data_to_viewer('spectrum-viewer', 'Test Flux') - cubeviz_helper.app.add_data_to_viewer('flux-viewer', 'Test Flux') + data = Spectrum1D(flux=np.ones((128, 128, 256)) * u.nJy, wcs=spectral_cube_wcs) + cubeviz_helper.load_data(data, data_label="Test Flux") flux_viewer = cubeviz_helper.app.get_viewer("flux-viewer") flux_viewer.apply_roi(CircularROI(xc=3, yc=4, radius=1)) @@ -260,11 +250,8 @@ def test_disjoint_spatial_subset(cubeviz_helper, spectral_cube_wcs): def test_disjoint_spectral_subset(cubeviz_helper, spectral_cube_wcs): subset_plugin = cubeviz_helper.app.get_tray_item_from_name('g-subset-plugin') - data = Data(flux=np.ones((128, 128, 256)), label='Test Flux', coords=spectral_cube_wcs) - cubeviz_helper.app.data_collection.append(data) - - cubeviz_helper.app.add_data_to_viewer('spectrum-viewer', 'Test Flux') - cubeviz_helper.app.add_data_to_viewer('flux-viewer', 'Test Flux') + data = Spectrum1D(flux=np.ones((128, 128, 256)) * u.nJy, wcs=spectral_cube_wcs) + cubeviz_helper.load_data(data, data_label="Test Flux") spec_viewer = cubeviz_helper.app.get_viewer("spectrum-viewer") spec_viewer.apply_roi(XRangeROI(5, 15.5)) @@ -332,7 +319,7 @@ def test_composite_region_from_subset_3d(cubeviz_helper): viewer.apply_roi(EllipticalROI(30, 30, 3, 6)) reg = cubeviz_helper.app.get_subsets("Subset 1") ellipse1 = EllipsePixelRegion(center=PixCoord(x=30, y=30), - width=3, height=6, angle=0.0 * u.deg) + width=6, height=12, angle=0.0 * u.deg) assert reg[-1] == {'name': 'EllipticalROI', 'glue_state': 'OrState', 'region': ellipse1, 'subset_state': reg[-1]['subset_state']} @@ -384,18 +371,18 @@ def test_composite_region_with_consecutive_and_not_states(cubeviz_helper): viewer.apply_roi(EllipticalROI(30, 30, 3, 6)) reg = cubeviz_helper.app.get_subsets("Subset 1") ellipse1 = EllipsePixelRegion(center=PixCoord(x=30, y=30), - width=3, height=6, angle=0.0 * u.deg) + width=6, height=12, angle=0.0 * u.deg) assert reg[-1] == {'name': 'EllipticalROI', 'glue_state': 'AndNotState', 'region': ellipse1, 'subset_state': reg[-1]['subset_state']} regions_list = cubeviz_helper.app.get_subsets("Subset 1", object_only=True) assert len(regions_list) == 3 - assert regions_list[-1].width == 3 + assert regions_list[-1].width == 6 regions_list = cubeviz_helper.app.get_subsets("Subset 1", spatial_only=True, object_only=True) assert len(regions_list) == 3 - assert regions_list[-1].width == 3 + assert regions_list[-1].width == 6 spatial_list = cubeviz_helper.app.get_subsets("Subset 1", spatial_only=True) assert len(spatial_list) == 3 @@ -446,7 +433,7 @@ def test_composite_region_with_imviz(imviz_helper, image_2d_wcs): viewer.apply_roi(EllipticalROI(3, 3, 3, 6)) reg = imviz_helper.app.get_subsets("Subset 1") ellipse1 = EllipsePixelRegion(center=PixCoord(x=3, y=3), - width=3, height=6, angle=0.0 * u.deg) + width=6, height=12, angle=0.0 * u.deg) assert reg[-1] == {'name': 'EllipticalROI', 'glue_state': 'AndNotState', 'region': ellipse1, 'subset_state': reg[-1]['subset_state']} @@ -504,6 +491,9 @@ def test_composite_region_from_subset_2d(specviz_helper, spectrum1d): assert subset_plugin.subset_types == ['Range', 'Range', 'Range', 'Range'] assert subset_plugin.glue_state_types == ['AndState', 'AndNotState', 'OrState', 'AndState'] + subset_plugin.vue_simplify_subset() + assert subset_plugin.glue_state_types == ["RangeSubsetState", "OrState"] + def test_edit_composite_spectral_subset(specviz_helper, spectrum1d): specviz_helper.load_spectrum(spectrum1d) @@ -550,3 +540,23 @@ def test_edit_composite_spectral_subset(specviz_helper, spectrum1d): viewer.apply_roi(XRangeROI(7800, 8000)) with pytest.raises(ValueError, match="AND mode should overlap with existing subset"): specviz_helper.app.get_subsets("Subset 1") + + +def test_edit_composite_spectral_with_xor(specviz_helper, spectrum1d): + specviz_helper.load_spectrum(spectrum1d) + viewer = specviz_helper.app.get_viewer(specviz_helper._default_spectrum_viewer_reference_name) + + viewer.apply_roi(XRangeROI(6400, 6600)) + specviz_helper.app.session.edit_subset_mode.mode = OrMode + viewer.apply_roi(XRangeROI(7200, 7400)) + + viewer.apply_roi(XRangeROI(7600, 7800)) + + specviz_helper.app.session.edit_subset_mode.mode = XorMode + viewer.apply_roi(XRangeROI(6700, 7700)) + reg = specviz_helper.app.get_subsets("Subset 1") + + assert reg[0].lower.value == 6400 and reg[0].upper.value == 6600 + assert reg[1].lower.value == 6700 and reg[1].upper.value == 7200 + assert reg[2].lower.value == 7400 and reg[2].upper.value == 7600 + assert reg[3].lower.value == 7700 and reg[3].upper.value == 7800 diff --git a/jdaviz/utils.py b/jdaviz/utils.py index 3d042d99f0..59ad0cf814 100644 --- a/jdaviz/utils.py +++ b/jdaviz/utils.py @@ -4,7 +4,6 @@ from collections import deque import matplotlib.pyplot as plt - from astropy.io import fits from ipyvue import watch from glue.config import settings diff --git a/notebooks/CubevizExample.ipynb b/notebooks/CubevizExample.ipynb index 73ef66238f..ac78ed88e7 100644 --- a/notebooks/CubevizExample.ipynb +++ b/notebooks/CubevizExample.ipynb @@ -85,6 +85,13 @@ " cubeviz.load_data(f'{data_dir}/{fn}')" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Alternatively, the data and the configuration can be autodetected and loaded simultaneously by calling `jdaviz.open(f'{data_dir}/{fn}')`" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -225,7 +232,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.8" + "version": "3.10.10" } }, "nbformat": 4, diff --git a/notebooks/ImvizExample.ipynb b/notebooks/ImvizExample.ipynb index 3b41256da3..b39014d4c5 100644 --- a/notebooks/ImvizExample.ipynb +++ b/notebooks/ImvizExample.ipynb @@ -152,6 +152,14 @@ "imviz.show()" ] }, + { + "cell_type": "markdown", + "id": "99d4bdef", + "metadata": {}, + "source": [ + "Alternatively, the data and the configuration can be autodetected and loaded simultaneously by calling `jdaviz.open(f'{data_dir}/{fn}')`" + ] + }, { "cell_type": "markdown", "id": "3e78efeb", @@ -720,7 +728,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.6" + "version": "3.10.10" } }, "nbformat": 4, diff --git a/notebooks/Specviz2dExample.ipynb b/notebooks/Specviz2dExample.ipynb index 32d7990d1a..5a8d4c8a8f 100644 --- a/notebooks/Specviz2dExample.ipynb +++ b/notebooks/Specviz2dExample.ipynb @@ -96,6 +96,14 @@ "source": [ "specviz2d.load_data(f'{data_dir}/{fn}')" ] + }, + { + "cell_type": "markdown", + "id": "795bc520", + "metadata": {}, + "source": [ + "Alternatively, the data and the configuration can be autodetected and loaded simultaneously by calling `jdaviz.open(f'{data_dir}/{fn}')`" + ] } ], "metadata": { @@ -114,7 +122,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.6" + "version": "3.10.10" } }, "nbformat": 4, diff --git a/notebooks/SpecvizExample.ipynb b/notebooks/SpecvizExample.ipynb index 6fd1a9044b..115cb03a1e 100644 --- a/notebooks/SpecvizExample.ipynb +++ b/notebooks/SpecvizExample.ipynb @@ -13,7 +13,9 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "import warnings\n", @@ -37,7 +39,9 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "from jdaviz import Specviz\n", @@ -63,7 +67,9 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "specviz.show()" @@ -91,7 +97,9 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "data_dir = tempfile.gettempdir()\n", @@ -103,6 +111,13 @@ "specviz.load_spectrum(f'{data_dir}/{fn}', \"myfile\")" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Alternatively, the data and the configuration can be autodetected and loaded simultaneously by calling `jdaviz.open(f'{data_dir}/{fn}')`" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -116,11 +131,13 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "# Returns a version of the whole spectra, with a mask applied\n", - "specviz.get_spectra(data_label='myfile', subset_to_apply='Subset 1')" + "specviz.get_data(data_label='myfile', spectral_subset='Subset 1')" ] }, { @@ -133,10 +150,12 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ - "specviz.app.get_subsets_from_viewer('spectrum-viewer')" + "specviz.get_spectra()" ] }, { @@ -281,7 +300,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.8" + "version": "3.10.10" } }, "nbformat": 4, diff --git a/notebooks/concepts/specviz_glue_unit_conversion.ipynb b/notebooks/concepts/specviz_glue_unit_conversion.ipynb new file mode 100644 index 0000000000..614d7c46b1 --- /dev/null +++ b/notebooks/concepts/specviz_glue_unit_conversion.ipynb @@ -0,0 +1,293 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "57c41ae5", + "metadata": {}, + "source": [ + "This is a concept notebook to investigate Glue unit conversion behavior integrated into Jdaviz spectrum viewer." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2b3bbfb1", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from astropy import units as u\n", + "from specutils import Spectrum1D\n", + "\n", + "from jdaviz import Specviz" + ] + }, + { + "cell_type": "markdown", + "id": "0d253733", + "metadata": {}, + "source": [ + "First spectrum." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cba69bb7", + "metadata": {}, + "outputs": [], + "source": [ + "wave1 = np.linspace(2, 5, 10) * u.um\n", + "flux1 = [1, 2, 3, 4, 5, 5, 4, 3, 2, 1] * u.Jy\n", + "spec1 = Spectrum1D(flux=flux1, spectral_axis=wave1)\n", + "\n", + "print(wave1)\n", + "print(flux1)" + ] + }, + { + "cell_type": "markdown", + "id": "1e0e7dc5", + "metadata": {}, + "source": [ + "Second spectrum in different units and with slight offsets in spectral axis." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "26bc794d", + "metadata": {}, + "outputs": [], + "source": [ + "wave2 = (wave1 + (0.1 * u.um)).to(u.GHz, u.spectral())\n", + "flux2 = flux1.to(u.mJy)\n", + "spec2 = Spectrum1D(flux=flux2, spectral_axis=wave2)\n", + "\n", + "print(wave2)\n", + "print(wave2.to(u.um, u.spectral()))\n", + "print(flux2)" + ] + }, + { + "cell_type": "markdown", + "id": "2bb4f4ea", + "metadata": {}, + "source": [ + "Fire up Specviz." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "74692a02", + "metadata": {}, + "outputs": [], + "source": [ + "specviz = Specviz()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "efdba701", + "metadata": {}, + "outputs": [], + "source": [ + "specviz.show()" + ] + }, + { + "cell_type": "markdown", + "id": "642fbae1", + "metadata": {}, + "source": [ + "Load the data into Specviz. Desired behavior:\n", + "\n", + "1. \"Jy_um\" would load with Jy in Y-axis and um in X-axis.\n", + "2. \"mJy_GHz\" would load with data automatically converted to Jy and um in the plot. You would see the same shape but slightly offset in X-axis, just slightly." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5ee09160", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "specviz.load_data(spec1, data_label=\"Jy_um\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9655292b", + "metadata": {}, + "outputs": [], + "source": [ + "specviz.load_data(spec2, data_label=\"mJy_GHz\")" + ] + }, + { + "cell_type": "markdown", + "id": "e6012551", + "metadata": {}, + "source": [ + "Change the spectral axis display unit to GHz." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6a4611b3", + "metadata": {}, + "outputs": [], + "source": [ + "viewer = specviz.app.get_viewer(\"spectrum-viewer\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ac319e71", + "metadata": {}, + "outputs": [], + "source": [ + "viewer.state.x_display_unit = \"GHz\"" + ] + }, + { + "cell_type": "markdown", + "id": "b2e84da2", + "metadata": {}, + "source": [ + "Change the flux axis display unit to FLAM." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4892cb38", + "metadata": {}, + "outputs": [], + "source": [ + "FLAM = u.erg / (u.s * u.cm * u.cm * u.AA)\n", + "\n", + "# If astropy can do it, Jdaviz should too.\n", + "spec1.flux.to(FLAM, u.spectral_density(spec1.spectral_axis))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3be87c54", + "metadata": {}, + "outputs": [], + "source": [ + "# this MIGHT fail depending on the version of astropy (since glue harcodes the expected string formatting \n", + "# for units, whereas astropy recently changed the default order of units)\n", + "try:\n", + " viewer.state.y_display_unit = FLAM.to_string()\n", + "except ValueError as e:\n", + " print(\"setting y_display_unit failed: \", repr(e))" + ] + }, + { + "cell_type": "markdown", + "id": "116e5806", + "metadata": {}, + "source": [ + "The plugin select component, however, is unit-aware and will handle mapping to the string formatting expected by glue" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "da5df8d4", + "metadata": {}, + "outputs": [], + "source": [ + "uc = specviz.plugins['Unit Conversion']\n", + "uc.flux_unit.choices" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b74ae8aa", + "metadata": {}, + "outputs": [], + "source": [ + "uc.flux_unit = FLAM.to_string()" + ] + }, + { + "cell_type": "markdown", + "id": "8be218b4", + "metadata": {}, + "source": [ + "Change the spectral axis again, this time to Angstrom via the plugin API." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8f9073f7", + "metadata": {}, + "outputs": [], + "source": [ + "uc.spectral_unit = 'Angstrom'" + ] + }, + { + "cell_type": "markdown", + "id": "fa487529", + "metadata": {}, + "source": [ + "Change everything back to original units." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8c110686", + "metadata": {}, + "outputs": [], + "source": [ + "viewer.state.x_display_unit = \"micron\"\n", + "viewer.state.y_display_unit = \"Jy\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "87642b18", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.15" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/concepts/specviz_unit_conversion.ipynb b/notebooks/concepts/specviz_unit_conversion.ipynb deleted file mode 100644 index 1402e6c56c..0000000000 --- a/notebooks/concepts/specviz_unit_conversion.ipynb +++ /dev/null @@ -1,61 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from jdaviz.configs.specviz.helper import Specviz\n", - "import specutils\n", - "import astropy.units as u\n", - "import numpy as np" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "spec_url = 'https://dr14.sdss.org/optical/spectrum/view/data/format=fits/spec=lite?plateid=1323&mjd=52797&fiberid=12'\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "scrolled": false - }, - "outputs": [], - "source": [ - "specviz = Specviz()\n", - "spec = specutils.Spectrum1D.read(spec_url)\n", - "specviz.load_spectrum(spec)\n", - "\n", - "specviz.show()" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.8.5" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/pyproject.toml b/pyproject.toml index ebd23f4b9e..44e46a75a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "jdaviz" description = "Astronomical data analysis development leveraging the Jupyter platform" requires-python = ">=3.8" authors = [ - { name = "JDADF Developers", email = "rosteen@stsci.edu" }, + { name = "JDADF Developers", email = "help@stsci.edu" }, ] dependencies = [ "packaging", @@ -12,8 +12,8 @@ dependencies = [ "traitlets>=5.0.5", "bqplot>=0.12.37", "bqplot-image-gl>=1.4.11", - "glue-core>=1.6.0,!=1.9.0,!=1.10", - "glue-jupyter>=0.15.0", + "glue-core>=1.11", + "glue-jupyter>=0.16.3", "echo>=0.5.0", "ipykernel>=6.19.4", "ipyvue>=1.6", @@ -26,7 +26,7 @@ dependencies = [ "specutils>=1.9", "specreduce>=1.3.0,<1.4.0", "photutils>=1.4", - "glue-astronomy>=0.7", + "glue-astronomy>=0.9", "asteval>=0.9.23", "idna", "vispy>=0.6.5", @@ -72,9 +72,8 @@ test = [ "pytest-tornasync", ] docs = [ - "sphinx-rtd-theme", - "sphinx-astropy", - "tomli; python_version <\"3.11\"" + "sphinx-astropy[confv2]>=1.9.1", + "sphinx_design" ] roman = [ "roman_datamodels>=0.14.2", diff --git a/tox.ini b/tox.ini index 8781af9977..73ddbd9c90 100644 --- a/tox.ini +++ b/tox.ini @@ -15,7 +15,7 @@ isolated_build = true setenv = MPLBACKEND=agg JUPYTER_PLATFORM_DIRS=1 - devdeps: PIP_EXTRA_INDEX_URL = https://pypi.anaconda.org/scipy-wheels-nightly/simple + devdeps: PIP_EXTRA_INDEX_URL = https://pypi.anaconda.org/scientific-python-nightly-wheels/simple # Pass through the following environment variables which may be needed for the CI passenv = HOME,WINDIR,LC_ALL,LC_CTYPE,CC,CI