diff --git a/CHANGES.rst b/CHANGES.rst index e0469340fc..fbbdb5b603 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,7 +5,7 @@ New Features ------------ - Added flux/surface brightness translation and surface brightness - unit conversion in Cubeviz and Specviz. [#2781, #2940, #3088, #3111, #3113, #3129, #3155] + unit conversion in Cubeviz and Specviz. [#2781, #2940, #3088, #3111, #3113, #3129, #3139, #3155] - Plugin tray is now open by default. [#2892] diff --git a/jdaviz/app.py b/jdaviz/app.py index 7674ec5c51..b552fc018e 100644 --- a/jdaviz/app.py +++ b/jdaviz/app.py @@ -75,18 +75,16 @@ def equivalent_units(self, data, cid, units): 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 Angstrom)', 'erg / (s cm2 Angstrom)', - 'erg / (s cm2 Hz)', 'erg / (Hz s cm2)', - 'ph / (Angstrom s cm2)', - 'ph / (Hz s cm2)', 'ph / (Hz s cm2)' + 'W / (Hz m2)', 'eV / (Hz s m2)', + 'erg / (Hz s cm2)', 'erg / (Angstrom s cm2)', + 'ph / (Angstrom s cm2)', 'ph / (Hz s cm2)' ] + [ 'Jy / sr', 'mJy / sr', 'uJy / sr', 'MJy / sr', - 'W / (Hz sr m2)', - 'eV / (Hz s sr m2)', - 'erg / (s sr cm2)', + 'W / (Hz sr m2)', 'eV / (Hz s sr m2)', + 'erg / (s sr cm2)', 'erg / (Hz s sr cm2)', + 'erg / (Angstrom s sr cm2)', + 'ph / (Angstrom s sr cm2)', 'ph / (Hz s sr cm2)' ]) else: # spectral axis # prefer Hz over Bq and um over micron @@ -108,7 +106,7 @@ def to_unit(self, data, cid, values, original_units, target_units): except RuntimeError: data = data.get_object(cls=NDDataArray) spec = Spectrum1D(flux=data.data * u.Unit(original_units)) - return flux_conversion(spec, values, original_units, target_units) + return flux_conversion(values, original_units, target_units, spec) else: # spectral axis return spectral_axis_conversion(values, original_units, target_units) diff --git a/jdaviz/configs/imviz/plugins/coords_info/coords_info.py b/jdaviz/configs/imviz/plugins/coords_info/coords_info.py index 27115b92f4..1e0b82983b 100644 --- a/jdaviz/configs/imviz/plugins/coords_info/coords_info.py +++ b/jdaviz/configs/imviz/plugins/coords_info/coords_info.py @@ -17,7 +17,7 @@ from jdaviz.core.marks import PluginScatter, PluginLine from jdaviz.core.registries import tool_registry from jdaviz.core.template_mixin import TemplateMixin, DatasetSelectMixin -from jdaviz.utils import _eqv_pixar_sr, _convert_surface_brightness_units +from jdaviz.utils import flux_conversion, _eqv_pixar_sr __all__ = ['CoordsInfo'] @@ -482,11 +482,21 @@ def _image_viewer_update(self, viewer, x, y): value = self._get_cube_value( image, arr, x, y, viewer ) - if self.image_unit is not None and self.image_unit.is_equivalent(unit): - value = _convert_surface_brightness_units( - value, unit, self.image_unit - ) - unit = self.image_unit + if self.image_unit is not None: + if 'PIXAR_SR' in self.app.data_collection[0].meta: + # Need current slice value and associated unit to use to compute + # spectral density equivalencies that enable Flux to Flux conversions. + # This is needed for units that are not directly convertible/translatable. + slice = viewer.slice_value * u.Unit(self.app._get_display_unit('spectral')) + + value = flux_conversion(value, unit, self.image_unit, + eqv=_eqv_pixar_sr(self.app.data_collection[0].meta['PIXAR_SR']), # noqa: E501 + slice=slice) + unit = self.image_unit + + elif self.image_unit.is_equivalent(unit): + value = (value * u.Unit(unit)).to_value(u.Unit(self.image_unit)) + unit = self.image_unit if associated_dq_layers is not None: associated_dq_layer = associated_dq_layers[0] @@ -590,8 +600,7 @@ def _copy_axes_to_spectral(): # 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.spectral_density(sp.spectral_axis) + _eqv_pixar_sr(sp.meta['_pixel_scale_factor']) # noqa - disp_flux = sp.flux.to_value(viewer.state.y_display_unit, eqv) + disp_flux = flux_conversion(sp.flux.value, sp.flux.unit, viewer.state.y_display_unit, spec=sp) # noqa: E501 else: disp_flux = sp.flux.to_value(viewer.state.y_display_unit, u.spectral_density(sp.spectral_axis)) diff --git a/jdaviz/configs/specviz/plugins/unit_conversion/unit_conversion.py b/jdaviz/configs/specviz/plugins/unit_conversion/unit_conversion.py index 4d1b90870c..34e19145ff 100644 --- a/jdaviz/configs/specviz/plugins/unit_conversion/unit_conversion.py +++ b/jdaviz/configs/specviz/plugins/unit_conversion/unit_conversion.py @@ -9,7 +9,6 @@ from jdaviz.core.validunits import (create_spectral_equivalencies_list, create_flux_equivalencies_list, check_if_unit_is_per_solid_angle, - units_to_strings, create_angle_equivalencies_list) __all__ = ['UnitConversion'] @@ -69,7 +68,6 @@ class UnitConversion(PluginTemplateMixin): flux_or_sb_items = List().tag(sync=True) flux_or_sb_selected = Unicode().tag(sync=True) - can_translate = Bool(True).tag(sync=True) # This is used a warning message if False. This can be changed from # bool to unicode when we eventually handle inputing this value if it # doesn't exist in the FITS header @@ -170,9 +168,8 @@ def _on_glue_y_display_unit_changed(self, y_unit_str): # if the y-axis is set to surface brightness, # untranslatable units need to be removed from the flux choices if check_if_unit_is_per_solid_angle(y_unit_str): - updated_flux_choices = list(set(create_flux_equivalencies_list(y_unit * u.sr, x_unit)) - - set(units_to_strings(self._untranslatable_units))) - self.flux_unit.choices = updated_flux_choices + flux_choices = create_flux_equivalencies_list(y_unit * u.sr, x_unit) + self.flux_unit.choices = flux_choices # sets the angle unit drop down and the surface brightness read-only text if self.app.data_collection[0]: @@ -183,6 +180,19 @@ def _on_glue_y_display_unit_changed(self, y_unit_str): self.flux_unit.selected, self.angle_unit.selected ) + if self.angle_unit.selected == 'pix': + mouseover_unit = self.flux_unit.selected + else: + mouseover_unit = self.sb_unit_selected + self.hub.broadcast(GlobalDisplayUnitChanged("sb", mouseover_unit, sender=self)) + + else: + # if cube was loaded in flux units, we still need to broadcast + # a 'sb' message for mouseover info. this should be removed when + # unit change messaging is improved and is a temporary fix + self.hub.broadcast(GlobalDisplayUnitChanged('sb', + self.flux_unit.selected, + sender=self)) if not self.flux_unit.selected: y_display_unit = self.spectrum_viewer.state.y_display_unit @@ -242,15 +252,6 @@ def _on_flux_unit_changed(self, msg): spectral_y = sb_unit if self.flux_or_sb == 'Surface Brightness' else flux_unit - untranslatable_units = self._untranslatable_units - # disable translator if flux unit is untranslatable, - # still can convert flux units, this just disables flux - # to surface brightness translation for units in list. - if spectral_y in untranslatable_units: - self.can_translate = False - else: - self.can_translate = True - yunit = _valid_glue_display_unit(spectral_y, self.spectrum_viewer, 'y') # update spectrum viewer with new y display unit @@ -280,18 +281,6 @@ def _translate(self, flux_or_sb=None): if self.app.config == 'specviz': return - # we want to raise an error if a user tries to translate with an - # untranslated Flux unit using the API - untranslatable_units = units_to_strings(self._untranslatable_units) - - if hasattr(self, 'flux_unit'): - if ((self.flux_unit.selected in untranslatable_units) - and (flux_or_sb == 'Surface Brightness')): - raise ValueError( - "Selected flux unit is not translatable. Please choose a flux unit " - f"that is not in the following list: {untranslatable_units}." - ) - if self.spectrum_viewer.state.y_display_unit: spec_units = u.Unit(self.spectrum_viewer.state.y_display_unit) else: @@ -325,17 +314,6 @@ def _translate(self, flux_or_sb=None): sender=self)) self.spectrum_viewer.reset_limits() - @property - def _untranslatable_units(self): - return [ - 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.Angstrom * u.s * u.cm**2), - u.ph / (u.s * u.cm**2 * u.Hz), - u.ST, u.bol - ] - def _append_angle_correctly(self, flux_unit, angle_unit): if angle_unit not in ['pix', 'sr']: self.sb_unit_selected = flux_unit diff --git a/jdaviz/configs/specviz/plugins/unit_conversion/unit_conversion.vue b/jdaviz/configs/specviz/plugins/unit_conversion/unit_conversion.vue index d0c6bbdcf9..09fed8b73f 100644 --- a/jdaviz/configs/specviz/plugins/unit_conversion/unit_conversion.vue +++ b/jdaviz/configs/specviz/plugins/unit_conversion/unit_conversion.vue @@ -74,9 +74,7 @@ :class="api_hints_enabled ? 'api-hint' : null" hint="Select the y-axis physical type for the spectrum-viewer." persistent-hint - :disabled="!can_translate" > - Translation is not available due to current unit selection. diff --git a/jdaviz/core/helpers.py b/jdaviz/core/helpers.py index dd03156ecd..194a3dfc80 100644 --- a/jdaviz/core/helpers.py +++ b/jdaviz/core/helpers.py @@ -487,8 +487,8 @@ def _handle_display_units(self, data, use_display_units=True): # if not specified as NDUncertainty, assume stddev: new_uncert = uncertainty if ('_pixel_scale_factor' in data.meta): - new_uncert_converted = flux_conversion(data, new_uncert.quantity.value, - new_uncert.unit, y_unit) + new_uncert_converted = flux_conversion(new_uncert.quantity.value, + new_uncert.unit, y_unit, spec=data) new_uncert = StdDevUncertainty(new_uncert_converted, unit=y_unit) else: new_uncert = StdDevUncertainty(new_uncert, unit=data.flux.unit) @@ -496,11 +496,11 @@ def _handle_display_units(self, data, use_display_units=True): else: new_uncert = None if ('_pixel_scale_factor' in data.meta): - new_y = flux_conversion(data, data.flux.value, data.flux.unit, - y_unit) * u.Unit(y_unit) + new_y = flux_conversion(data.flux.value, data.flux.unit, + y_unit, data) * u.Unit(y_unit) else: - new_y = flux_conversion(data, data.flux.value, data.flux.unit, - data.flux.unit) * u.Unit(data.flux.unit) + new_y = flux_conversion(data.flux.value, data.flux.unit, + data.flux.unit, spec=data) * u.Unit(data.flux.unit) new_spec = (spectral_axis_conversion(data.spectral_axis.value, data.spectral_axis.unit, spectral_unit) diff --git a/jdaviz/core/validunits.py b/jdaviz/core/validunits.py index 8aad68ac63..578c94f175 100644 --- a/jdaviz/core/validunits.py +++ b/jdaviz/core/validunits.py @@ -73,12 +73,9 @@ def create_flux_equivalencies_list(flux_unit, spectral_axis_unit): # Get local flux units. locally_defined_flux_units = ['Jy', 'mJy', 'uJy', 'MJy', - 'W / (Hz m2)', - 'eV / (s m2 Hz)', - 'erg / (s cm2 Hz)', - 'erg / (s cm2 Angstrom)', - 'ph / (Angstrom s cm2)', - 'ph / (Hz s cm2)', + 'W / (Hz m2)', 'eV / (Hz s m2)', + 'erg / (s cm2 Angstrom)', 'erg / (Hz s cm2)', + 'ph / (Angstrom s cm2)', 'ph / (Hz s cm2)' ] local_units = [u.Unit(unit) for unit in locally_defined_flux_units] diff --git a/jdaviz/tests/test_app.py b/jdaviz/tests/test_app.py index e2d3ca11bb..42fd5541a5 100644 --- a/jdaviz/tests/test_app.py +++ b/jdaviz/tests/test_app.py @@ -227,8 +227,8 @@ def test_to_unit(cubeviz_helper): original_units = u.MJy / u.sr target_units = u.MJy - value = flux_conversion(data.get_object(cls=Spectrum1D), - values, original_units, target_units) + value = flux_conversion(values, original_units, + target_units, data.get_object(cls=Spectrum1D)) # will be a uniform array since not wavelength dependent # so test first value in array @@ -240,8 +240,8 @@ def test_to_unit(cubeviz_helper): original_units = u.MJy target_units = u.erg / u.cm**2 / u.s / u.AA - new_values = flux_conversion(data.get_object(cls=Spectrum1D), values, - original_units, target_units) + new_values = flux_conversion(values, original_units, + target_units, data.get_object(cls=Spectrum1D)) assert np.allclose(new_values, (values * original_units) @@ -255,8 +255,8 @@ def test_to_unit(cubeviz_helper): original_units = u.MJy target_units = u.erg / u.cm**2 / u.s / u.AA - new_values = flux_conversion(data.get_object(cls=Spectrum1D), values, - original_units, target_units) + new_values = flux_conversion(values, original_units, + target_units, data.get_object(cls=Spectrum1D)) # In this case we do a regular spectral density conversion, but using the # first value in the spectral axis for the equivalency diff --git a/jdaviz/tests/test_utils.py b/jdaviz/tests/test_utils.py index bf4f7ceb2d..86294bcd12 100644 --- a/jdaviz/tests/test_utils.py +++ b/jdaviz/tests/test_utils.py @@ -10,7 +10,8 @@ from numpy.testing import assert_allclose from specutils import Spectrum1D -from jdaviz.utils import alpha_index, download_uri_to_path, flux_conversion +from jdaviz.utils import (alpha_index, download_uri_to_path, flux_conversion, + _indirect_conversion, _eqv_pixar_sr) PHOTUTILS_LT_1_12_1 = not minversion(photutils, "1.12.1.dev") @@ -24,50 +25,120 @@ def test_spec_sb_flux_conversion(): # Float scalar pixel scale factor spec.meta["_pixel_scale_factor"] = 0.1 - assert_allclose(flux_conversion(spec, values, u.Jy / u.sr, u.Jy), [1, 2, 3]) - assert_allclose(flux_conversion(spec, values, u.Jy, u.Jy / u.sr), [100, 200, 300]) + assert_allclose(flux_conversion(values, u.Jy / u.sr, u.Jy, spec), [1, 2, 3]) + assert_allclose(flux_conversion(values, u.Jy, u.Jy / u.sr, spec), [100, 200, 300]) + + # complex translation Jy / sr -> erg / (Angstrom s cm2 sr) + targ = [2.99792458e-12, 1.49896229e-12, 9.99308193e-13] * (u.erg / (u.Angstrom * u.s * u.cm**2 * u.sr)) # noqa: E501 + assert_allclose(flux_conversion(values, u.Jy / u.sr, u.erg / (u.Angstrom * u.s * u.cm**2 * u.sr), spec), targ.value) # noqa: E501 + + # complex translation erg / (Angstrom s cm2 sr) -> Jy / sr + targ = [3.33564095e+13, 2.66851276e+14, 9.00623057e+14] * (u.Jy / u.sr) + assert_allclose(flux_conversion(values, u.erg / (u.Angstrom * u.s * u.cm**2 * u.sr), u.Jy / u.sr, spec), targ.value) # noqa: E501 + + spectral_values = spec.spectral_axis + spec_unit = u.MJy + eqv = u.spectral_density(spectral_values) + _eqv_pixar_sr(spec.meta["_pixel_scale_factor"]) + + # test spectrum when target unit in untranslatable unit list + target_values = [5.03411657e-05, 2.01364663e-04, 4.53070491e-04] + expected_units = (u.ph / (u.Hz * u.s * u.cm**2)) + returned_values, return_units, unit_flag = _indirect_conversion( + values=values, orig_units=(u.MJy), + targ_units=(u.ph / (u.s * u.cm**2 * u.Hz * u.sr)), # noqa + eqv=eqv, spec_unit=spec_unit, image_data=None + ) + assert_allclose(returned_values, target_values) + assert (return_units == expected_units) + assert (unit_flag == 'orig') + + # test spectrum when original unit in untranslatable unit list + target_values = [1., 2., 3.] + expected_units = (u.ph / (u.Angstrom * u.s * u.cm**2)) + returned_values, return_units, unit_flag = _indirect_conversion( + values=values, + orig_units=(u.ph / (u.Angstrom * u.s * u.cm**2 * u.sr)), # noqa + targ_units=(u.MJy), eqv=eqv, + spec_unit=spec_unit, image_data=None + ) + assert_allclose(returned_values, target_values) + assert (return_units == expected_units) + assert (unit_flag == 'targ') + + # test the default case where units are translatable + target_values = [10, 20, 30] + expected_units = (u.MJy) + returned_values, return_units, unit_flag = _indirect_conversion( + values=values, orig_units=(u.Jy/u.sr), + targ_units=(u.MJy), eqv=eqv, + spec_unit=spec_unit, image_data=None + ) + assert_allclose(returned_values, target_values) + assert (return_units == expected_units) + assert (unit_flag == 'targ') + + # test image viewer data units are untranslatable + target_value = 1.e-18 + expected_units = (u.erg / (u.s * u.cm**2 * u.Hz)) + returned_values, return_units = _indirect_conversion( + values=1, orig_units=(u.MJy/u.sr), + targ_units=(u.erg / (u.s * u.cm**2 * u.Hz * u.sr)), + eqv=eqv, spec_unit=None, image_data=True + ) + assert_allclose(returned_values, target_value) + assert return_units == expected_units + + # test image viewer data units are translatable + target_value = 10 + expected_units = (u.MJy / u.sr) + returned_values, return_units = _indirect_conversion( + values=10, orig_units=(u.MJy/u.sr), targ_units=(u.Jy/u.sr), + eqv=eqv, spec_unit=None, image_data=True + ) + assert_allclose(returned_values, target_value) + assert return_units == expected_units # Quantity scalar pixel scale factor spec.meta["_pixel_scale_factor"] = 0.1 * (u.sr / u.pix) - assert_allclose(flux_conversion(spec, values, u.Jy / u.sr, u.Jy), [1, 2, 3]) - assert_allclose(flux_conversion(spec, values, u.Jy, u.Jy / u.sr), [100, 200, 300]) + assert_allclose(flux_conversion(values, u.Jy / u.sr, u.Jy, spec), [1, 2, 3]) + assert_allclose(flux_conversion(values, u.Jy, u.Jy / u.sr, spec), [100, 200, 300]) # values == 2 values = [10, 20] - assert_allclose(flux_conversion(spec, values, u.Jy / u.sr, u.Jy), [1, 2]) - assert_allclose(flux_conversion(spec, values, u.Jy, u.Jy / u.sr), [100, 200]) + assert_allclose(flux_conversion(values, u.Jy / u.sr, u.Jy, spec), [1, 2]) + assert_allclose(flux_conversion(values, u.Jy, u.Jy / u.sr, spec), [100, 200]) # float array pixel scale factor spec.meta["_pixel_scale_factor"] = [0.1, 0.2, 0.3] # min_max = [0.1, 0.3] - assert_allclose(flux_conversion(spec, values, u.Jy / u.sr, u.Jy), [1, 6]) - assert_allclose(flux_conversion(spec, values, u.Jy, u.Jy / u.sr), [100, 66.66666666666667]) + assert_allclose(flux_conversion(values, u.Jy / u.sr, u.Jy, spec), [1, 6]) + assert_allclose(flux_conversion(values, u.Jy, u.Jy / u.sr, spec), [100, 66.66666666666667]) # Quantity array pixel scale factor spec.meta["_pixel_scale_factor"] = [0.1, 0.2, 0.3] * (u.sr / u.pix) # min_max = [0.1, 0.3] - assert_allclose(flux_conversion(spec, values, u.Jy / u.sr, u.Jy), [1, 6]) - assert_allclose(flux_conversion(spec, values, u.Jy, u.Jy / u.sr), [100, 66.66666666666667]) + assert_allclose(flux_conversion(values, u.Jy / u.sr, u.Jy, spec), [1, 6]) + assert_allclose(flux_conversion(values, u.Jy, u.Jy / u.sr, spec), [100, 66.66666666666667]) # values != 2 values = [10, 20, 30] spec.meta["_pixel_scale_factor"] = [0.1, 0.2, 0.3] - assert_allclose(flux_conversion(spec, values, u.Jy / u.sr, u.Jy), [1, 4, 9]) - assert_allclose(flux_conversion(spec, values, u.Jy, u.Jy / u.sr), 100) + assert_allclose(flux_conversion(values, u.Jy / u.sr, u.Jy, spec=spec), [1, 4, 9]) + assert_allclose(flux_conversion(values, u.Jy, u.Jy / u.sr, spec=spec), 100) # values != 2 but _pixel_scale_factor size mismatch with pytest.raises(ValueError, match="operands could not be broadcast together"): spec.meta["_pixel_scale_factor"] = [0.1, 0.2, 0.3, 0.4] - flux_conversion(spec, values, u.Jy / u.sr, u.Jy) + flux_conversion(values, u.Jy / u.sr, u.Jy, spec=spec) # Other kind of flux conversion unrelated to _pixel_scale_factor. # The answer was obtained from synphot unit conversion. spec.meta["_pixel_scale_factor"] = 0.1 targ = [2.99792458e-12, 1.49896229e-12, 9.99308193e-13] * (u.erg / (u.AA * u.cm * u.cm * u.s)) # FLAM # noqa: E501 - assert_allclose(flux_conversion(spec, values, u.Jy, targ.unit), targ.value) + assert_allclose(flux_conversion(values, u.Jy, targ.unit, spec=spec), targ.value) # values == 2 (only used spec.spectral_axis[0] for some reason) values = [10, 20] targ = [2.99792458e-12, 5.99584916e-12] * (u.erg / (u.AA * u.cm * u.cm * u.s)) # FLAM - assert_allclose(flux_conversion(spec, values, u.Jy, targ.unit), targ.value) + assert_allclose(flux_conversion(values, u.Jy, targ.unit, spec=spec), targ.value) @pytest.mark.parametrize("test_input,expected", [(0, 'a'), (1, 'b'), (25, 'z'), (26, 'aa'), diff --git a/jdaviz/utils.py b/jdaviz/utils.py index a9d0d517f6..ff19929662 100644 --- a/jdaviz/utils.py +++ b/jdaviz/utils.py @@ -21,6 +21,8 @@ from glue.core.subset import SubsetState, RangeSubsetState, RoiSubsetState from ipyvue import watch +from jdaviz.core.validunits import check_if_unit_is_per_solid_angle + __all__ = ['SnackbarQueue', 'enable_hot_reloading', 'bqplot_clear_figure', 'standardize_metadata', 'ColorCycler', 'alpha_index', 'get_subset_type', 'download_uri_to_path', 'flux_conversion', 'spectral_axis_conversion', @@ -286,31 +288,48 @@ def standardize_metadata(metadata): return out_meta -def flux_conversion(spec, values, original_units, target_units): +def indirect_units(): + return [ + u.erg / (u.s * u.cm**2 * u.Angstrom * u.sr), + u.erg / (u.s * u.cm**2 * u.Hz * u.sr), + u.ph / (u.Angstrom * u.s * u.cm**2 * u.sr), u.ph / (u.Angstrom * u.s * u.sr * u.cm**2), + u.ph / (u.s * u.cm**2 * u.Hz * u.sr) + ] + + +def flux_conversion(values, original_units, target_units, spec=None, eqv=None, slice=None): """ - Given a Spectrum1D object, flux values, original flux units, and target units, - this method will return the flux values in the converted units. This conversion - takes into account the possible surface brightness to flux or vice versa change that - may happen between units. + Convert flux or surface brightness values from original units to target units. + + This function handles the conversion of flux or surface brightness values between different + units, taking into account changes between flux and surface brightness. It supports complex + conversions for Spectrum1D objects or cube image data. Parameters ---------- - spec : `~specutils.Spectrum1D` object - The Spectrum1D object that will have converted flux units. - values : float array - Flux values in the original units. + Flux or surface brightness values in the original units. original_units : str - The flux units of the spec object. + The flux or surface brightness units of the spec object or cube image. target_units : str - The units the flux will be converted to. + The units the flux or surface brightness will be converted to. + + spec : `~specutils.Spectrum1D`, optional + The Spectrum1D object that will have converted flux or surface brightness units. + + eqv : list of `astropy.units.equivalencies`, optional + A list of Astropy equivalencies necessary for complex unit conversions/translations. + + slice : `astropy.units.Quantity`, optional + The current slice of a data cube, with units. Necessary for complex unit + conversions/translations that require spectral density equivalencies. Returns ------- result : float array - Flux values in the target units. + Flux or surface brightness values in the target units. """ # we set surface brightness choices and selection before flux, which can # cause a dimensionless translation attempt at instantiation @@ -319,13 +338,22 @@ def flux_conversion(spec, values, original_units, target_units): # If there are only two values, this is likely the limits being converted, so then # in case we need to use the spectral density equivalency, we need to provide only # to spectral axis values. If there is only one value - if not np.isscalar(values) and len(values) == 2: - spectral_values = spec.spectral_axis[0] - else: - spectral_values = spec.spectral_axis + image_data = False + if spec: + if not np.isscalar(values) and len(values) == 2: + spectral_values = spec.spectral_axis[0] + else: + spectral_values = spec.spectral_axis + + # the unit of the data collection item object, could be flux or surface brightness + spec_unit = str(spec.flux.unit) - # Need this for setting the y-limits - eqv = u.spectral_density(spectral_values) + # Need this for setting the y-limits + eqv = u.spectral_density(spectral_values) + elif slice is not None and eqv: + image_data = True + # Need this to convert Flux to Flux for complex conversions/translations of cube image data + eqv += u.spectral_density(slice) orig_units = u.Unit(original_units) orig_bases = orig_units.bases @@ -333,7 +361,7 @@ def flux_conversion(spec, values, original_units, target_units): targ_bases = targ_units.bases # Ensure a spectrum passed through Spectral Extraction plugin - if (('_pixel_scale_factor' in spec.meta) and + if (((spec and ('_pixel_scale_factor' in spec.meta))) and (((u.sr in orig_bases) and (u.sr not in targ_bases)) or ((u.sr not in orig_bases) and (u.sr in targ_bases)))): # Data item in data collection does not update from conversion/translation. @@ -351,15 +379,72 @@ def flux_conversion(spec, values, original_units, target_units): eqv_in = [min(fac), max(fac)] else: eqv_in = fac - eqv += _eqv_pixar_sr(np.array(eqv_in)) + # indirect units cannot be directly converted, and require + # additional conversions to reach the desired end unit. + # if spec_unit in [original_units, target_units]: + result = _indirect_conversion( + values=values, orig_units=orig_units, targ_units=targ_units, + eqv=eqv, spec_unit=spec_unit + ) + + if result and len(result) == 2: + values, updated_units = result + orig_units = updated_units + else: + values, updated_units, selected_unit_updated = result + if selected_unit_updated == 'targ': + targ_units = updated_units + elif selected_unit_updated == 'orig': + orig_units = updated_units + + elif image_data: + values, orig_units = _indirect_conversion( + values=values, orig_units=orig_units, targ_units=targ_units, + eqv=eqv, image_data=image_data + ) + return (values * orig_units).to_value(targ_units, equivalencies=eqv) -def _convert_surface_brightness_units(data, from_unit, to_unit): - quantity = data * u.Unit(from_unit) - return quantity.to_value(u.Unit(to_unit)) +def _indirect_conversion(values, orig_units, targ_units, eqv, + spec_unit=None, image_data=None): + # indirect units cannot be directly converted, and require + # additional conversions to reach the desired end unit. + if (spec_unit and spec_unit in [orig_units, targ_units] + and not check_if_unit_is_per_solid_angle(spec_unit)): + if u.Unit(targ_units) in indirect_units(): + temp_targ = targ_units * u.sr + values = (values * orig_units).to_value(temp_targ, equivalencies=eqv) + orig_units = u.Unit(temp_targ) + return values, orig_units, 'orig' + elif u.Unit(orig_units) in indirect_units(): + temp_orig = orig_units * u.sr + values = (values * orig_units).to_value(temp_orig, equivalencies=eqv) + targ_units = u.Unit(temp_orig) + return values, targ_units, 'targ' + + return values, targ_units, 'targ' + + elif image_data or (spec_unit and check_if_unit_is_per_solid_angle(spec_unit)): + if not check_if_unit_is_per_solid_angle(targ_units): + targ_units /= u.sr + if ((u.Unit(targ_units) in indirect_units()) or + (u.Unit(orig_units) in indirect_units())): + # SB -> Flux -> Flux -> SB + temp_orig = orig_units * u.sr + temp_targ = targ_units * u.sr + + # Convert Surface Brightness to Flux, then Flux to Flux + values = (values * orig_units).to_value(temp_orig, equivalencies=eqv) + values = (values * temp_orig).to_value(temp_targ, equivalencies=eqv) + # Lastly a Flux to Surface Brightness translation in the return statement + orig_units = temp_targ + + return values, orig_units + + return values, orig_units def _eqv_pixar_sr(pixar_sr): @@ -369,7 +454,12 @@ def converter_flux(x): # Surface Brightness -> Flux def iconverter_flux(x): # Flux -> Surface Brightness return x / pixar_sr - return [(u.MJy / u.sr, u.MJy, converter_flux, iconverter_flux)] + return [ + (u.MJy / u.sr, u.MJy, converter_flux, iconverter_flux), + (u.erg / (u.s * u.cm**2 * u.Angstrom * u.sr), u.erg / (u.s * u.cm**2 * u.Angstrom), converter_flux, iconverter_flux), # noqa + (u.ph / (u.Angstrom * u.s * u.cm**2 * u.sr), u.ph / (u.Angstrom * u.s * u.cm**2), converter_flux, iconverter_flux), # noqa + (u.ph / (u.Hz * u.s * u.cm**2 * u.sr), u.ph / (u.Hz * u.s * u.cm**2), converter_flux, iconverter_flux) # noqa + ] def spectral_axis_conversion(values, original_units, target_units):