Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Glue unit conversion in Specviz #2048

Merged
merged 19 commits into from
Apr 4, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ New Features
Cubeviz
^^^^^^^

* Re-enabled unit conversion support. [#2048]

Imviz
^^^^^

Expand All @@ -18,9 +20,13 @@ Mosviz
Specviz
^^^^^^^

* Re-enabled unit conversion support. [#2048]

Comment on lines +21 to +22
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we might want to replace this with the eventual bulk-PR into main, but it can serve as a placeholder for now

Specviz2d
^^^^^^^^^

* Re-enabled unit conversion support. [#2048]

API Changes
-----------

Expand Down
15 changes: 3 additions & 12 deletions docs/specviz/plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -118,15 +118,8 @@ To export the table into the notebook via the API, call
Unit Conversion
===============

.. note::

This plugin is temporarily disabled. Effort to improve it is being
tracked at https://github.com/spacetelescope/jdaviz/issues/1972 .

The spectral flux density and spectral axis units can be converted
using the Unit Conversion plugin. The Spectrum1D object to be
converted is the currently selected spectrum in the spectrum viewer :guilabel:`Data`
icon in the viewer toolbar.
using the Unit Conversion plugin.

Select the frequency, wavelength, or energy unit in the
:guilabel:`New Spectral Axis Unit` pulldown
Expand All @@ -136,10 +129,8 @@ Select the flux density unit in the :guilabel:`New Flux Unit` pulldown
(e.g., Jansky, W/(Hz/m2), ph/(Angstrom cm2 s)).

The :guilabel:`Apply` button will convert the flux density and/or
kecnry marked this conversation as resolved.
Show resolved Hide resolved
spectral axis units and create a new Spectrum1D object that
is automatically switched to in the spectrum viewer.
The name of the new Spectrum1D object is "_units_copy_" plus
the flux and spectral units of the spectrum.
spectral axis units being displayed in the spectrum viewer, if applicable.
This does not affect the underlying data.

.. _line-lists:

Expand Down
70 changes: 47 additions & 23 deletions jdaviz/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
from ipywidgets import widget_serialization
import ipyvue

from astropy import units as u
from astropy.nddata import CCDData, NDData
from astropy.io import fits
from astropy import units as u
from astropy.coordinates import Angle
from regions import PixCoord, CirclePixelRegion, RectanglePixelRegion, EllipsePixelRegion

Expand All @@ -27,7 +27,7 @@
from glue.config import colormaps, data_translator
from glue.config import settings as glue_settings
from glue.core import BaseData, HubListener, Data, DataCollection
from glue.core.link_helpers import LinkSame
from glue.core.link_helpers import LinkSame, LinkSameWithUnits
from glue.plugins.wcs_autolinking.wcs_autolinking import WCSLink, IncompatibleWCS
from glue.core.message import (DataCollectionAddMessage,
DataCollectionDeleteMessage,
Expand All @@ -38,6 +38,7 @@
from glue.core.subset import (Subset, RangeSubsetState, RoiSubsetState,
CompositeSubsetState, InvertState)
from glue.core.roi import CircularROI, EllipticalROI, RectangularROI
from glue.core.units import unit_converter
from glue_astronomy.spectral_coordinates import SpectralCoordinates
from glue_jupyter.app import JupyterApplication
from glue_jupyter.common.toolbar_vuetify import read_icon
Expand Down Expand Up @@ -68,10 +69,49 @@
mask=['mask', 'dq'])


@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',
'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 Hz)', 'erg / (Hz s cm2)',
'ph / (s cm2 Angstrom)', 'ph / (Angstrom s cm2)',
'ph / (s cm2 Hz)', 'ph / (Hz s cm2)'
Comment on lines +80 to +87
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need the duplicate order any more now that setting through the plugin maps to a valid glue-unit (in unit_conversion._valid_glue_display_unit). @pllim - any opinion on which order we should adopt as the default? And would we benefit from merging this logic with the logic in validunits.py?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm... but what if someone tries to set the unit via API?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the plugin API handles the mapping automatically allowing any valid representation, if they were to use the glue-API, then they would need to match the exact string (see the updated concept notebook for an example).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, probably ok then since a majority of users won't know how to use the glue API anyway.

])
else: # spectral axis
list_of_units = map(str, u.Unit(units).find_equivalent_units(
include_prefix_units=True, equivalencies=u.spectral()))
return list_of_units

def to_unit(self, data, cid, values, original_units, target_units):
# Given a glue data object (data), a component ID (cid), the values
# to convert, and the original and target units of the values, this method
# should return the converted values. Note that original_units
# gives the units of the values array, which might not be the same
# as the original native units of the component in the data.
if cid.label == "flux":
spec = data.get_object(cls=Spectrum1D)
eqv = u.spectral_density(spec.spectral_axis)
else: # spectral axis
eqv = u.spectral()
return (values * u.Unit(original_units)).to_value(u.Unit(target_units), equivalencies=eqv)


# Set default opacity for data layers to 1 instead of 0.8 in
# some glue-core versions
glue_settings.DATA_ALPHA = 1

# Enable spectrum unit conversion.
glue_settings.UNIT_CONVERTER = 'custom-jdaviz'

custom_components = {'j-tooltip': 'components/tooltip.vue',
'j-external-link': 'components/external_link.vue',
'j-docs-link': 'components/docs_link.vue',
Expand Down Expand Up @@ -446,7 +486,7 @@ def _link_new_data(self, reference_data=None, data_to_be_linked=None):
if isinstance(linked_data.coords, SpectralCoordinates):
wc_old = ref_data.world_component_ids[-1]
wc_new = linked_data.world_component_ids[0]
self.data_collection.add_link(LinkSame(wc_old, wc_new))
self.data_collection.add_link(LinkSameWithUnits(wc_old, wc_new))
return

try:
Expand Down Expand Up @@ -492,8 +532,8 @@ def _link_new_data(self, reference_data=None, data_to_be_linked=None):
else:
continue

links.append(LinkSame(ref_data.pixel_component_ids[ref_index],
linked_data.pixel_component_ids[linked_index]))
links.append(LinkSameWithUnits(ref_data.pixel_component_ids[ref_index],
linked_data.pixel_component_ids[linked_index]))

dc.add_link(links)

Expand Down Expand Up @@ -1204,19 +1244,6 @@ def add_data_to_viewer(self, viewer_reference, data_label,

self.set_data_visibility(viewer_item, data_label, visible=visible, replace=clear_other_data)

def _set_plot_axes_labels(self, viewer_id):
"""
Sets the plot axes labels to be the units of the data to be loaded.

Parameters
----------
viewer_id : str
The UUID associated with the desired viewer item.
"""
viewer = self._viewer_by_id(viewer_id)

viewer.set_plot_axes()

def remove_data_from_viewer(self, viewer_reference, data_label):
"""
Removes a data set from the specified viewer.
Expand Down Expand Up @@ -1589,11 +1616,8 @@ def set_data_visibility(self, viewer_reference, data_label, visible=True, replac
# active data.
viewer_data_labels = [layer.layer.label for layer in viewer.layers]
if len(viewer_data_labels) > 0 and getattr(self._jdaviz_helper, '_in_batch_load', 0) == 0:
active_data = self.data_collection[viewer_data_labels[0]]
if (hasattr(active_data, "_preferred_translation")
and active_data._preferred_translation is not None):
self._set_plot_axes_labels(viewer_id)

# This "if" is nested on purpose to make parent "if" available
# for other configs in the future, as needed.
if self.config == 'imviz':
viewer.on_limits_change() # Trigger compass redraw

Expand Down
7 changes: 6 additions & 1 deletion jdaviz/configs/default/plugins/line_lists/line_lists.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,8 @@ def _on_viewer_data_changed(self, msg=None):
self._on_spectrum_viewer_limits_changed() # will also trigger _auto_slider_step

# set the choices (and default) for the units for new custom lines
self.custom_unit_choices = create_spectral_equivalencies_list(viewer_data)
self.custom_unit_choices = create_spectral_equivalencies_list(
viewer_data.spectral_axis.unit)
self.custom_unit = str(viewer_data.spectral_axis.unit)

def _parse_redshift_msg(self, msg):
Expand Down Expand Up @@ -410,6 +411,10 @@ def vue_slider_reset(self, event):

def _on_spectrum_viewer_limits_changed(self, event=None):
sv = self.app.get_viewer(self._default_spectrum_viewer_reference_name)

if sv.state.x_min is None or sv.state.x_max is None:
return

self.spectrum_viewer_min = float(sv.state.x_min)
self.spectrum_viewer_max = float(sv.state.x_max)

Expand Down
2 changes: 0 additions & 2 deletions jdaviz/configs/default/plugins/viewers.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,6 @@ def _get_layer_info(layer):
return "mdi-chart-bell-curve", ""
return "", suffix

return '', ''

visible_layers = {}
for layer in self.state.layers[::-1]:
if layer.visible:
Expand Down
55 changes: 31 additions & 24 deletions jdaviz/configs/imviz/plugins/coords_info/coords_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,13 +389,10 @@ def _image_viewer_update(self, viewer, x, y):

def _spectrum_viewer_update(self, viewer, x, y):
def _cursor_fallback():
statistic = getattr(viewer.state, 'function', None)
cache_key = (viewer.state.layers[0].layer.label, statistic)
sp = self.app._get_object_cache.get(cache_key, viewer.data()[0])
self._dict['axes_x'] = x
self._dict['axes_x:unit'] = sp.spectral_axis.unit.to_string()
self._dict['axes_x:unit'] = viewer.state.x_display_unit
self._dict['axes_y'] = y
self._dict['axes_y:unit'] = sp.flux.unit.to_string()
self._dict['axes_y:unit'] = viewer.state.y_display_unit
self._dict['data_label'] = ''

def _copy_axes_to_spectral():
Expand All @@ -418,8 +415,6 @@ def _copy_axes_to_spectral():
self.row3_text = ''
self.icon = 'mdi-cursor-default'
self.marks[viewer._reference_id].visible = False
# get the units from the first layer
# TODO: replace with display units once implemented
_cursor_fallback()
_copy_axes_to_spectral()
return
Expand Down Expand Up @@ -462,17 +457,22 @@ def _copy_axes_to_spectral():
subset_to_apply=subset_label)
self.app._get_object_cache[cache_key] = sp

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

# Out of range in spectral axis.
if (self.dataset.selected != lyr.layer.label and
(x < sp.spectral_axis.value.min() or x > sp.spectral_axis.value.max())):
(x < disp_wave.min() or x > disp_wave.max())):
continue

cur_i = np.argmin(abs(sp.spectral_axis.value - x))
cur_wave = sp.spectral_axis[cur_i]
cur_flux = sp.flux[cur_i]
cur_i = np.argmin(abs(disp_wave - x))
cur_wave = disp_wave[cur_i]
cur_flux = disp_flux[cur_i]

dx = cur_wave.value - x
dy = cur_flux.value - y
dx = cur_wave - x
dy = cur_flux - y
cur_distance = math.sqrt(dx * dx + dy * dy)
if (closest_distance is None) or (cur_distance < closest_distance):
closest_distance = cur_distance
Expand All @@ -497,27 +497,34 @@ def _copy_axes_to_spectral():
return

self.row2_title = 'Wave'
self.row2_text = f'{closest_wave.value:10.5e} {closest_wave.unit.to_string()}'
self._dict['axes_x'] = closest_wave.value
self._dict['axes_x:unit'] = closest_wave.unit.to_string()
if closest_wave.unit != u.pix:
self.row2_text = f'{closest_wave:10.5e} {viewer.state.x_display_unit}'
self._dict['axes_x'] = closest_wave
self._dict['axes_x:unit'] = viewer.state.x_display_unit
if viewer.state.x_display_unit != u.pix:
self.row2_text += f' ({int(closest_i)} pix)'
if self.app.config == 'cubeviz':
# float to be compatible with nan
self._dict['slice'] = float(closest_i)
self._dict['spectral_axis'] = closest_wave.value
self._dict['spectral_axis:unit'] = closest_wave.unit.to_string()
self._dict['spectral_axis'] = closest_wave
self._dict['spectral_axis:unit'] = viewer.state.x_display_unit
else:
# float to be compatible with nan
self._dict['index'] = float(closest_i)

if viewer.state.y_display_unit is None:
flux_unit = ""
else:
flux_unit = viewer.state.y_display_unit
self.row3_title = 'Flux'
self.row3_text = f'{closest_flux.value:10.5e} {closest_flux.unit.to_string()}'
self._dict['axes_y'] = closest_flux.value
self._dict['axes_y:unit'] = closest_flux.unit.to_string()
self.row3_text = f'{closest_flux:10.5e} {flux_unit}'
self._dict['axes_y'] = closest_flux
self._dict['axes_y:unit'] = viewer.state.y_display_unit

self.icon = closest_icon
if closest_icon is not None:
self.icon = closest_icon
else:
self.icon = ""

self.marks[viewer._reference_id].update_xy([closest_wave.value], [closest_flux.value]) # noqa
self.marks[viewer._reference_id].update_xy([closest_wave], [closest_flux])
self.marks[viewer._reference_id].visible = True
_copy_axes_to_spectral()
4 changes: 2 additions & 2 deletions jdaviz/configs/mosviz/plugins/parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from astropy.io.registry import IORegistryError
from astropy.wcs import WCS
from glue.core.data import Data
from glue.core.link_helpers import LinkSame
from glue.core.link_helpers import LinkSameWithUnits
from specutils import Spectrum1D, SpectrumList, SpectrumCollection
from specutils.io.default_loaders.jwst_reader import identify_jwst_s2d_multi_fits

Expand Down Expand Up @@ -141,7 +141,7 @@ def link_data_in_table(app, data_obj=None):
wc_spec_1d = app.session.data_collection[spec_1d].world_component_ids
wc_spec_2d = app.session.data_collection[spec_2d].world_component_ids

wc_spec_ids.append(LinkSame(wc_spec_1d[0], wc_spec_2d[1]))
wc_spec_ids.append(LinkSameWithUnits(wc_spec_1d[0], wc_spec_2d[1]))

app.session.data_collection.add_link(wc_spec_ids)

Expand Down
12 changes: 0 additions & 12 deletions jdaviz/configs/specviz/plugins/parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,6 @@ def specviz_spectrum1d_parser(app, data, data_label=None, format=None, show_in_v
raise ValueError(f"Length of data labels list ({len(data_label)}) is different"
f" than length of list of data ({len(data)})")

# If there's already visible data in the viewer, convert units if needed
current_unit = None
if spectrum_viewer_reference_name in app.get_viewer_reference_names():
sv = app.get_viewer(spectrum_viewer_reference_name)
for layer_state in sv.state.layers:
if layer_state.visible:
current_unit = sv.state.x_display_unit

with app.data_collection.delay_link_manager_update():
# these are used to build a combined spectrum with all
# input spectra included (taken from https://github.com/spacetelescope/
Expand All @@ -98,10 +90,6 @@ def specviz_spectrum1d_parser(app, data, data_label=None, format=None, show_in_v
wave_units = spec.spectral_axis.unit
flux_units = spec.flux.unit

if current_unit is not None and spec.spectral_axis.unit != current_unit:
spec = Spectrum1D(flux=spec.flux,
spectral_axis=spec.spectral_axis.to(current_unit))

# Make metadata layout conform with other viz.
spec.meta = standardize_metadata(spec.meta)

Expand Down
Loading