Skip to content

Commit

Permalink
expanded support of SB/Flux translations (#2940)
Browse files Browse the repository at this point in the history
* UI functioning unit conversion, need to reconcile tests

* update syntax, remove unnecessary logic

* tests now pass, API functionality improved

* add additional equivalencies for coords info

* first pass at disabling specviz translations

* Remove dev flag, consolidate untranslatable units, multi-config support, styling changes

* Refactor moment map unit conversion
to not require spectrum, so nothing to centralize now.

Fix devdeps

Simplify flux_conversion logic
and other minor fixes. Add flux conversion tests.

Clarify which one should be reverted
in the future

* Remove SB/flux conversion for moment map
because moment map always in SB

* add os import

* remove duplicate import from rebase

* reconcile tests, change hints/docs, handle dimensionless case

* emit GlobalDisplayUnitChanged msg when toggling SB<>flux

* resolve most tests

* temporarily remove translation message

* add comments, reenable global display message, resolve tests

* force sb unit for line flux test

* add comments, clarify naming, add case handling for args

* remove args and loop, replaced with msg

* consolidate 2 translator functions into 1, consolidate _translate and _on_flux_unit_changed

* address test failures

* update marks.py equivalencies, resolving UnitConversion errors

* make sure not only translation equivalencies are in marks.py but also conversions

* add test coverage

* use astropy units, small logic tweaks, cache untranslatable units

* technical review changes

* add warning message in tray if PIXAR_SR not present in FITS header

* second pass at technical changes

* move flux_unit checks to beginning of _on_flux_unit_changed

* change _unit to flux.unit

---------

Co-authored-by: P. L. Lim <[email protected]>
Co-authored-by: Kyle Conroy <[email protected]>
  • Loading branch information
3 people authored Jul 23, 2024
1 parent 22ed4cf commit 143cd8d
Show file tree
Hide file tree
Showing 14 changed files with 455 additions and 230 deletions.
2 changes: 1 addition & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ New Features
------------

- Added flux/surface brightness translation and surface brightness
unit conversion in Cubeviz and Specviz. [#2781, #3088]
unit conversion in Cubeviz and Specviz. [#2781, #2940, #3088]

- Plugin tray is now open by default. [#2892]

Expand Down
11 changes: 4 additions & 7 deletions jdaviz/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,20 +76,17 @@ def equivalent_units(self, data, cid, units):
'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 / (s cm2 Angstrom)',
'erg / (s cm2 Hz)', 'erg / (Hz s cm2)',
'ph / (s cm2 Angstrom)', 'ph / (s cm2 Angstrom)',
'ph / (Angstrom s cm2)',
'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'
'eV / (Hz s sr m2)',
'erg / (s sr cm2)',
'AB / sr'
])
else: # spectral axis
# prefer Hz over Bq and um over micron
Expand Down
15 changes: 1 addition & 14 deletions jdaviz/configs/cubeviz/plugins/moment_maps/moment_maps.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@
with_spinner)
from jdaviz.core.validunits import check_if_unit_is_per_solid_angle
from jdaviz.core.user_api import PluginUserApi
from jdaviz.utils import flux_conversion


__all__ = ['MomentMap']

Expand Down Expand Up @@ -358,18 +356,7 @@ def calculate_moment(self, add_data=True):
moment_new_unit = flux_or_sb_display_unit
else:
moment_new_unit = flux_or_sb_display_unit * self.spectrum_viewer.state.x_display_unit # noqa: E501

# Create a temporary Spectrum1D object with ability to convert from surface brightness
# to flux
temp_spec = Spectrum1D(flux=self.moment)
flux_values = np.sum(np.ones_like(temp_spec.flux.value), axis=(0, 1))
pix_scale = self.dataset.selected_dc_item.meta.get('PIXAR_SR', 1.0)
pix_scale_factor = (flux_values * pix_scale)
temp_spec.meta['_pixel_scale_factor'] = pix_scale_factor
converted_spec = flux_conversion(temp_spec, self.moment.value,
self.moment.unit,
moment_new_unit) * moment_new_unit
self.moment = converted_spec
self.moment = self.moment.to(moment_new_unit)

# Reattach the WCS so we can load the result
self.moment = CCDData(self.moment, wcs=data_wcs)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ def test_correct_output_flux_or_sb_units(cubeviz_helper, spectrum1d_cube_custom_

# now change surface brightness units in the unit conversion plugin

uc.flux_or_sb_unit = 'Jy / sr'
uc.sb_unit = 'Jy / sr'

# and make sure this change is propogated
output_unit_moment_0 = mm.output_unit_items[0]
Expand All @@ -344,21 +344,3 @@ def test_correct_output_flux_or_sb_units(cubeviz_helper, spectrum1d_cube_custom_
# and that calculated moment has the correct units
mm.calculate_moment()
assert mm.moment.unit == moment_unit

uc.flux_or_sb.selected = 'Flux'
mm._set_data_units()

# and make sure this change is propogated
output_unit_moment_0 = mm.output_unit_items[0]
assert output_unit_moment_0['label'] == 'Flux'
assert output_unit_moment_0['unit_str'] == 'Jy'

# TODO: Failing because of dev version of upstream dependency, figure
# out which one
# assert mm.calculate_moment()

# TODO: This test should pass once continuum subtraction works with
# flux to surface brightness conversion
# mm.continuum.selected = 'Surrounding'
#
# assert mm.calculate_moment()
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,15 @@ def extract(self, return_bg=False, add_data=True, **kwargs):
pix_scale_factor = self.aperture_area_along_spectral * self.spectral_cube.meta.get('PIXAR_SR', 1.0) # noqa
spec.meta['_pixel_scale_factor'] = pix_scale_factor

# inform the user if scale factor keyword not in metadata
if 'PIXAR_SR' not in self.spectral_cube.meta:
snackbar_message = SnackbarMessage(
("PIXAR_SR FITS header keyword not found when parsing spectral cube. "
"Flux/Surface Brightness will use default PIXAR_SR value of 1 sr/pix^2."),
color="warning",
sender=self)
self.hub.broadcast(snackbar_message)

# stuff for exporting to file
self.extracted_spec = spec
self.extracted_spec_available = True
Expand Down
6 changes: 2 additions & 4 deletions jdaviz/configs/imviz/plugins/coords_info/coords_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +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

__all__ = ['CoordsInfo']

Expand Down Expand Up @@ -561,10 +562,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.MJy / u.sr,
u.MJy,
lambda x: (x * sp.meta['_pixel_scale_factor']),
lambda x: x)]
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)
else:
disp_flux = sp.flux.to_value(viewer.state.y_display_unit,
Expand Down
15 changes: 13 additions & 2 deletions jdaviz/configs/specviz/plugins/line_analysis/line_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,11 @@ def _uncertainty(result):
# don't need these if statements
if function == "Line Flux":
flux_unit = spec_subtracted.flux.unit
if flux_unit == u.dimensionless_unscaled:
add_flux = True
flux_unit = u.Unit(self.spectrum_viewer.state.y_display_unit)
else:
add_flux = False
# If the flux unit is equivalent to Jy, or Jy per spaxel for Cubeviz,
# enforce integration in frequency space
if (flux_unit.is_equivalent(u.Jy) or
Expand All @@ -300,7 +305,10 @@ def _uncertainty(result):
uncertainty=spec_subtracted.uncertainty)

try:
raw_result = analysis.line_flux(freq_spec)
if add_flux:
raw_result = analysis.line_flux(freq_spec) * flux_unit
else:
raw_result = analysis.line_flux(freq_spec)
except ValueError as e:
# can happen if interpolation out-of-bounds or any error from specutils
# let's avoid the whole app crashing and instead expose the error to the
Expand Down Expand Up @@ -332,7 +340,10 @@ def _uncertainty(result):
flux=spec_subtracted.flux,
uncertainty=spec_subtracted.uncertainty)
try:
raw_result = analysis.line_flux(wave_spec)
if add_flux:
raw_result = analysis.line_flux(wave_spec) * flux_unit
else:
raw_result = raw_result = analysis.line_flux(wave_spec)
except ValueError as e:
# can happen if interpolation out-of-bounds or any error from specutils
# let's avoid the whole app crashing and instead expose the error to the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,14 +89,28 @@ def test_conv_wave_flux(specviz_helper, spectrum1d, uncert):
assert u.Unit(viewer.state.y_display_unit) == u.Unit(new_flux)


