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

implement unit conversion in specviz2d #3253

Open
wants to merge 29 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
7152a2d
first pass specviz2d implementation
gibsongreen Oct 1, 2024
274c917
resolve stash
gibsongreen Oct 18, 2024
5ba84a7
fix issues with stash and update get_data
gibsongreen Oct 18, 2024
f137213
second pass specviz2d uc implementation
gibsongreen Oct 21, 2024
640cd62
revert 2D viewer for uc
gibsongreen Oct 21, 2024
58516a0
don't allow conversion of spectral line y values
gibsongreen Oct 22, 2024
124d829
fix bug to ensure non-scale factor data converts on _handle_display_u…
gibsongreen Oct 24, 2024
a025530
fix get_data bug using native unit for non-scale factor data
gibsongreen Oct 24, 2024
35d7733
add line analysis uc test
gibsongreen Oct 24, 2024
aad7c6f
add test for solid angle equivalency list
gibsongreen Oct 24, 2024
acd3693
add MosvizProfileView test
gibsongreen Oct 24, 2024
72dbf9d
add change log
gibsongreen Oct 24, 2024
05ca476
reconcile test failures
gibsongreen Oct 24, 2024
e86342b
add API test
gibsongreen Oct 24, 2024
b0ed10b
remove global display unit change handler, syntax error in helpers
gibsongreen Oct 24, 2024
fdb6a5d
ensure continuum marks don't double translate, test coverage
gibsongreen Oct 24, 2024
b538d1e
move line analysis test
gibsongreen Oct 25, 2024
556b66f
remove files that weren't removed during rebase
gibsongreen Nov 29, 2024
0758920
fix styling
gibsongreen Nov 29, 2024
bb956cc
add missing import
gibsongreen Dec 11, 2024
e08cf1f
add spectral unit support, add eqvs to model fit, coords, marks
gibsongreen Dec 17, 2024
0a3a171
fix syntax error
gibsongreen Dec 17, 2024
c838bd5
2dviewer spectral conversion, first iteration of test coverage
gibsongreen Dec 18, 2024
c11a3c3
resolve test failures
gibsongreen Dec 19, 2024
62998b5
address spec density edge case, test coverage
gibsongreen Dec 19, 2024
f170224
address Kyle's review comments
gibsongreen Dec 26, 2024
b481be1
resolve test failures, update handle_display_unit for get_selected_sp…
gibsongreen Dec 30, 2024
320d8c8
ensure spectral axis only passed in correct cases
gibsongreen Jan 3, 2025
9e031c9
add uc support for spec extract, support for matched zoom and uc
gibsongreen Jan 23, 2025
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
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ Specviz
Specviz2d
^^^^^^^^^

