diff --git a/.github/workflows/standalone.yml b/.github/workflows/standalone.yml
index b6a5d0eafa..58250bd0ae 100644
--- a/.github/workflows/standalone.yml
+++ b/.github/workflows/standalone.yml
@@ -8,17 +8,79 @@ on:
tags:
- 'v*'
workflow_dispatch:
+ pull_request:
defaults:
run:
shell: bash {0}
jobs:
- build_binary:
+ build_binary_not_osx:
runs-on: ${{ matrix.os }}-latest
+ if: (github.repository == 'spacetelescope/jdaviz' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || contains(github.event.pull_request.labels.*.name, 'Build standalone')))
strategy:
matrix:
- os: [ubuntu, windows, macos]
+ os: [ubuntu, windows]
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - uses: actions/setup-python@v5
+ with:
+ python-version: "3.11"
+
+ - name: Install jdaviz
+ run: pip install .[test]
+
+ - name: Install pyinstaller
+ run: pip install pyinstaller==6.6
+
+ - name: Create standalone binary
+ env:
+ DEVELOPER_ID_APPLICATION: ${{ secrets.DEVELOPER_ID_APPLICATION }}
+ run: (cd standalone; pyinstaller ./jdaviz.spec)
+
+ - name: Run jdaviz cmd in background
+ run: ./standalone/dist/jdaviz/jdaviz-cli imviz&
+
+ - name: Install playwright
+ run: (pip install playwright; playwright install chromium)
+
+ - name: Install pytest
+ run: pip install pytest-playwright
+
+ - name: Wait for Voila to get online
+ uses: ifaxity/wait-on-action@a7d13170ec542bdca4ef8ac4b15e9c6aa00a6866 # v1.2.1
+ with:
+ resource: tcp:8866
+ timeout: 60000
+
+ - name: Test standalone
+ run: (cd standalone; touch pytest.ini; JUPYTER_PLATFORM_DIRS=1 pytest test_standalone.py --video=on)
+
+ - name: Upload Test artifacts
+ if: github.event_name != 'pull_request'
+ uses: actions/upload-artifact@v3
+ with:
+ name: test-results-${{ matrix.os }}
+ path: standalone/test-results
+
+ - name: Upload jdaviz standalone (non-OSX)
+ if: github.event_name != 'pull_request'
+ uses: actions/upload-artifact@v3
+ with:
+ name: jdaviz-standlone-${{ matrix.os }}
+ path: |
+ standalone/dist/jdaviz
+
+ # Do not want to deal with OSX certs in pull request builds.
+ build_binary_osx:
+ runs-on: ${{ matrix.os }}-latest
+ if: (github.repository == 'spacetelescope/jdaviz' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch'))
+ strategy:
+ matrix:
+ os: [macos]
steps:
# osx signing based on https://melatonin.dev/blog/how-to-code-sign-and-notarize-macos-audio-plugins-in-ci/
- name: Import Certificates (macOS)
@@ -27,19 +89,20 @@ jobs:
with:
p12-file-base64: ${{ secrets.DEV_ID_APP_CERT }}
p12-password: ${{ secrets.DEV_ID_APP_PASSWORD }}
+
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-python@v5
with:
- python-version: "3.10"
+ python-version: "3.11"
- name: Install jdaviz
run: pip install .[test]
- name: Install pyinstaller
- run: pip install pyinstaller==5.11
+ run: pip install pyinstaller==6.6
- name: Create standalone binary
env:
@@ -59,6 +122,8 @@ jobs:
rm -rf standalone/dist/jdaviz.app/Contents/Resources/skimage/.dylibs
rm -rf standalone/dist/jdaviz.app/Contents/MacOS/shapely/.dylibs
rm -rf standalone/dist/jdaviz.app/Contents/Resources/shapely/.dylibs
+ rm -rf standalone/dist/jdaviz.app/Contents/MacOS/scipy/.dylibs
+ rm -rf standalone/dist/jdaviz.app/Contents/Resources/scipy/.dylibs
- name: Codesign (OSX)
if: ${{ matrix.os == 'macos' }}
@@ -101,10 +166,6 @@ jobs:
if: ${{ matrix.os == 'macos' }}
run: ./standalone/dist/jdaviz.app/Contents/MacOS/jdaviz-cli imviz&
- - name: Run jdaviz cmd in background
- if: ${{ matrix.os != 'macos' }}
- run: ./standalone/dist/jdaviz/jdaviz-cli imviz&
-
- name: Install playwright
run: (pip install playwright; playwright install chromium)
@@ -112,7 +173,7 @@ jobs:
run: pip install pytest-playwright
- name: Wait for Voila to get online
- uses: ifaxity/wait-on-action@df89d0cf8089bb0c38e25279c74848ef313da53b # v1.2.0
+ uses: ifaxity/wait-on-action@a7d13170ec542bdca4ef8ac4b15e9c6aa00a6866 # v1.2.1
with:
resource: tcp:8866
timeout: 60000
@@ -127,14 +188,6 @@ jobs:
name: test-results-${{ matrix.os }}
path: standalone/test-results
- - name: Upload jdaviz standalone (non-OSX)
- if: ${{ always() && (matrix.os != 'macos') }}
- uses: actions/upload-artifact@v3
- with:
- name: jdaviz-standlone-${{ matrix.os }}
- path: |
- standalone/dist/jdaviz
-
- name: Upload jdaviz standalone (OSX)
if: ${{ always() && (matrix.os == 'macos') }}
uses: actions/upload-artifact@v3
diff --git a/CHANGES.rst b/CHANGES.rst
index 2bc72bc8b7..c4b3970a08 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,23 +1,15 @@
-3.10 (unreleased)
-=================
+4.0 (unreleased)
+================
New Features
------------
-- The filename entry in the export plugin is now automatically populated based on the selection. [#2824]
-
-- Adding Data Quality plugin for Imviz and Cubeviz. [#2767, #2817, #2844]
-
-- Enable exporting spectral regions to ECSV files readable by ``astropy.table.QTable`` or
- ``specutils.SpectralRegion`` [#2843]
+- Adding flux/surface brightness translation and surface brightness
+ unit conversion in Cubeviz and Specviz. [#2781]
Cubeviz
^^^^^^^
-- Enable spectral unit conversion in cubeviz. [#2758, #2803]
-
-- Enable spectral extraction for composite subsets. [#2837]
-
Imviz
^^^^^
@@ -36,9 +28,6 @@ API Changes
Cubeviz
^^^^^^^
-- ERROR and DATA_QUALITY extension names are now recognized as
- uncertainty and mask, respectively. [#2840]
-
Imviz
^^^^^
@@ -69,19 +58,47 @@ Specviz
Specviz2d
^^^^^^^^^
-- Loading a specific extension with ``ext`` keyword no longer crashes. [#2830]
-
Other Changes and Additions
---------------------------
-- Bump required Python version to 3.10. [#2757]
+3.11 (unreleased)
+=================
-- Line menu in Redshift from Centroid section of Line Analysis now shows values in current units. [#2816, #2831]
+New Features
+------------
-- Bump required specutils version to 1.15. [#2843]
+Cubeviz
+^^^^^^^
-3.9.2 (unreleased)
-==================
+Imviz
+^^^^^
+
+Mosviz
+^^^^^^
+
+Specviz
+^^^^^^^
+
+Specviz2d
+^^^^^^^^^
+
+API Changes
+-----------
+
+Cubeviz
+^^^^^^^
+
+Imviz
+^^^^^
+
+Mosviz
+^^^^^^
+
+Specviz
+^^^^^^^
+
+Specviz2d
+^^^^^^^^^
Bug Fixes
---------
@@ -89,7 +106,29 @@ Bug Fixes
Cubeviz
^^^^^^^
-- Re-enable support for exporting spectrum-viewer. [#2825]
+Imviz
+^^^^^
+
+Mosviz
+^^^^^^
+
+Specviz
+^^^^^^^
+
+Specviz2d
+^^^^^^^^^
+
+Other Changes and Additions
+---------------------------
+
+3.10.1 (unreleased)
+===================
+
+Bug Fixes
+---------
+
+Cubeviz
+^^^^^^^
Imviz
^^^^^
@@ -103,6 +142,58 @@ Specviz
Specviz2d
^^^^^^^^^
+3.10 (2024-05-03)
+=================
+
+New Features
+------------
+
+- The filename entry in the export plugin is now automatically populated based on the selection. [#2824]
+
+- Adding Data Quality plugin for Imviz and Cubeviz. [#2767, #2817, #2844]
+
+- Enable exporting spectral regions to ECSV files readable by ``astropy.table.QTable`` or
+ ``specutils.SpectralRegion`` [#2843]
+
+Cubeviz
+^^^^^^^
+
+- Enable spectral unit conversion in cubeviz. [#2758, #2803]
+
+- Enable spectral extraction for composite subsets. [#2837]
+
+API Changes
+-----------
+
+Cubeviz
+^^^^^^^
+
+- ERROR and DATA_QUALITY extension names are now recognized as
+ uncertainty and mask, respectively. [#2840]
+
+Bug Fixes
+---------
+
+Cubeviz
+^^^^^^^
+
+- Re-enable support for exporting spectrum-viewer. [#2825]
+
+
+Specviz2d
+^^^^^^^^^
+
+- Loading a specific extension with ``ext`` keyword no longer crashes. [#2830]
+
+Other Changes and Additions
+---------------------------
+
+- Bump required Python version to 3.10. [#2757]
+
+- Line menu in Redshift from Centroid section of Line Analysis now shows values in current units. [#2816, #2831]
+
+- Bump required specutils version to 1.15. [#2843]
+
3.9.1 (2024-04-19)
==================
diff --git a/CITATION.cff b/CITATION.cff
index 363f08aa34..09b648771c 100644
--- a/CITATION.cff
+++ b/CITATION.cff
@@ -73,9 +73,9 @@ authors:
- family-names: "Volfman"
given-names: "Sabrina"
title: "Jdaviz"
-version: 3.9.1
+version: 3.10
doi: https://doi.org/10.5281/zenodo.5513927
-date-released: 2024-04-19
+date-released: 2024-05-03
url: "https://github.com/spacetelescope/jdaviz"
# see a full list of contributors here: https://github.com/spacetelescope/jdaviz/graphs/contributors
diff --git a/docs/img/slit_overlay.png b/docs/img/slit_overlay.png
deleted file mode 100644
index 325aa5160a..0000000000
Binary files a/docs/img/slit_overlay.png and /dev/null differ
diff --git a/docs/mosviz/displayspectra.rst b/docs/mosviz/displayspectra.rst
index 677328cd55..b88d508c91 100644
--- a/docs/mosviz/displayspectra.rst
+++ b/docs/mosviz/displayspectra.rst
@@ -23,9 +23,6 @@ Synchronous Spectral Pan/Zoom
-----------------------------
Mosviz assumes the 1D and 2D spectra objects share a relationship in spectral space. As a result, the 1D and 2D spectral viewers have their spectral axes (the horizontal x-axis) linked. As you pan and zoom in spectral space (horizontally) in either of the two spectral viewers, the other will follow, simultaneously panning and zooming by the same amounts.
-.. warning::
- If you pan too far away from the bounds of the dataset provided in the 1D or 2D spectral viewers, a warning will be displayed to notify the user. If you go too far, there is a risk of desynchronizing the two viewers.
-
Defining Spectral Regions
=========================
diff --git a/docs/mosviz/img/mosviz_ui.png b/docs/mosviz/img/mosviz_ui.png
index 4150eb7cc8..77351c72d4 100644
Binary files a/docs/mosviz/img/mosviz_ui.png and b/docs/mosviz/img/mosviz_ui.png differ
diff --git a/docs/mosviz/navigation.rst b/docs/mosviz/navigation.rst
index 49306b5923..3cdcea5ae5 100644
--- a/docs/mosviz/navigation.rst
+++ b/docs/mosviz/navigation.rst
@@ -23,9 +23,9 @@ Clicking on a row in the table viewer populates the other viewers with the data
corresponding to that row. Note that the checkbox at the far left of the row does
not need to be checked to display the data - that checkbox is related to selecting
subsets of rows. By default, 10 rows are shown at a time but this can be changed
-with the selector at the bottom right of the table viewer. To the right of the
+with the selector at the bottom right of the table viewer. To the right of the
rows-per-page selector are left and right arrows that are used to move between
-pages of the table, if there is more data than fits on a single page.
+pages of the table, if there is more data than fits on a single page.
.. _slit-overlay:
@@ -33,9 +33,6 @@ pages of the table, if there is more data than fits on a single page.
Using the Slit Overlay
======================
-Currently, the option to overlay slit geometry is enabled by default if the
+Currently, the option to overlay slit geometry is enabled by default if the
required information is present. The option to toggle whether slits are displayed
-is located in the top right tool menu under the "Slit Overlay" heading, as seen
-in the screenshot below.
-
-.. image:: ../img/slit_overlay.png
+is located in the top right tool menu under the "Slit Overlay" heading.
diff --git a/docs/mosviz/notebook.rst b/docs/mosviz/notebook.rst
index 66e2c11630..bf76105aa8 100644
--- a/docs/mosviz/notebook.rst
+++ b/docs/mosviz/notebook.rst
@@ -1,8 +1,8 @@
.. _mosviz-notebook:
-***********************************
-Using Mosviz in a Jupyter Notebook
-***********************************
+***************************
+Exporting data from Mosviz
+***************************
To initialize an instance of the Mosviz app in a Jupyter notebook, simply run
the following code in a cell of the notebook:
@@ -13,7 +13,7 @@ the following code in a cell of the notebook:
mosviz = Mosviz()
mosviz.show()
-After running the code above, you can interact with the Mosviz application from
+After running the code above, you can interact with the Mosviz application from
subsequent notebook cells via the API methods attached to the
:class:`~jdaviz.configs.mosviz.helper.Mosviz` object,
for example loading data into the app as described in :ref:`mosviz-import-api`.
diff --git a/jdaviz/app.py b/jdaviz/app.py
index 7030b0d7b7..e65de0270e 100644
--- a/jdaviz/app.py
+++ b/jdaviz/app.py
@@ -66,20 +66,29 @@
@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',
+ include_prefix_units=True, equivalencies=eqv)))
+ + [
+ 'Jy', 'mJy', 'uJy', 'MJy',
'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 Angstrom)', 'erg / (s cm2 Angstrom)',
'erg / (s cm2 Hz)', 'erg / (Hz s cm2)',
- 'ph / (s cm2 Angstrom)', 'ph / (Angstrom s cm2)',
- 'ph / (s cm2 Hz)', 'ph / (Hz s cm2)'
+ 'ph / (s cm2 Angstrom)', 'ph / (s cm2 Angstrom)',
+ 'ph / (Hz s cm2)', 'ph / (Hz s cm2)', 'bol', 'AB', 'ST'
+ ]
+ + [
+ 'Jy / sr', 'mJy / sr', 'uJy / sr', 'MJy / sr',
+ 'W / (Hz sr m2)',
+ 'eV / (s m2 Hz sr)',
+ 'erg / (s cm2 sr)',
+ 'erg / (s cm2 Angstrom sr)', 'erg / (s cm2 Hz sr)',
+ 'ph / (s cm2 Angstrom sr)', 'ph / (s cm2 Hz sr)',
+ 'bol / sr', 'AB / sr', 'ST / sr'
])
else: # spectral axis
# prefer Hz over Bq and um over micron
@@ -100,12 +109,48 @@ def to_unit(self, data, cid, values, original_units, target_units):
except RuntimeError:
eqv = []
else:
- if len(values) == 2:
+ # Ensure a spectrum passed through Spectral Extraction plugin
+ if '_pixel_scale_factor' in spec.meta:
+ # if spectrum data collection item is in Surface Brightness units
+ if u.sr in spec.unit.bases:
+ # Data item in data collection does not update from conversion/translation.
+ # App wide orginal data units are used for conversion, orginal_units and
+ # target_units dicate the conversion to take place.
+ if (u.sr in u.Unit(original_units).bases) and \
+ (u.sr not in u.Unit(target_units).bases):
+ # Surface Brightness -> Flux
+ eqv = [(u.MJy / u.sr,
+ u.MJy,
+ lambda x: (x * spec.meta['_pixel_scale_factor']),
+ lambda x: x)]
+ else:
+ # Flux -> Surface Brightness
+ eqv = u.spectral_density(spec.spectral_axis)
+
+ # if spectrum data collection item is in Flux units
+ elif u.sr not in spec.unit.bases:
+ # Data item in data collection does not update from conversion/translation.
+ # App wide orginal data units are used for conversion, orginal_units and
+ # target_units dicate the conversion to take place.
+ if (u.sr not in u.Unit(original_units).bases) and \
+ (u.sr in u.Unit(target_units).bases):
+ # Flux -> Surface Brightness
+ eqv = [(u.MJy,
+ u.MJy / u.sr,
+ lambda x: (x / spec.meta['_pixel_scale_factor']),
+ lambda x: x)]
+ else:
+ # Surface Brightness -> Flux
+ eqv = u.spectral_density(spec.spectral_axis)
+
+ elif 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() + u.pixel_scale(1*u.pix)
diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py
index 906879beb9..528b172490 100644
--- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py
+++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py
@@ -3,7 +3,6 @@
import numpy as np
import astropy
-from astropy import units as u
from astropy.nddata import (
NDDataArray, StdDevUncertainty
)
@@ -604,13 +603,3 @@ def _live_update(self, event={}):
for mark in self.marks.values():
mark.update_xy(sp.spectral_axis.value, sp.flux.value)
mark.visible = True
-
- def translate_units(self, collapsed_spec):
- # remove sr
- if u.sr in collapsed_spec._unit.bases:
- collapsed_spec._data *= collapsed_spec.meta['_pixel_scale_factor']
- collapsed_spec._unit *= u.sr
- # add sr
- elif u.sr not in collapsed_spec._unit.bases:
- collapsed_spec._data /= collapsed_spec.meta['_pixel_scale_factor']
- collapsed_spec._unit /= u.sr
diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py
index edccaa7106..bda139f53d 100644
--- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py
+++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/tests/test_spectral_extraction.py
@@ -12,7 +12,6 @@
from regions import (CirclePixelRegion, CircleAnnulusPixelRegion, EllipsePixelRegion,
RectanglePixelRegion, PixCoord)
from specutils import Spectrum1D
-from astropy.wcs import WCS
def test_version_after_nddata_update(cubeviz_helper, spectrum1d_cube_with_uncerts):
@@ -375,53 +374,6 @@ def test_cube_extraction_with_nan(cubeviz_helper, image_cube_hdu_obj):
assert_allclose(sp_subset.flux.value, 12) # (4 x 4) - 4
-def test_unit_translation(cubeviz_helper):
- # custom cube so we have PIXAR_SR in metadata, and flux units = Jy/pix
- wcs_dict = {"CTYPE1": "WAVE-LOG", "CTYPE2": "DEC--TAN", "CTYPE3": "RA---TAN",
- "CRVAL1": 4.622e-7, "CRVAL2": 27, "CRVAL3": 205,
- "CDELT1": 8e-11, "CDELT2": 0.0001, "CDELT3": -0.0001,
- "CRPIX1": 0, "CRPIX2": 0, "CRPIX3": 0, "PIXAR_SR": 8e-11}
- w = WCS(wcs_dict)
- flux = np.zeros((30, 20, 3001), dtype=np.float32)
- flux[5:15, 1:11, :] = 1
- cube = Spectrum1D(flux=flux * u.MJy, wcs=w, meta=wcs_dict)
- cubeviz_helper.load_data(cube, data_label="test")
-
- center = PixCoord(5, 10)
- cubeviz_helper.load_regions(CirclePixelRegion(center, radius=2.5))
-
- extract_plg = cubeviz_helper.plugins['Spectral Extraction']
-
- extract_plg.aperture = extract_plg.aperture.choices[-1]
- extract_plg.aperture_method.selected = 'Exact'
- extract_plg.wavelength_dependent = True
- extract_plg.function = 'Sum'
- # set so pixel scale factor != 1
- extract_plg.reference_spectral_value = 0.000001
-
- # collapse to spectrum, now we can get pixel scale factor
- collapsed_spec = extract_plg.collapse_to_spectrum()
-
- assert collapsed_spec.meta['_pixel_scale_factor'] != 1
-
- # store to test second time after calling translate_units
- mjy_sr_data1 = collapsed_spec._data[0]
-
- extract_plg._obj.translate_units(collapsed_spec)
-
- assert collapsed_spec._unit == u.MJy / u.sr
- # some value in MJy/sr that we know the outcome after translation
- assert np.allclose(collapsed_spec._data[0], 8.7516529e10)
-
- extract_plg._obj.translate_units(collapsed_spec)
-
- # translating again returns the original units
- assert collapsed_spec._unit == u.MJy
- # returns to the original values
- # which is a value in Jy/pix that we know the outcome after translation
- assert np.allclose(collapsed_spec._data[0], mjy_sr_data1)
-
-
def test_autoupdate_results(cubeviz_helper, spectrum1d_cube_largest):
cubeviz_helper.load_data(spectrum1d_cube_largest)
fv = cubeviz_helper.viewers['flux-viewer']._obj
diff --git a/jdaviz/configs/imviz/plugins/coords_info/coords_info.py b/jdaviz/configs/imviz/plugins/coords_info/coords_info.py
index 5cfaefb44c..0c63f085bc 100644
--- a/jdaviz/configs/imviz/plugins/coords_info/coords_info.py
+++ b/jdaviz/configs/imviz/plugins/coords_info/coords_info.py
@@ -554,8 +554,18 @@ def _copy_axes_to_spectral():
# 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))
+
+ # temporarily here, may be removed after upstream units handling
+ # or will be generalized for any sb <-> flux
+ if '_pixel_scale_factor' in sp.meta:
+ eqv = [(u.MJy / u.sr,
+ u.MJy,
+ lambda x: (x * sp.meta['_pixel_scale_factor']),
+ lambda x: x)]
+ disp_flux = sp.flux.to_value(viewer.state.y_display_unit, eqv)
+ else:
+ 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
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 813df897a1..82307c6136 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
@@ -5,6 +5,8 @@
from astropy.nddata import InverseVariance
from specutils import Spectrum1D
from astropy.utils.introspection import minversion
+from astropy.wcs import WCS
+from regions import PixCoord, CirclePixelRegion
ASTROPY_LT_5_3 = not minversion(astropy, "5.3")
@@ -120,3 +122,113 @@ def test_non_stddev_uncertainty(specviz_helper):
np.abs(viewer.figure.marks[-1].y - viewer.figure.marks[-1].y.mean(0)),
stddev
)
+
+
+def test_unit_translation(cubeviz_helper):
+ # custom cube so PIXAR_SR is in metadata, and Flux units, and in MJy
+ wcs_dict = {"CTYPE1": "WAVE-LOG", "CTYPE2": "DEC--TAN", "CTYPE3": "RA---TAN",
+ "CRVAL1": 4.622e-7, "CRVAL2": 27, "CRVAL3": 205,
+ "CDELT1": 8e-11, "CDELT2": 0.0001, "CDELT3": -0.0001,
+ "CRPIX1": 0, "CRPIX2": 0, "CRPIX3": 0, "PIXAR_SR": 8e-11}
+ w = WCS(wcs_dict)
+ flux = np.zeros((30, 20, 3001), dtype=np.float32)
+ flux[5:15, 1:11, :] = 1
+ cube = Spectrum1D(flux=flux * u.MJy, wcs=w, meta=wcs_dict)
+ cubeviz_helper.load_data(cube, data_label="test")
+
+ center = PixCoord(5, 10)
+ cubeviz_helper.load_regions(CirclePixelRegion(center, radius=2.5))
+
+ uc_plg = cubeviz_helper.plugins['Unit Conversion']
+ # we can get rid of this after all spectra pass through
+ # spectral extraction plugin
+ extract_plg = cubeviz_helper.plugins['Spectral Extraction']
+
+ extract_plg.aperture = extract_plg.aperture.choices[-1]
+ extract_plg.aperture_method.selected = 'Exact'
+ extract_plg.wavelength_dependent = True
+ extract_plg.function = 'Sum'
+ # set so pixel scale factor != 1
+ extract_plg.reference_spectral_value = 0.000001
+
+ # all spectra will pass through spectral extraction,
+ # this will store a scale factor for use in translations.
+ collapsed_spec = extract_plg.collapse_to_spectrum()
+
+ # test that the scale factor was set
+ assert collapsed_spec.meta['_pixel_scale_factor'] != 1
+
+ # When the dropdown is displayed, this ensures the loaded
+ # data collection item units will be used for translations.
+ uc_plg._obj.show_translator = True
+ assert uc_plg._obj.flux_or_sb_selected == 'Flux'
+
+ # to have access to display units
+ viewer_1d = cubeviz_helper.app.get_viewer(
+ cubeviz_helper._default_spectrum_viewer_reference_name)
+
+ # for testing _set_flux_or_sb()
+ uc_plg._obj.show_translator = False
+
+ # change global y-units from Flux -> Surface Brightness
+ uc_plg._obj.flux_or_sb_selected = 'Surface Brightness'
+
+ uc_plg._obj.show_translator = True
+ assert uc_plg._obj.flux_or_sb_selected == 'Surface Brightness'
+ y_display_unit = u.Unit(viewer_1d.state.y_display_unit)
+
+ # check if units translated
+ assert y_display_unit == u.MJy / u.sr
+
+
+def test_sb_unit_conversion(cubeviz_helper):
+ # custom cube to have Surface Brightness units
+ wcs_dict = {"CTYPE1": "WAVE-LOG", "CTYPE2": "DEC--TAN", "CTYPE3": "RA---TAN",
+ "CRVAL1": 4.622e-7, "CRVAL2": 27, "CRVAL3": 205,
+ "CDELT1": 8e-11, "CDELT2": 0.0001, "CDELT3": -0.0001,
+ "CRPIX1": 0, "CRPIX2": 0, "CRPIX3": 0, "PIXAR_SR": 8e-11}
+ w = WCS(wcs_dict)
+ flux = np.zeros((30, 20, 3001), dtype=np.float32)
+ flux[5:15, 1:11, :] = 1
+ cube = Spectrum1D(flux=flux * (u.MJy / u.sr), wcs=w, meta=wcs_dict)
+ cubeviz_helper.load_data(cube, data_label="test")
+
+ uc_plg = cubeviz_helper.plugins['Unit Conversion']
+ uc_plg.open_in_tray()
+
+ # to have access to display units
+ viewer_1d = cubeviz_helper.app.get_viewer(
+ cubeviz_helper._default_spectrum_viewer_reference_name)
+
+ # Surface Brightness conversion
+ uc_plg.flux_or_sb_unit = 'Jy / sr'
+ y_display_unit = u.Unit(viewer_1d.state.y_display_unit)
+ assert y_display_unit == u.Jy / u.sr
+
+ # Try a second conversion
+ uc_plg.flux_or_sb_unit = 'W / Hz sr m2'
+ y_display_unit = u.Unit(viewer_1d.state.y_display_unit)
+ assert y_display_unit == u.Unit("W / (Hz sr m2)")
+
+ # really a translation test, test_unit_translation loads a Flux
+ # cube, this test load a Surface Brightness Cube, this ensures
+ # two-way translation
+ uc_plg.flux_or_sb_unit = 'MJy / sr'
+ y_display_unit = u.Unit(viewer_1d.state.y_display_unit)
+
+ # we can get rid of this after all spectra pass through
+ # spectral extraction plugin
+ extract_plg = cubeviz_helper.plugins['Spectral Extraction']
+ extract_plg.aperture = extract_plg.aperture.choices[-1]
+ extract_plg.aperture_method.selected = 'Exact'
+ extract_plg.wavelength_dependent = True
+ extract_plg.function = 'Sum'
+ extract_plg.reference_spectral_value = 0.000001
+ extract_plg.collapse_to_spectrum()
+
+ uc_plg._obj.show_translator = True
+ uc_plg._obj.flux_or_sb_selected = 'Flux'
+ uc_plg.flux_or_sb_unit = 'MJy'
+ y_display_unit = u.Unit(viewer_1d.state.y_display_unit)
+
+ assert y_display_unit == u.MJy
diff --git a/jdaviz/configs/specviz/plugins/unit_conversion/unit_conversion.py b/jdaviz/configs/specviz/plugins/unit_conversion/unit_conversion.py
index 8821f039ef..8a01ad43fe 100644
--- a/jdaviz/configs/specviz/plugins/unit_conversion/unit_conversion.py
+++ b/jdaviz/configs/specviz/plugins/unit_conversion/unit_conversion.py
@@ -1,10 +1,11 @@
import numpy as np
from astropy import units as u
-from traitlets import List, Unicode, observe
+from traitlets import List, Unicode, observe, Bool
from jdaviz.core.events import GlobalDisplayUnitChanged
from jdaviz.core.registries import tray_registry
-from jdaviz.core.template_mixin import PluginTemplateMixin, UnitSelectPluginComponent, PluginUserApi
+from jdaviz.core.template_mixin import (PluginTemplateMixin, UnitSelectPluginComponent,
+ SelectPluginComponent, PluginUserApi)
from jdaviz.core.validunits import (create_spectral_equivalencies_list,
create_flux_equivalencies_list)
@@ -40,18 +41,26 @@ class UnitConversion(PluginTemplateMixin):
* :meth:`~jdaviz.core.template_mixin.PluginTemplateMixin.show`
* :meth:`~jdaviz.core.template_mixin.PluginTemplateMixin.open_in_tray`
* :meth:`~jdaviz.core.template_mixin.PluginTemplateMixin.close_in_tray`
- * ``spectral_unit`` (:class:`~jdaviz.core.template_mixin.SelectPluginComponent`):
+ * ``spectral_unit`` (:class:`~jdaviz.core.template_mixin.UnitSelectPluginComponent`):
Global unit to use for all spectral axes.
- * ``flux_unit`` (:class:`~jdaviz.core.template_mixin.SelectPluginComponent`):
- Global unit to use for all flux axes.
+ * ``flux_or_sb_unit`` (:class:`~jdaviz.core.template_mixin.UnitSelectPluginComponent`):
+ Global unit to use for all flux/surface brightness (depending on flux_or_sb selection) axes.
+ * ``flux_or_sb`` (:class:`~jdaviz.core.template_mixin.SelectPluginComponent`):
+ Y-axis physical type selection. Currently only accessible in Cubeviz (pixel scale factor
+ added in Cubeviz Spectral Extraction, and is used for this translation).
"""
template_file = __file__, "unit_conversion.vue"
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)
+ show_translator = Bool(False).tag(sync=True)
+ flux_or_sb_items = List().tag(sync=True)
+ flux_or_sb_selected = Unicode().tag(sync=True)
+
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -73,13 +82,17 @@ def __init__(self, *args, **kwargs):
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')
+ self.flux_or_sb_unit = UnitSelectPluginComponent(self,
+ items='flux_unit_items',
+ selected='flux_unit_selected')
+ self.flux_or_sb = SelectPluginComponent(self,
+ items='flux_or_sb_items',
+ selected='flux_or_sb_selected',
+ manual_options=['Surface Brightness', 'Flux'])
@property
def user_api(self):
- return PluginUserApi(self, expose=('spectral_unit',))
+ return PluginUserApi(self, expose=('spectral_unit', 'flux_or_sb', 'flux_or_sb_unit'))
def _on_glue_x_display_unit_changed(self, x_unit):
if x_unit is None:
@@ -97,7 +110,7 @@ def _on_glue_x_display_unit_changed(self, x_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):
+ if not len(self.flux_or_sb_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)
@@ -107,11 +120,11 @@ def _on_glue_y_display_unit_changed(self, y_unit):
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.
+ # setting the spectral unit will check len(flux_or_sb_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:
+ if y_unit != self.flux_or_sb_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)
@@ -119,8 +132,22 @@ def _on_glue_y_display_unit_changed(self, y_unit):
# 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
+ self.flux_or_sb_unit.choices = choices
+ self.flux_or_sb_unit.selected = y_unit
+
+ def translate_units(self, flux_or_sb_selected):
+ spec_units = u.Unit(self.spectrum_viewer.state.y_display_unit)
+ # Surface Brightness -> Flux
+ if u.sr in spec_units.bases and flux_or_sb_selected == 'Flux':
+ spec_units *= u.sr
+ # update display units
+ self.spectrum_viewer.state.y_display_unit = str(spec_units)
+
+ # Flux -> Surface Brightness
+ elif u.sr not in spec_units.bases and flux_or_sb_selected == 'Surface Brightness':
+ spec_units /= u.sr
+ # update display units
+ self.spectrum_viewer.state.y_display_unit = str(spec_units)
@observe('spectral_unit_selected')
def _on_spectral_unit_changed(self, *args):
@@ -128,14 +155,47 @@ def _on_spectral_unit_changed(self, *args):
if self.spectrum_viewer.state.x_display_unit != xunit:
self.spectrum_viewer.state.x_display_unit = xunit
self.hub.broadcast(GlobalDisplayUnitChanged('spectral',
- self.spectral_unit.selected,
- sender=self))
+ self.spectral_unit.selected,
+ sender=self))
@observe('flux_unit_selected')
def _on_flux_unit_changed(self, *args):
- yunit = _valid_glue_display_unit(self.flux_unit.selected, self.spectrum_viewer, 'y')
+ yunit = _valid_glue_display_unit(self.flux_or_sb_unit.selected, self.spectrum_viewer, 'y')
if self.spectrum_viewer.state.y_display_unit != yunit:
self.spectrum_viewer.state.y_display_unit = yunit
self.hub.broadcast(GlobalDisplayUnitChanged('flux',
- self.flux_unit.selected,
+ self.flux_or_sb_unit.selected,
sender=self))
+
+ # Ensure first dropdown selection for Flux/Surface Brightness
+ # is in accordance with the data collection item's units.
+ @observe('show_translator')
+ def _set_flux_or_sb(self, *args):
+ if (self.spectrum_viewer and hasattr(self.spectrum_viewer.state, 'y_display_unit')
+ and self.spectrum_viewer.state.y_display_unit is not None):
+ if u.sr in u.Unit(self.spectrum_viewer.state.y_display_unit).bases:
+ self.flux_or_sb_selected = 'Surface Brightness'
+ else:
+ self.flux_or_sb_selected = 'Flux'
+
+ @observe('flux_or_sb_selected')
+ def _translate(self, *args):
+ # currently unsupported, can be supported with a scale factor
+ if self.app.config == 'specviz':
+ return
+
+ # Check for a scale factor/data passed through spectral extraction plugin.
+ specs_w_factor = [spec for spec in self.app.data_collection
+ if "_pixel_scale_factor" in spec.meta]
+ # Translate if we have a scale factor
+ if specs_w_factor:
+ self.translate_units(self.flux_or_sb_selected)
+ # The translator dropdown hasn't been loaded yet so don't try translating
+ elif not self.show_translator:
+ return
+ # Notify the user to extract a spectrum before using the surface brightness/flux
+ # translation. Can be removed after all 1D spectra in Cubeviz pass through
+ # spectral extraction plugin (as the scale factor will then be stored).
+ else:
+ raise ValueError("No collapsed spectra in data collection, \
+ please collapse a spectrum first.")
diff --git a/jdaviz/configs/specviz/plugins/unit_conversion/unit_conversion.vue b/jdaviz/configs/specviz/plugins/unit_conversion/unit_conversion.vue
index e952337d97..ee252b246b 100644
--- a/jdaviz/configs/specviz/plugins/unit_conversion/unit_conversion.vue
+++ b/jdaviz/configs/specviz/plugins/unit_conversion/unit_conversion.vue
@@ -19,22 +19,28 @@
>
+
+
+
+
-
-
- Flux conversion is not yet implemented in Cubeviz.
-
-
diff --git a/jdaviz/configs/specviz/plugins/viewers.py b/jdaviz/configs/specviz/plugins/viewers.py
index e92f4abfa4..a228c2b026 100644
--- a/jdaviz/configs/specviz/plugins/viewers.py
+++ b/jdaviz/configs/specviz/plugins/viewers.py
@@ -559,9 +559,26 @@ def set_plot_axes(self):
y_display_unit = self.state.y_display_unit
y_unit = u.Unit(y_display_unit) if y_display_unit else u.dimensionless_unscaled
- if y_unit.is_equivalent(u.Jy / u.sr):
- flux_unit_type = "Surface brightness"
- elif y_unit.is_equivalent(u.erg / (u.s * u.cm**2)):
+ # Get local units.
+ locally_defined_flux_units = [
+ u.Jy, u.mJy, u.uJy, u.MJy,
+ u.W / (u.m**2 * u.Hz),
+ u.eV / (u.s * u.m**2 * u.Hz),
+ u.erg / (u.s * u.cm**2),
+ u.erg / (u.s * u.cm**2 * u.Angstrom),
+ u.erg / (u.s * u.cm**2 * u.Hz),
+ u.ph / (u.s * u.cm**2 * u.Angstrom),
+ u.ph / (u.s * u.cm**2 * u.Hz),
+ u.bol, u.AB, u.ST
+ ]
+
+ locally_defined_sb_units = [
+ unit / u.sr for unit in locally_defined_flux_units
+ ]
+
+ if any(y_unit.is_equivalent(unit) for unit in locally_defined_sb_units):
+ flux_unit_type = "Surface Brightness"
+ elif any(y_unit.is_equivalent(unit) for unit in locally_defined_flux_units):
flux_unit_type = 'Flux'
elif y_unit.is_equivalent(u.electron / u.s) or y_unit.physical_type == 'dimensionless':
# electron / s or 'dimensionless_unscaled' should be labeled counts
diff --git a/jdaviz/configs/specviz/tests/test_viewers.py b/jdaviz/configs/specviz/tests/test_viewers.py
index 80928d4ad9..2c40e390ff 100644
--- a/jdaviz/configs/specviz/tests/test_viewers.py
+++ b/jdaviz/configs/specviz/tests/test_viewers.py
@@ -6,8 +6,8 @@
@pytest.mark.parametrize(
('input_unit', 'y_axis_label'),
- [(u.MJy, 'Flux density'),
- (u.MJy / u.sr, 'Surface brightness'),
+ [(u.MJy, 'Flux'),
+ (u.MJy / u.sr, 'Surface Brightness'),
(u.electron / u.s, 'Counts'),
(u.dimensionless_unscaled, 'Counts'),
(u.erg / (u.s * u.cm ** 2), 'Flux'),
diff --git a/jdaviz/core/validunits.py b/jdaviz/core/validunits.py
index 372a5f2904..378cf755eb 100644
--- a/jdaviz/core/validunits.py
+++ b/jdaviz/core/validunits.py
@@ -54,7 +54,7 @@ def create_spectral_equivalencies_list(spectral_axis_unit,
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))
+ if ((flux_unit in (u.count, u.dimensionless_unscaled))
or (spectral_axis_unit in (u.pix, u.dimensionless_unscaled))):
return []
@@ -67,15 +67,27 @@ def create_flux_equivalencies_list(flux_unit, spectral_axis_unit):
return []
# Get local units.
- locally_defined_flux_units = ['Jy', 'mJy', 'uJy',
- 'W / (m2 Hz)',
- 'eV / (s m2 Hz)',
- 'erg / (s cm2)',
- 'erg / (s cm2 Angstrom)',
- 'erg / (s cm2 Hz)',
- 'ph / (s cm2 Angstrom)',
- 'ph / (s cm2 Hz)']
- local_units = [u.Unit(unit) for unit in locally_defined_flux_units]
+ if u.sr not in flux_unit.bases:
+ locally_defined_flux_units = ['Jy', 'mJy', 'uJy', 'MJy', 'Jy',
+ 'W / (Hz m2)',
+ 'eV / (s m2 Hz)',
+ 'erg / (s cm2)',
+ 'erg / (s cm2 Angstrom)',
+ 'erg / (s cm2 Hz)',
+ 'ph / (s cm2 Angstrom)',
+ 'ph / (s cm2 Hz)']
+ local_units = [u.Unit(unit) for unit in locally_defined_flux_units]
+ else:
+ locally_defined_flux_units = ['Jy / sr', 'mJy / sr', 'uJy / sr', 'MJy / sr', 'Jy / sr',
+ 'W / (Hz sr m2)',
+ 'eV / (s m2 Hz sr)',
+ 'erg / (s cm2 sr)',
+ 'erg / (s cm2 Angstrom sr)',
+ 'erg / (s cm2 Hz sr)',
+ 'ph / (s cm2 Angstrom sr)',
+ 'ph / (s cm2 Hz sr)',
+ 'bol / sr', 'AB / sr', 'ST / sr']
+ local_units = [u.Unit(unit) for unit in locally_defined_flux_units]
# Remove overlap units.
curr_flux_unit_equivalencies = list(set(curr_flux_unit_equivalencies)
diff --git a/jdaviz/tests/test_app.py b/jdaviz/tests/test_app.py
index a81ad5ac42..9e923ab301 100644
--- a/jdaviz/tests/test_app.py
+++ b/jdaviz/tests/test_app.py
@@ -1,8 +1,12 @@
import pytest
import numpy as np
+from astropy import units as u
+from astropy.wcs import WCS
+from specutils import Spectrum1D
from jdaviz import Application, Specviz
from jdaviz.configs.default.plugins.gaussian_smooth.gaussian_smooth import GaussianSmooth
+from jdaviz.app import UnitConverterWithSpectral as uc
# This applies to all viz but testing with Imviz should be enough.
@@ -192,3 +196,41 @@ def test_data_associations(imviz_helper):
with pytest.raises(ValueError):
# ensure the parent actually exists:
imviz_helper.load_data(data_child, data_label='child_data', parent='absent parent')
+
+
+def test_to_unit(cubeviz_helper):
+ # custom cube to have Surface Brightness units
+ wcs_dict = {"CTYPE1": "WAVE-LOG", "CTYPE2": "DEC--TAN", "CTYPE3": "RA---TAN",
+ "CRVAL1": 4.622e-7, "CRVAL2": 27, "CRVAL3": 205,
+ "CDELT1": 8e-11, "CDELT2": 0.0001, "CDELT3": -0.0001,
+ "CRPIX1": 0, "CRPIX2": 0, "CRPIX3": 0, "PIXAR_SR": 8e-11}
+ w = WCS(wcs_dict)
+ flux = np.zeros((30, 20, 3001), dtype=np.float32)
+ flux[5:15, 1:11, :] = 1
+ cube = Spectrum1D(flux=flux * (u.MJy / u.sr), wcs=w, meta=wcs_dict)
+ cubeviz_helper.load_data(cube, data_label="test")
+
+ # this can be removed once spectra pass through spectral extraction
+ extract_plg = cubeviz_helper.plugins['Spectral Extraction']
+
+ extract_plg.aperture = extract_plg.aperture.choices[-1]
+ extract_plg.aperture_method.selected = 'Exact'
+ extract_plg.wavelength_dependent = True
+ extract_plg.function = 'Sum'
+ # set so pixel scale factor != 1
+ extract_plg.reference_spectral_value = 0.000001
+
+ extract_plg.collapse_to_spectrum()
+
+ cid = cubeviz_helper.app.data_collection[0].data.find_component_id('flux')
+ data = cubeviz_helper.app.data_collection[-1].data
+ values = 1
+ original_units = u.MJy / u.sr
+ target_units = u.MJy
+
+ value = uc.to_unit(cubeviz_helper, data, cid, values, original_units, target_units)
+
+ assert np.allclose(value, 4.7945742429049767e-11)
+
+ original_units = u.MJy
+ target_units = u.MJy / u.sr