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