def test_conv_no_data(specviz_helper):
def test_conv_no_data(specviz_helper, spectrum1d):
"""plugin unit selections won't have valid choices yet, preventing
attempting to set display units."""
plg = specviz_helper.plugins["Unit Conversion"]
# spectrum not load is in Flux units, sb_unit and flux_unit
# should be enabled, flux_or_sb should not be
assert hasattr(plg, 'sb_unit')
assert hasattr(plg, 'flux_unit')
assert not hasattr(plg, 'flux_or_sb')
with pytest.raises(ValueError, match="no valid unit choices"):
plg.spectral_unit = "micron"
assert len(specviz_helper.app.data_collection) == 0

specviz_helper.load_data(spectrum1d, data_label="Test 1D Spectrum")
plg = specviz_helper.plugins["Unit Conversion"]

# spectrum loaded in Flux units, make sure sb_units don't
# display in the API and exposed translation isn't possible
assert hasattr(plg, 'flux_unit')
assert not hasattr(plg, 'sb_unit')
assert not hasattr(plg, 'flux_or_sb')


@pytest.mark.skipif(ASTROPY_LT_5_3, reason='this feature relies on astropy v5.3+')
def test_non_stddev_uncertainty(specviz_helper):
Expand Down Expand Up @@ -140,40 +154,21 @@ def test_unit_translation(cubeviz_helper):
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.extract()

# test that the scale factor was set
assert np.all(collapsed_spec.meta['_pixel_scale_factor'] != 1)
assert np.all(cubeviz_helper.app.data_collection['Spectrum (sum)'].meta['_pixel_scale_factor'] != 1) # noqa

# 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)

Expand All @@ -196,42 +191,35 @@ def test_sb_unit_conversion(cubeviz_helper):
uc_plg = cubeviz_helper.plugins['Unit Conversion']
uc_plg.open_in_tray()

# ensure that per solid angle cube defaults to Flux spectrum
assert uc_plg.flux_or_sb == 'Flux'
# flux choices is populated with flux units
assert uc_plg.flux_unit.choices

# to have access to display units
viewer_1d = cubeviz_helper.app.get_viewer(
cubeviz_helper._default_spectrum_viewer_reference_name)

uc_plg._obj.show_translator = True
uc_plg.flux_or_sb.selected = 'Surface Brightness'

# Surface Brightness conversion
uc_plg.flux_or_sb_unit = 'Jy / sr'
uc_plg.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'
uc_plg.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'
uc_plg.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.extract()

uc_plg._obj.show_translator = True
uc_plg._obj.flux_or_sb_selected = 'Flux'
uc_plg.flux_or_sb_unit = 'MJy'
uc_plg.flux_unit = 'MJy'
y_display_unit = u.Unit(viewer_1d.state.y_display_unit)

assert y_display_unit == u.MJy
Expand Down
Loading

0 comments on commit 143cd8d

Please sign in to comment.