From 5cbbdaece6c9346d23d20a0cc5d4f1d355c6a51b Mon Sep 17 00:00:00 2001
From: Gilbert Green <42986583+gibsongreen@users.noreply.github.com>
Date: Tue, 7 May 2024 09:38:21 -0400
Subject: [PATCH] toggle flux <-> surface brightness units (#2781)
* add toggle feature between surface brightness and flux, currently incomplete
* custom eqv added, hide toggle with traitlet, move test
* add sb units to equiv_units(), cursor to recognize default data toggle, sb/flux dropdown updating with correct units
* add sb units to valid units, change from toggle to dropdown for sb/flux
* changing from switch to dropdown, updating logic for conversion
* removing toggle from spectral extraction
* resolve UnitConversionError tracebacks in notebook
* change flux_unit to flux_or_sb_unit in tests, seperate translation test from sb conversion test
* resolve CI test failures
* adding flux_or_sb doc entry, and TEMP exposing flux_or_sb_unit to verify CI testing
* fix styling
* add test coverage
* add change log
---
CHANGES.rst | 3 +
jdaviz/app.py | 59 +++++++--
.../spectral_extraction.py | 11 --
.../tests/test_spectral_extraction.py | 48 --------
.../imviz/plugins/coords_info/coords_info.py | 14 ++-
.../tests/test_unit_conversion.py | 112 ++++++++++++++++++
.../unit_conversion/unit_conversion.py | 98 ++++++++++++---
.../unit_conversion/unit_conversion.vue | 22 ++--
jdaviz/configs/specviz/plugins/viewers.py | 23 +++-
jdaviz/configs/specviz/tests/test_viewers.py | 4 +-
jdaviz/core/validunits.py | 32 +++--
jdaviz/tests/test_app.py | 42 +++++++
12 files changed, 358 insertions(+), 110 deletions(-)
diff --git a/CHANGES.rst b/CHANGES.rst
index 2d291f6c0e..c4b3970a08 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -4,6 +4,9 @@
New Features
------------
+- Adding flux/surface brightness translation and surface brightness
+ unit conversion in Cubeviz and Specviz. [#2781]
+
Cubeviz
^^^^^^^
diff --git a/jdaviz/app.py b/jdaviz/app.py
index 2c0b4d6886..8b60246c98 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()
diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py
index 1851c4853d..d5d558fcbc 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.utils.decorators import deprecated
from astropy.nddata import (
NDDataArray, StdDevUncertainty
@@ -562,13 +561,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 6d2c687e7a..120ddbe91c 100644
--- a/jdaviz/configs/imviz/plugins/coords_info/coords_info.py
+++ b/jdaviz/configs/imviz/plugins/coords_info/coords_info.py
@@ -573,8 +573,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 b9838ecab2..9ead4b9d98 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:
@@ -100,7 +113,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)
@@ -110,11 +123,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)
@@ -122,8 +135,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):
@@ -131,14 +158,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