- Implement the Unit Conversion plugin in Specviz2D. [#3253]

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

Expand Down
2 changes: 2 additions & 0 deletions jdaviz/configs/cubeviz/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ def get_data(self, data_label=None, spatial_subset=None, spectral_subset=None,
Spectral subset applied to data.
cls : `~specutils.Spectrum1D`, `~astropy.nddata.CCDData`, optional
The type that data will be returned as.
use_display_units : bool, optional
Specify whether the returned data is in native units or the current display units.

Returns
-------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -520,9 +520,18 @@ def test_spectral_extraction_with_correct_sum_units(cubeviz_helper,
cubeviz_helper.load_data(spectrum1d_cube_fluxunit_jy_per_steradian)
spec_extr_plugin = cubeviz_helper.plugins['Spectral Extraction']._obj
collapsed = spec_extr_plugin.extract()

assert '_pixel_scale_factor' in collapsed.meta

# Original units in Jy / sr
# After collapsing, sr is removed via the scale factor and the extracted spectrum is in Jy
expected_flux_values = (np.array([190., 590., 990., 1390., 1790.,
2190., 2590., 2990., 3390., 3790.]) *
collapsed.meta.get('_pixel_scale_factor'))

np.testing.assert_allclose(
collapsed.flux.value,
[190., 590., 990., 1390., 1790., 2190., 2590., 2990., 3390., 3790.]
expected_flux_values
)
assert collapsed.flux.unit == u.Jy
assert collapsed.uncertainty.unit == u.Jy
Expand Down
3 changes: 2 additions & 1 deletion jdaviz/configs/default/plugins/markers/markers.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,12 @@ def __init__(self, *args, **kwargs):
elif self.config == 'specviz':
headers = ['spectral_axis', 'spectral_axis:unit',
'index', 'value', 'value:unit']

elif self.config == 'specviz2d':
# TODO: add "index" if/when specviz2d supports plotting spectral_axis
headers = ['spectral_axis', 'spectral_axis:unit',
'pixel_x', 'pixel_y', 'value', 'value:unit', 'viewer']

elif self.config == 'mosviz':
headers = ['spectral_axis', 'spectral_axis:unit',
'pixel_x', 'pixel_y', 'world_ra', 'world_dec', 'index',
Expand Down Expand Up @@ -223,7 +225,6 @@ def _on_is_active_changed(self, *args):
def _on_viewer_key_event(self, viewer, data):
if data['event'] == 'keydown' and data['key'] == 'm':
row_info = self.coords_info.as_dict()

if 'viewer' in self.table.headers_avail:
row_info['viewer'] = viewer.reference if viewer.reference is not None else viewer.reference_id # noqa

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import numpy as np
from numpy.testing import assert_allclose
import pytest
from specutils import Spectrum1D

from jdaviz.core.custom_units_and_equivs import PIX2, SPEC_PHOTON_FLUX_DENSITY_UNITS
from jdaviz.core.marks import MarkersMark
Expand Down Expand Up @@ -242,6 +243,61 @@ def test_markers_cubeviz_flux_unit_conversion(cubeviz_helper,
assert last_row['value:unit'] == new_cube_unit_str


def test_markers_specviz2d_unit_conversion(specviz2d_helper, spectrum2d):
data = np.zeros((5, 10))
data[3] = np.arange(10)
spectrum2d = Spectrum1D(flux=data*u.MJy, spectral_axis=data[3]*u.AA)
specviz2d_helper.load_data(spectrum2d)

uc = specviz2d_helper.plugins['Unit Conversion']
uc.open_in_tray()
mp = specviz2d_helper.plugins['Markers']
mp.keep_active = True

label_mouseover = specviz2d_helper.app.session.application._tools["g-coords-info"]
viewer2d = specviz2d_helper.app.get_viewer("spectrum-2d-viewer")
label_mouseover._viewer_mouse_event(viewer2d, {"event": "mousemove",
"domain": {"x": 6, "y": 3}})
assert label_mouseover.as_text() == ('Pixel x=06.0 y=03.0 Value +6.00000e+00 MJy',
'Wave 6.00000e+00 Angstrom',
'')
mp._obj._on_viewer_key_event(viewer2d, {'event': 'keydown',
'key': 'm'})

# make sure last marker added to table reflects new unit selection
last_row = mp.export_table()[-1]
assert last_row['value:unit'] == uc.flux_unit
assert last_row['spectral_axis:unit'] == uc.spectral_unit

# ensure marks work with flux conversion where spectral axis is required and
# spectral axis conversion
uc.flux_unit = 'erg / (Angstrom s cm2)'
uc.spectral_unit = 'Ry'
label_mouseover._viewer_mouse_event(viewer2d, {"event": "mousemove",
"domain": {"x": 4, "y": 3}})
assert label_mouseover.as_text() == ('Pixel x=04.0 y=03.0 Value +7.49481e+00 erg / (Angstrom s cm2)', # noqa
'Wave 2.27817e+02 Ry',
'')
mp._obj._on_viewer_key_event(viewer2d, {'event': 'keydown',
'key': 'm'})

# make sure last marker added to table reflects new unit selection
last_row = mp.export_table()[-1]
assert last_row['value:unit'] == uc.flux_unit
assert last_row['spectral_axis:unit'] == uc.spectral_unit

second_marker_flux_unit = uc.flux_unit.selected
second_marker_spectral_unit = uc.spectral_unit.selected

# test edge case two non-native spectral axis required conversions
uc.flux_unit = 'ph / (Angstrom s cm2)'
uc.spectral_unit = 'eV'

# make sure table flux and spectral unit doesn't update
assert last_row['value:unit'] == second_marker_flux_unit
assert last_row['spectral_axis:unit'] == second_marker_spectral_unit


class TestImvizMultiLayer(BaseImviz_WCS_NoWCS):
def test_markers_layer_cycle(self):
label_mouseover = self.imviz.app.session.application._tools['g-coords-info']
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1113,8 +1113,11 @@ def _fit_model_to_spectrum(self, add_data):
return
models_to_fit = self._reinitialize_with_fixed()

masked_spectrum = self._apply_subset_masks(self.dataset.selected_spectrum,
spec = self.dataset.get_selected_spectrum(use_display_units=True)

masked_spectrum = self._apply_subset_masks(spec,
self.spectral_subset)

try:
fitted_model, fitted_spectrum = fit_model_to_spectrum(
masked_spectrum,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,8 @@ def test_results_table(specviz_helper, spectrum1d):
mf = specviz_helper.plugins['Model Fitting']
mf.create_model_component('Linear1D')

uc = specviz_helper.plugins['Unit Conversion']

mf.add_results.label = 'linear model'
with warnings.catch_warnings():
warnings.filterwarnings('ignore', message='Model is linear in parameters.*')
Expand All @@ -325,6 +327,9 @@ def test_results_table(specviz_helper, spectrum1d):
'L:intercept_0', 'L:intercept_0:unit',
'L:intercept_0:fixed', 'L:intercept_0:std']

# verify units in table match the current display unit
assert mf_table['L:intercept_0:unit'][-1] == uc.flux_unit

mf.create_model_component('Gaussian1D')
mf.add_results.label = 'composite model'
with warnings.catch_warnings():
Expand All @@ -346,6 +351,19 @@ def test_results_table(specviz_helper, spectrum1d):
'G:stddev_1', 'G:stddev_1:unit',
'G:stddev_1:fixed', 'G:stddev_1:std']

mf.remove_model_component('G')
assert len(mf_table) == 2

# verify Spectrum1D model fitting plugin and table can handle spectral density conversions
uc.flux_unit = 'erg / (Angstrom s cm2)'
mf.reestimate_model_parameters()

with warnings.catch_warnings():
warnings.filterwarnings('ignore', message='Model is linear in parameters.*')
mf.calculate_fit(add_data=True)

assert mf_table['L:intercept_0:unit'][-1] == uc.flux_unit


def test_equation_validation(specviz_helper, spectrum1d):
data_label = 'test'
Expand Down
21 changes: 20 additions & 1 deletion jdaviz/configs/imviz/plugins/coords_info/coords_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from jdaviz.core.unit_conversion_utils import (all_flux_unit_conversion_equivs,
check_if_unit_is_per_solid_angle,
flux_conversion_general)
from jdaviz.utils import flux_conversion

__all__ = ['CoordsInfo']

Expand Down Expand Up @@ -433,6 +434,12 @@ def _image_viewer_update(self, viewer, x, y):
# use WCS to expose the wavelength for a 2d spectrum shown in pixel space
try:
wave, pixel = image.coords.pixel_to_world(x, y)
if wave is not None:
equivalencies = all_flux_unit_conversion_equivs(cube_wave=wave)
wave = wave.to(self.app._get_display_unit('spectral'),
equivalencies=equivalencies)
self._dict['spectral_axis'] = wave.value
self._dict['spectral_axis:unit'] = wave.unit.to_string()
Comment on lines +441 to +442
Copy link
Member

Choose a reason for hiding this comment

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

where were these handled before (I don't see them in the else 🤔)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

X-axis conversions were not previously handled in Specviz2d, so they were previously always using the native units and value when mousing over

except Exception: # WCS might not be valid # pragma: no cover
coords_status = False
else:
Expand Down Expand Up @@ -483,12 +490,21 @@ def _image_viewer_update(self, viewer, x, y):

if isinstance(viewer, (ImvizImageView, MosvizImageView, MosvizProfile2DView)):
value = image.get_data(attribute)[int(round(y)), int(round(x))]

if associated_dq_layers is not None:
associated_dq_layer = associated_dq_layers[0]
dq_attribute = associated_dq_layer.state.attribute
dq_data = associated_dq_layer.layer.get_data(dq_attribute)
dq_value = dq_data[int(round(y)), int(round(x))]

unit = u.Unit(image.get_component(attribute).units)
if (isinstance(viewer, MosvizProfile2DView) and unit != ''
and unit != self.app._get_display_unit(attribute)):
equivalencies = all_flux_unit_conversion_equivs(cube_wave=wave)
value = flux_conversion(value, unit, self.app._get_display_unit(attribute),
eqv=equivalencies)
unit = self.app._get_display_unit(attribute)

elif isinstance(viewer, (CubevizImageView, RampvizImageView)):
arr = image.get_component(attribute).data
unit = u.Unit(image.get_component(attribute).units)
Expand Down Expand Up @@ -543,7 +559,10 @@ def _image_viewer_update(self, viewer, x, y):
else:
dq_text = ''
self.row1b_text = f'{value:+10.5e} {unit}{dq_text}'
self._dict['value'] = float(value)
if not isinstance(value, (float, np.floating)):
self._dict['value'] = float(value)
else:
self._dict['value'] = value
self._dict['value:unit'] = str(unit)
self._dict['value:unreliable'] = unreliable_pixel
else:
Expand Down
34 changes: 34 additions & 0 deletions jdaviz/configs/mosviz/plugins/tools.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os

from glue.config import viewer_tool
from astropy import units as u

from jdaviz.configs.mosviz.plugins.viewers import MosvizProfileView, MosvizProfile2DView
from jdaviz.core.tools import _MatchedZoomMixin, HomeZoom, BoxZoom, XRangeZoom, PanZoom, PanZoomX
Expand All @@ -18,12 +19,45 @@ def _is_matched_viewer(self, viewer):
return isinstance(viewer, (MosvizProfile2DView, MosvizProfileView))

def _map_limits(self, from_viewer, to_viewer, limits={}):
components = self.viewer.state.data_collection[0]._components
# Determine cid for spectral axis
cid = None
for key in components.keys():
if 'Wavelength' in str(key):
cid = str(key)
break
elif 'Wave' in str(key):
cid = str(key)
break

if cid is None:
raise ValueError("Neither 'Wavelength' nor 'Wave' component'" +
"found in the data collection.")

native_unit = u.Unit(self.viewer.state.data_collection[0].get_component(cid).units)
current_display_unit = u.Unit(self.viewer.jdaviz_helper.app._get_display_unit('spectral'))

if isinstance(from_viewer, MosvizProfileView) and isinstance(to_viewer, MosvizProfile2DView): # noqa
if native_unit != current_display_unit:
limits['x_min'] = (limits['x_min'] * native_unit).to_value(
current_display_unit, equivalencies=u.spectral()
)

limits['x_max'] = (limits['x_max'] * native_unit).to_value(
current_display_unit, equivalencies=u.spectral()
)
limits['x_min'], limits['x_max'] = to_viewer.world_to_pixel_limits((limits['x_min'],
limits['x_max']))
elif isinstance(from_viewer, MosvizProfile2DView) and isinstance(to_viewer, MosvizProfileView): # noqa
limits['x_min'], limits['x_max'] = from_viewer.pixel_to_world_limits((limits['x_min'],
limits['x_max']))
if native_unit != current_display_unit:
limits['x_min'] = (limits['x_min'] * native_unit).to_value(
current_display_unit, equivalencies=u.spectral()
)
limits['x_max'] = (limits['x_max'] * native_unit).to_value(
current_display_unit, equivalencies=u.spectral()
)
return limits


Expand Down
5 changes: 4 additions & 1 deletion jdaviz/configs/mosviz/tests/test_data_loading.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,8 +222,11 @@ def test_load_single_image_multi_spec(mosviz_helper, mos_image, spectrum1d, mos_

label_mouseover._viewer_mouse_event(spec2d_viewer,
{'event': 'mousemove', 'domain': {'x': 10, 'y': 100}})

# Note: spectra2d Wave loaded in meters, but we respect one spectral unit, so the meters in
# converted to Angstrom (the spectra1d spectral unit).
Comment on lines +226 to +227
Copy link
Member

Choose a reason for hiding this comment

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

Can you explain this a little more? Is it that a previous data entry was in angstroms or just that that is the default spectral display unit?

Suggested change
# Note: spectra2d Wave loaded in meters, but we respect one spectral unit, so the meters in
# converted to Angstrom (the spectra1d spectral unit).
# Note: spectra2d Wave loaded in meters, but we respect one spectral unit, so the meters is
# converted to Angstrom (the spectra1d spectral unit).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So the loaded 1D and 2D spectra have different native spectral units, with one in meters and one in angstroms and app-wide we're respecting one choice for spectral unit.

assert label_mouseover.as_text() == ('Pixel x=00010.0 y=00100.0 Value +8.12986e-01',
'Wave 1.10000e-05 m', '')
'Wave 1.10000e+05 Angstrom', '')
assert label_mouseover.icon == 'c'

# need to trigger a mouseleave or mouseover to reset the traitlets
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from numpy.testing import assert_allclose
from regions import RectanglePixelRegion, PixCoord
from specutils import Spectrum1D, SpectralRegion
from glue.core.roi import XRangeROI

from jdaviz.configs.specviz.plugins.line_analysis.line_analysis import _coerce_unit
from jdaviz.core.custom_units_and_equivs import PIX2
Expand Down Expand Up @@ -91,7 +92,7 @@ def test_cubeviz_units(cubeviz_helper, spectrum1d_cube_custom_fluxunit,
is in flux/pix2 and flux/sr, and that the results remain consistant
between translations of the spectral y axis flux<>surface brightness.
"""
cube = spectrum1d_cube_custom_fluxunit(fluxunit=u.Jy / sq_angle_unit,
cube = spectrum1d_cube_custom_fluxunit(fluxunit=u.MJy / sq_angle_unit,
shape=(25, 25, 4), with_uncerts=True)
cubeviz_helper.load_data(cube, data_label="Test Cube")

Expand All @@ -107,9 +108,46 @@ def test_cubeviz_units(cubeviz_helper, spectrum1d_cube_custom_fluxunit,
results = plugin.results
assert results[0]['unit'] == 'W / m2'

viewer = cubeviz_helper.app.get_viewer('spectrum-viewer')
viewer.apply_roi(XRangeROI(4.63e-7, 4.64e-7))

la = cubeviz_helper.plugins['Line Analysis']
la.keep_active = True
la.spectral_subset.selected = 'Subset 1'

marks_before = [la._obj.continuum_marks['left'].y,
la._obj.continuum_marks['right'].y]

# change flux unit and make sure result stays the same after conversion
uc.flux_unit.selected = 'Jy'

marks_after = [la._obj.continuum_marks['left'].y,
la._obj.continuum_marks['right'].y]

# ensure continuum marks update when spectral_y is changed by
# multiply converted continuum marks by expected scale factor (MJy -> Jy)
scaling_factor = 1e6
assert_allclose([mark * scaling_factor for mark in marks_before], marks_after, rtol=1e-5)

# reset to test again after spectral_y_type is changed
marks_before = marks_after

# now change to surface brightness
uc.spectral_y_type = 'Surface Brightness'

if sq_angle_unit == PIX2:
# translation does not alter spectral_y values in viewer
scaling_factor = 1
else:
scaling_factor = cube.meta.get('PIXAR_SR')

marks_after = [la._obj.continuum_marks['left'].y,
la._obj.continuum_marks['right'].y]

# ensure continuum marks update when spectral_y_type is changed
# multiply converted continuum marks by expected pixel scale factor
assert_allclose([mark / scaling_factor for mark in marks_before], marks_after, rtol=1e-5)

results = plugin.results
line_flux_before_unit_conversion = results[0]
# convert back and forth between unit<>str for string format consistency
Expand Down
Loading
Loading