From 8088366870a5e5746ec7054c856155ba022832e2 Mon Sep 17 00:00:00 2001 From: Ricky O'Steen Date: Wed, 31 Jul 2024 13:17:18 -0400 Subject: [PATCH 001/127] Remove unneeded changelog --- CHANGES.rst | 3 --- 1 file changed, 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 3f589137d0..b20298b248 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -208,9 +208,6 @@ Other Changes and Additions Bug Fixes --------- -- Fixed the data menu in Model Fitting not being populated if Cube Fit was toggled - while a spectral subset was selected. [#3123] - Cubeviz ^^^^^^^ From 50023c8dfaef6cec1f3d9199c11b8b802c0eadb5 Mon Sep 17 00:00:00 2001 From: Gilbert Green <42986583+gibsongreen@users.noreply.github.com> Date: Thu, 1 Aug 2024 17:07:18 -0400 Subject: [PATCH 002/127] Surface brightness -> solid angle conversion updates (#3111) * first pass at sb -> angle drop down changes * update conversion logic, .pix support for counts * update tests, cut out sb code * add comments, handle limits and choices * add change log * handle load_data case where PIXAR_SR not in metadata, reset limits adjustment * u.erg/u.s/u.cm**2/u.Angstrom in untranslatable units, switch from SB to flux for test * get_object -> get_component, 2 returns to 1 * broadcast sb_unit, fix zoom limits, ensure self.sb_unit sets * reconcile test failures * remove flux_or_sb extra conditions, small comment tweak * sb_unit -> sb_unit_selected for app._get_display_unit(sb) compatibility --- CHANGES.rst | 2 +- .../plugins/moment_maps/moment_maps.py | 4 +- .../moment_maps/tests/test_moment_maps.py | 2 +- .../line_analysis/tests/test_lineflux.py | 6 +- .../tests/test_unit_conversion.py | 21 +-- .../unit_conversion/unit_conversion.py | 163 ++++++++---------- .../unit_conversion/unit_conversion.vue | 23 ++- jdaviz/core/helpers.py | 19 +- jdaviz/core/validunits.py | 52 +++--- 9 files changed, 137 insertions(+), 155 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index b20298b248..b3276caa95 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, #3113] + unit conversion in Cubeviz and Specviz. [#2781, #2940, #3088, #3111, #3113] - Plugin tray is now open by default. [#2892] diff --git a/jdaviz/configs/cubeviz/plugins/moment_maps/moment_maps.py b/jdaviz/configs/cubeviz/plugins/moment_maps/moment_maps.py index b0089f99d6..2c71128fd9 100644 --- a/jdaviz/configs/cubeviz/plugins/moment_maps/moment_maps.py +++ b/jdaviz/configs/cubeviz/plugins/moment_maps/moment_maps.py @@ -182,6 +182,8 @@ def _set_data_units(self, event={}): "Velocity": "km/s", "Velocity^N": f"km{self.n_moment}/s{self.n_moment}"} + sb_or_flux_label = None + if self.dataset_selected != "": # Spectral axis is first in this list data = self.app.data_collection[self.dataset_selected] @@ -217,7 +219,7 @@ def _set_data_units(self, event={}): # Update units in selection item dictionary for item in self.output_unit_items: item["unit_str"] = unit_dict[item["label"]] - if item["label"] in ["Flux", "Surface Brightness"]: + if item["label"] in ["Flux", "Surface Brightness"] and sb_or_flux_label: # change unit label to reflect if unit is flux or SB item["label"] = sb_or_flux_label diff --git a/jdaviz/configs/cubeviz/plugins/moment_maps/tests/test_moment_maps.py b/jdaviz/configs/cubeviz/plugins/moment_maps/tests/test_moment_maps.py index c2efc22c4b..81727d67b0 100644 --- a/jdaviz/configs/cubeviz/plugins/moment_maps/tests/test_moment_maps.py +++ b/jdaviz/configs/cubeviz/plugins/moment_maps/tests/test_moment_maps.py @@ -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.sb_unit = 'Jy / sr' + uc.flux_unit = 'Jy' # and make sure this change is propogated output_unit_moment_0 = mm.output_unit_items[0] diff --git a/jdaviz/configs/specviz/plugins/line_analysis/tests/test_lineflux.py b/jdaviz/configs/specviz/plugins/line_analysis/tests/test_lineflux.py index cc79d5e74d..44fe58070a 100644 --- a/jdaviz/configs/specviz/plugins/line_analysis/tests/test_lineflux.py +++ b/jdaviz/configs/specviz/plugins/line_analysis/tests/test_lineflux.py @@ -115,10 +115,12 @@ def test_unit_gaussian_mixed_units_per_steradian(specviz_helper): ''' # unit-flux gaussian in wavelength space, mixed units, per steradian lam_a = np.arange(1, 2, 0.001)*u.Angstrom - flx_wave = _gauss_with_unity_area(lam_a.value, mn, sig)*1E3*u.erg/u.s/u.cm**2/u.Angstrom/u.sr + # test changed from Surface Brightness to Flux, + # u.erg/u.s/u.cm**2/u.Angstrom/u.sr in untranslatable units (check unit_conversion.py) + flx_wave = _gauss_with_unity_area(lam_a.value, mn, sig)*1E3*u.erg/u.s/u.cm**2/u.Angstrom fl_wave = Spectrum1D(spectral_axis=lam_a, flux=flx_wave) specviz_helper.load_data(fl_wave) lineflux_result = _calculate_line_flux(specviz_helper) assert_quantity_allclose(float(lineflux_result['result']) * u.Unit(lineflux_result['unit']), - 1*u.Unit('W/(m2sr)')) + 1*u.Unit('W/(m2)')) 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 6ab2bd439d..5dd70ad6ff 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 @@ -88,23 +88,18 @@ def test_conv_wave_flux(specviz_helper, spectrum1d, uncert): 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') + plg = specviz_helper.plugins["Unit Conversion"] 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 + # make sure we don't expose translations in Specviz assert hasattr(plg, 'flux_unit') - assert not hasattr(plg, 'sb_unit') + assert hasattr(plg, 'angle_unit') assert not hasattr(plg, 'flux_or_sb') @@ -198,7 +193,7 @@ def test_sb_unit_conversion(cubeviz_helper): uc_plg.flux_or_sb.selected = 'Surface Brightness' # Surface Brightness conversion - uc_plg.sb_unit = 'Jy / sr' + uc_plg.flux_unit = 'Jy' y_display_unit = u.Unit(viewer_1d.state.y_display_unit) assert y_display_unit == u.Jy / u.sr label_mouseover = cubeviz_helper.app.session.application._tools["g-coords-info"] @@ -214,7 +209,7 @@ def test_sb_unit_conversion(cubeviz_helper): "204.9987654313 27.0008999946 (deg)") # Try a second conversion - uc_plg.sb_unit = 'W / Hz sr m2' + uc_plg.flux_unit = 'W / Hz m2' y_display_unit = u.Unit(viewer_1d.state.y_display_unit) assert y_display_unit == u.Unit("W / (Hz sr m2)") @@ -230,7 +225,7 @@ def test_sb_unit_conversion(cubeviz_helper): # 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.sb_unit = 'MJy / sr' + uc_plg.flux_unit = 'MJy' y_display_unit = u.Unit(viewer_1d.state.y_display_unit) label_mouseover._viewer_mouse_event( flux_viewer, {"event": "mousemove", "domain": {"x": 10, "y": 8}} @@ -241,10 +236,10 @@ def test_sb_unit_conversion(cubeviz_helper): "204.9987654313 27.0008999946 (deg)") uc_plg._obj.flux_or_sb_selected = 'Flux' - uc_plg.flux_unit = 'MJy' + uc_plg.flux_unit = 'Jy' y_display_unit = u.Unit(viewer_1d.state.y_display_unit) - assert y_display_unit == u.MJy + assert y_display_unit == u.Jy la = cubeviz_helper.plugins['Line Analysis']._obj assert la.dataset.get_selected_spectrum(use_display_units=True) diff --git a/jdaviz/configs/specviz/plugins/unit_conversion/unit_conversion.py b/jdaviz/configs/specviz/plugins/unit_conversion/unit_conversion.py index 4bacd38f92..363da07ec5 100644 --- a/jdaviz/configs/specviz/plugins/unit_conversion/unit_conversion.py +++ b/jdaviz/configs/specviz/plugins/unit_conversion/unit_conversion.py @@ -8,9 +8,9 @@ SelectPluginComponent, PluginUserApi) from jdaviz.core.validunits import (create_spectral_equivalencies_list, create_flux_equivalencies_list, - create_sb_equivalencies_list, check_if_unit_is_per_solid_angle, - units_to_strings) + units_to_strings, + create_angle_equivalencies_list) __all__ = ['UnitConversion'] @@ -50,8 +50,8 @@ class UnitConversion(PluginTemplateMixin): Select the y-axis physical type for the spectrum-viewer. * ``flux_unit`` (:class:`~jdaviz.core.template_mixin.UnitSelectPluginComponent`): Global display unit for flux axis. - * ``sb_unit`` (:class:`~jdaviz.core.template_mixin.UnitSelectPluginComponent`): - Global display unit for surface brightness axis. + * ``angle_unit`` (:class:`~jdaviz.core.template_mixin.UnitSelectPluginComponent`): + Solid angle unit. """ template_file = __file__, "unit_conversion.vue" @@ -61,15 +61,14 @@ class UnitConversion(PluginTemplateMixin): flux_unit_items = List().tag(sync=True) flux_unit_selected = Unicode().tag(sync=True) - sb_unit_items = List().tag(sync=True) sb_unit_selected = Unicode().tag(sync=True) + angle_unit_items = List().tag(sync=True) + angle_unit_selected = Unicode().tag(sync=True) + flux_or_sb_items = List().tag(sync=True) flux_or_sb_selected = Unicode().tag(sync=True) - # in certain configs, a pixel scale factor will not be in the FITS header - # we need to disable translation in the API and UI variables/functions. - flux_or_sb_config_disabler = 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 @@ -108,20 +107,16 @@ def __init__(self, *args, **kwargs): items='flux_unit_items', selected='flux_unit_selected') - self.sb_unit = UnitSelectPluginComponent(self, - items='sb_unit_items', - selected='sb_unit_selected') + self.angle_unit = UnitSelectPluginComponent(self, + items='angle_unit_items', + selected='angle_unit_selected') @property def user_api(self): if self.app.config == 'cubeviz': - expose = ('spectral_unit', 'flux_or_sb', 'flux_unit', 'sb_unit') - elif self.app.config == 'specviz' and not self.flux_or_sb_config_disabler: - expose = ('spectral_unit', 'flux_unit', 'sb_unit') - elif self.flux_or_sb_config_disabler == 'Flux': - expose = ('spectral_unit', 'sb_unit') - else: # self.flux_or_sb_config_disabler == 'Surface Brightness' - expose = ('spectral_unit', 'flux_unit') + expose = ('spectral_unit', 'flux_or_sb', 'flux_unit', 'angle_unit') + else: + expose = ('spectral_unit', 'flux_unit', 'angle_unit') return PluginUserApi(self, expose=expose) def _on_glue_x_display_unit_changed(self, x_unit): @@ -140,7 +135,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) or not len(self.sb_unit.choices): + if not len(self.flux_unit.choices) or not len(self.angle_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) @@ -155,45 +150,42 @@ def _on_glue_y_display_unit_changed(self, y_unit): return self.spectrum_viewer.set_plot_axes() - if check_if_unit_is_per_solid_angle(y_unit): - flux_or_sb = 'Surface Brightness' - else: - flux_or_sb = 'Flux' - 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) - if flux_or_sb == 'Flux' and y_unit != self.flux_unit.selected: + if not check_if_unit_is_per_solid_angle(y_unit) and y_unit != self.flux_unit.selected: flux_choices = create_flux_equivalencies_list(y_u, x_u) - # ensure that original entry is in the list of choices if not np.any([y_u == u.Unit(choice) for choice in flux_choices]): flux_choices = [y_unit] + flux_choices - if self.app.config == 'cubeviz': - sb_choices = create_sb_equivalencies_list(y_u / u.sr, x_u) - self.sb_unit.choices = sb_choices - if y_unit + ' / sr' in self.sb_unit.choices: - self.sb_unit.selected = y_unit + ' / sr' - self.flux_unit.choices = flux_choices self.flux_unit.selected = y_unit - self.flux_or_sb.selected = 'Flux' - - elif flux_or_sb == 'Surface Brightness' and y_unit != self.sb_unit.selected: - sb_choices = create_sb_equivalencies_list(y_u, x_u) - # ensure that original entry is in the list of choices - if not np.any([y_u == u.Unit(choice) for choice in sb_choices]): - sb_choices = [y_unit] + sb_choices - - if self.app.config == 'cubeviz': - flux_choices = create_flux_equivalencies_list(y_u * u.sr, x_u) - self.flux_unit.choices = flux_choices - - self.sb_unit.choices = sb_choices - self.sb_unit.selected = y_unit + # 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): + updated_flux_choices = list(set(create_flux_equivalencies_list(y_u * u.sr, x_u)) + - set(units_to_strings(self._untranslatable_units))) + self.flux_unit.choices = updated_flux_choices + + # sets the angle unit drop down and the surface brightness read-only text + if self.app.data_collection[0]: + dc_unit = self.app.data_collection[0].get_component("flux").units + self.angle_unit.choices = create_angle_equivalencies_list(dc_unit) + self.angle_unit.selected = self.angle_unit.choices[0] + self.sb_unit_selected = self._append_angle_correctly( + self.flux_unit.selected, + self.angle_unit.selected + ) + self.hub.broadcast(GlobalDisplayUnitChanged('sb', + self.sb_unit_selected, + sender=self)) + + if not self.flux_unit.selected: + y_display_unit = self.spectrum_viewer.state.y_display_unit + self.flux_unit.selected = (str(u.Unit(y_display_unit * u.sr))) @observe('spectral_unit_selected') def _on_spectral_unit_changed(self, *args): @@ -204,30 +196,15 @@ def _on_spectral_unit_changed(self, *args): self.spectral_unit.selected, sender=self)) - @observe('flux_or_sb_selected', 'flux_unit_selected', 'sb_unit_selected') + @observe('flux_or_sb_selected', 'flux_unit_selected') def _on_flux_unit_changed(self, msg): # may need to be updated if translations in other configs going to be supported if not hasattr(self, 'flux_unit'): return if not self.flux_unit.choices and self.app.config == 'cubeviz': return - flux_or_sb = None - current_y = self.spectrum_viewer.state.y_display_unit - data_collection_unit = '' - # need to determine the input spectrum units to disable the additional - # drop down and possiblity of translations in Specviz. - if ( - len(self.app.data_collection) > 0 and - self.app.data_collection[0] and - self.app.config == 'specviz' - ): - if check_if_unit_is_per_solid_angle(self.app.data_collection[0].get_object().flux.unit): # noqa - data_collection_unit = 'Surface Brightness' - self.flux_or_sb_config_disabler = 'Flux' - else: - data_collection_unit = 'Flux' - self.flux_or_sb_config_disabler = 'Surface Brightness' + flux_or_sb = None name = msg.get('name') # determine if flux or surface brightness unit was changed by user @@ -235,16 +212,14 @@ def _on_flux_unit_changed(self, msg): # when the configuration is Specviz, translation is not currently supported. # If in Cubeviz, all spectra pass through Spectral Extraction plugin and will # have a scale factor assigned in the metadata, enabling translation. - if data_collection_unit == 'Surface Brightness': - raise ValueError( - f"Unit translation between Flux and Surface Brightness " - f"is not supported in {self.app.config}." + current_y_unit = self.spectrum_viewer.state.y_display_unit + if self.angle_unit.selected and check_if_unit_is_per_solid_angle(current_y_unit): + flux_or_sb = self._append_angle_correctly( + self.flux_unit.selected, + self.angle_unit.selected ) - flux_or_sb = self.flux_unit.selected - # update flux or surface brightness dropdown if necessary - if check_if_unit_is_per_solid_angle(current_y): - self._translate('Flux') - self.flux_or_sb.selected = 'Flux' + else: + flux_or_sb = self.flux_unit.selected untranslatable_units = self._untranslatable_units # disable translator if flux unit is untranslatable, # still can convert flux units, this just disables flux @@ -253,22 +228,6 @@ def _on_flux_unit_changed(self, msg): self.can_translate = False else: self.can_translate = True - - elif name == 'sb_unit_selected': - if data_collection_unit == 'Flux': - # when the configuration is Specviz, translation is not currently supported. - # If in Cubeviz, all spectra pass through Spectral Xxtraction plugin and will - # have a scale factor assigned in the metadata, enabling translation. - raise ValueError( - "Unit translation between Flux and Surface Brightness " - f"is not supported in {self.app.config}." - ) - flux_or_sb = self.sb_unit.selected - self.can_translate = True - # update flux or surface brightness dropdown if necessary - if not check_if_unit_is_per_solid_angle(current_y): - self._translate('Surface Brightness') - self.flux_or_sb.selected = 'Surface Brightness' elif name == 'flux_or_sb_selected': self._translate(self.flux_or_sb_selected) return @@ -279,13 +238,18 @@ def _on_flux_unit_changed(self, msg): if self.spectrum_viewer.state.y_display_unit != yunit: self.spectrum_viewer.state.y_display_unit = yunit + self.spectrum_viewer.reset_limits() self.hub.broadcast( GlobalDisplayUnitChanged( "flux" if name == "flux_unit_selected" else "sb", flux_or_sb, sender=self ) ) - self.spectrum_viewer.reset_limits() + if not check_if_unit_is_per_solid_angle(self.spectrum_viewer.state.y_display_unit): + self.flux_or_sb_selected = 'Flux' + else: + self.flux_or_sb_selected = 'Surface Brightness' + # for displaying message that PIXAR_SR = 1 if it is not found in the FITS header if ( len(self.app.data_collection) > 0 and not self.app.data_collection[0].meta.get('PIXAR_SR') @@ -299,8 +263,7 @@ def _translate(self, flux_or_sb=None): # we want to raise an error if a user tries to translate with an # untranslated Flux unit using the API - untranslatable_units = self._untranslatable_units - untranslatable_units = units_to_strings(untranslatable_units) + untranslatable_units = units_to_strings(self._untranslatable_units) if hasattr(self, 'flux_unit'): if ((self.flux_unit.selected in untranslatable_units) @@ -323,7 +286,6 @@ def _translate(self, flux_or_sb=None): spec_units *= u.sr # update display units self.spectrum_viewer.state.y_display_unit = str(spec_units) - self.flux_or_sb.selected = 'Flux' # Flux -> Surface Brightness elif (not check_if_unit_is_per_solid_angle(spec_units) @@ -331,7 +293,6 @@ def _translate(self, flux_or_sb=None): spec_units /= u.sr # update display units self.spectrum_viewer.state.y_display_unit = str(spec_units) - self.flux_or_sb.selected = 'Surface Brightness' # entered the translator when we shouldn't translate else: return @@ -351,3 +312,19 @@ def _untranslatable_units(self): 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 + return flux_unit + if '(' in flux_unit: + pos = flux_unit.rfind(')') + sb_unit_selected = flux_unit[:pos] + ' ' + angle_unit + flux_unit[pos:] + else: + # append angle if there are no parentheses + sb_unit_selected = flux_unit + ' / ' + angle_unit + + if sb_unit_selected: + self.sb_unit_selected = sb_unit_selected + + return sb_unit_selected diff --git a/jdaviz/configs/specviz/plugins/unit_conversion/unit_conversion.vue b/jdaviz/configs/specviz/plugins/unit_conversion/unit_conversion.vue index 5eae0ec7d2..ab6fc041c4 100644 --- a/jdaviz/configs/specviz/plugins/unit_conversion/unit_conversion.vue +++ b/jdaviz/configs/specviz/plugins/unit_conversion/unit_conversion.vue @@ -19,7 +19,7 @@ > - + - - + + + + + + - Translation is not available due to current unit selection. + :disabled='true' + > diff --git a/jdaviz/core/helpers.py b/jdaviz/core/helpers.py index bd3f97d0a3..e4e4c377b0 100644 --- a/jdaviz/core/helpers.py +++ b/jdaviz/core/helpers.py @@ -486,14 +486,21 @@ def _handle_display_units(self, data, use_display_units=True): else: # if not specified as NDUncertainty, assume stddev: new_uncert = uncertainty - new_uncert_converted = flux_conversion(data, new_uncert.quantity.value, - new_uncert.unit, flux_unit) - new_uncert = StdDevUncertainty(new_uncert_converted, unit=flux_unit) + if ('_pixel_scale_factor' in data.meta): + new_uncert_converted = flux_conversion(data, new_uncert.quantity.value, + new_uncert.unit, flux_unit) + new_uncert = StdDevUncertainty(new_uncert_converted, unit=flux_unit) + else: + new_uncert = StdDevUncertainty(new_uncert, unit=data.flux.unit) + else: new_uncert = None - - new_flux = flux_conversion(data, data.flux.value, data.flux.unit, - flux_unit) * u.Unit(flux_unit) + if ('_pixel_scale_factor' in data.meta): + new_flux = flux_conversion(data, data.flux.value, data.flux.unit, + flux_unit) * u.Unit(flux_unit) + else: + new_flux = flux_conversion(data, data.flux.value, data.flux.unit, + data.flux.unit) * 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 8880eb39f1..8ba718bff4 100644 --- a/jdaviz/core/validunits.py +++ b/jdaviz/core/validunits.py @@ -90,38 +90,28 @@ def create_flux_equivalencies_list(flux_unit, spectral_axis_unit): return sorted(units_to_strings(local_units)) + flux_unit_equivalencies_titles -def create_sb_equivalencies_list(sb_unit, spectral_axis_unit): - """Get all possible conversions for flux from current flux units.""" - if ((sb_unit in (u.count, u.dimensionless_unscaled)) - or (spectral_axis_unit in (u.pix, u.dimensionless_unscaled))): - return [] - - # Get unit equivalencies. Value passed into u.spectral_density() is irrelevant. - try: - curr_sb_unit_equivalencies = sb_unit.find_equivalent_units( - equivalencies=u.spectral_density(1 * spectral_axis_unit), - include_prefix_units=False) - except u.core.UnitConversionError: - return [] - - locally_defined_sb_units = ['Jy / sr', 'mJy / sr', - 'uJy / sr', 'MJy / sr', - 'W / (Hz sr m2)', - 'eV / (Hz s sr m2)', - 'AB / sr' - ] - - local_units = [u.Unit(unit) for unit in locally_defined_sb_units] - - # Remove overlap units. - curr_sb_unit_equivalencies = list(set(curr_sb_unit_equivalencies) - - set(local_units)) - - # Convert equivalencies into readable versions of the units and sort them alphabetically. - sb_unit_equivalencies_titles = sorted(units_to_strings(curr_sb_unit_equivalencies)) +def create_angle_equivalencies_list(unit): + # first, convert string to u.Unit obj. + # this will take care of some formatting consistency like + # turning something like Jy / (degree*degree) to Jy / deg**2 + # and erg sr^1 to erg / sr + if isinstance(unit, u.core.Unit) or isinstance(unit, u.core.CompositeUnit): + unit_str = unit.to_string() + elif isinstance(unit, str): + unit = u.Unit(unit) + unit_str = unit.to_string() + elif unit == 'ct': + return ['pix'] + else: + raise ValueError('Unit must be u.Unit, or string that can be converted into a u.Unit') - # Concatenate both lists with the local units coming first. - return sorted(units_to_strings(local_units)) + sb_unit_equivalencies_titles + if '/' in unit_str: + # might be comprised of several units in denom. + denom = unit_str.split('/')[-1].split() + return denom + else: + # this could be where force / u.pix + return ['pix'] def check_if_unit_is_per_solid_angle(unit): From 146552ba32b58031348e22faafc0e923ff89645a Mon Sep 17 00:00:00 2001 From: Jesse Averbukh Date: Thu, 1 Aug 2024 17:19:54 -0400 Subject: [PATCH 003/127] Add Gaia catalog to plugin (#3090) * Add Gaia catalog to plugin Address review comments Add max rows option for Gaia * Update table with results * Address review comments * Update changes * Address review comments * Enable sources for SDSS * Remove using row limits for SDSS * Apply suggestions from code review Co-authored-by: Brett M. Morris --------- Co-authored-by: Brett M. Morris --- CHANGES.rst | 2 ++ .../imviz/plugins/catalogs/catalogs.py | 20 ++++++++++++++++++- .../imviz/plugins/catalogs/catalogs.vue | 18 +++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index b3276caa95..c430cd4577 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -34,6 +34,8 @@ Imviz - "Imviz Line Profiles (XY)" plugin is renamed to "Image Profiles (XY)". [#3121] +- Added Gaia catalog to Catalog plugin. [#3090] + Mosviz ^^^^^^ diff --git a/jdaviz/configs/imviz/plugins/catalogs/catalogs.py b/jdaviz/configs/imviz/plugins/catalogs/catalogs.py index 7109dc8c35..24a6ffd07e 100644 --- a/jdaviz/configs/imviz/plugins/catalogs/catalogs.py +++ b/jdaviz/configs/imviz/plugins/catalogs/catalogs.py @@ -10,6 +10,7 @@ from jdaviz.core.template_mixin import (PluginTemplateMixin, ViewerSelectMixin, FileImportSelectPluginComponent, HasFileImportSelect, with_spinner) +from jdaviz.core.custom_traitlets import IntHandleEmpty from jdaviz.core.template_mixin import TableMixin from jdaviz.core.user_api import PluginUserApi @@ -34,6 +35,7 @@ class Catalogs(PluginTemplateMixin, ViewerSelectMixin, HasFileImportSelect, Tabl catalog_selected = Unicode("").tag(sync=True) results_available = Bool(False).tag(sync=True) number_of_results = Int(0).tag(sync=True) + max_gaia_sources = IntHandleEmpty(1000).tag(sync=True) # setting the default table headers and values _default_table_values = { @@ -50,7 +52,8 @@ def __init__(self, *args, **kwargs): self.catalog = FileImportSelectPluginComponent(self, items='catalog_items', selected='catalog_selected', - manual_options=['SDSS', 'From File...']) + manual_options=['SDSS', 'Gaia', + 'From File...']) # set the custom file parser for importing catalogs self.catalog._file_parser = self._file_parser @@ -169,6 +172,21 @@ def search(self, error_on_fail=False): 'Object ID': row['objid'].astype(str)} self.table.add_item(row_info) + elif self.catalog_selected == 'Gaia': + from astroquery.gaia import Gaia, conf + + with conf.set_temp("ROW_LIMIT", self.max_gaia_sources): + sources = Gaia.query_object(skycoord_center, radius=zoom_radius, + columns=('source_id', 'ra', 'dec')) + self.app._catalog_source_table = sources + skycoord_table = SkyCoord(sources['ra'], sources['dec'], unit='deg') + # adding in coords + Id's into table + for row in sources: + row_info = {'Right Ascension (degrees)': row['ra'], + 'Declination (degrees)': row['dec'], + 'Source ID': row['SOURCE_ID']} + self.table.add_item(row_info) + elif self.catalog_selected == 'From File...': # all exceptions when going through the UI should have prevented setting this path # but this exceptions might be raised here if setting from_file from the UI diff --git a/jdaviz/configs/imviz/plugins/catalogs/catalogs.vue b/jdaviz/configs/imviz/plugins/catalogs/catalogs.vue index 6f62e0dbfb..4f23c35247 100644 --- a/jdaviz/configs/imviz/plugins/catalogs/catalogs.vue +++ b/jdaviz/configs/imviz/plugins/catalogs/catalogs.vue @@ -28,6 +28,24 @@ + + + See the for details on the query defaults. + + + + + + + Date: Fri, 2 Aug 2024 07:49:25 -0400 Subject: [PATCH 004/127] Adding newest videos to the docs (#3127) * Add jwebbinar videos and new mast video * typo * Update docs/video_tutorials.rst --- docs/video_tutorials.rst | 38 +++++++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/docs/video_tutorials.rst b/docs/video_tutorials.rst index 77de06ddb5..47daa6d304 100644 --- a/docs/video_tutorials.rst +++ b/docs/video_tutorials.rst @@ -34,7 +34,14 @@ Create a color-composite image -Interact with the viewer form the notebook, from the JWebbinar 02/2023 +Instrument footprints and data quality arrays, from JWebbinar 05/2024 +--------------------------------------------------------------------- + +.. raw:: html + + + +Interact with the viewer from the notebook, from JWebbinar 02/2023 ---------------------------------------------------------------------- .. raw:: html @@ -82,7 +89,21 @@ Aperture photometry (old version) Cubeviz ======= -Measure emission line properties, from the JWebbinar 02/2023 +Measure distances on the sky +---------------------------- + +.. raw:: html + + + +Create moment maps, from JWebbinar 05/2024 +------------------------------------------ + +.. raw:: html + + + +Measure emission line properties, from JWebbinar 02/2023 ------------------------------------------------------------ .. raw:: html @@ -122,7 +143,7 @@ Line analysis (old version) Specviz ======= -Line analysis and model fitting, from the JWebbinar 02/2023 +Line analysis and model fitting, from JWebbinar 02/2023 ----------------------------------------------------------- .. raw:: html @@ -156,7 +177,14 @@ Line analysis (old version) Specviz2d ========= -Spectral extraction, from the JWebbinar 02/2023 +Extraction and basic analysis, from JWebbinar 05/2024 +----------------------------------------------------- + +.. raw:: html + + + +Spectral extraction, from JWebbinar 02/2023 ----------------------------------------------- .. raw:: html @@ -168,7 +196,7 @@ Spectral extraction, from the JWebbinar 02/2023 Mosviz ====== -Measure redshifts, from the JWebbinar 02/2023 +Measure redshifts, from JWebbinar 02/2023 --------------------------------------------- .. raw:: html From b3bbd9ff152e3865f7a64f9769f1629c6a839bec Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Fri, 2 Aug 2024 10:10:34 -0400 Subject: [PATCH 005/127] expose surface brightness units in internal get_display_units method (#3112) * expose surface brightness units in app._get_display_units --- jdaviz/app.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/jdaviz/app.py b/jdaviz/app.py index 15d721ee4f..775d9169c0 100644 --- a/jdaviz/app.py +++ b/jdaviz/app.py @@ -52,6 +52,7 @@ from jdaviz.utils import (SnackbarQueue, alpha_index, data_has_valid_wcs, layer_is_table_data, MultiMaskSubsetState, _wcs_only_label, flux_conversion, spectral_axis_conversion) +from jdaviz.core.validunits import check_if_unit_is_per_solid_angle __all__ = ['Application', 'ALL_JDAVIZ_CONFIGS', 'UnitConverterWithSpectral'] @@ -1249,14 +1250,29 @@ def _get_display_unit(self, axis): if axis == 'spectral': sv = self.get_viewer(self._jdaviz_helper._default_spectrum_viewer_reference_name) return sv.data()[0].spectral_axis.unit - elif axis == 'flux': + elif axis in ('flux', 'sb', 'spectral_y'): sv = self.get_viewer(self._jdaviz_helper._default_spectrum_viewer_reference_name) - return sv.data()[0].flux.unit + sv_y_unit = sv.data()[0].flux.unit + if axis == 'spectral_y': + return sv_y_unit + elif axis == 'flux': + if check_if_unit_is_per_solid_angle(sv_y_unit): + # TODO: this will need updating once solid-angle unit can be non-steradian + return sv_y_unit * u.sr + return sv_y_unit + else: + # surface_brightness + if check_if_unit_is_per_solid_angle(sv_y_unit): + return sv_y_unit + return sv_y_unit / u.sr else: raise ValueError(f"could not find units for axis='{axis}'") + uc = self._jdaviz_helper.plugins.get('Unit Conversion')._obj + if axis == 'spectral_y': + # translate options from uc.flux_or_sb to the prefix used in uc.??_unit_selected + axis = {'Surface Brightness': 'sb', 'Flux': 'flux'}[uc.flux_or_sb_selected] try: - return getattr(self._jdaviz_helper.plugins.get('Unit Conversion')._obj, - f'{axis}_unit_selected') + return getattr(uc, f'{axis}_unit_selected') except AttributeError: raise ValueError(f"could not find display unit for axis='{axis}'") From 01f8880c757f23e9b0be428173c9df890c325edf Mon Sep 17 00:00:00 2001 From: Migelo Date: Fri, 2 Aug 2024 14:24:02 +0000 Subject: [PATCH 006/127] upgrade upload-artifact to v4 --- .github/workflows/standalone.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/standalone.yml b/.github/workflows/standalone.yml index dd5b5aef1e..e081f486a8 100644 --- a/.github/workflows/standalone.yml +++ b/.github/workflows/standalone.yml @@ -62,14 +62,14 @@ jobs: - name: Upload Test artifacts if: github.event_name != 'pull_request' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: test-results-${{ matrix.os }} path: standalone/test-results - name: Upload jdaviz standalone (non-OSX) if: github.event_name != 'pull_request' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: jdaviz-standlone-${{ matrix.os }} path: | @@ -185,14 +185,14 @@ jobs: - name: Upload Test artifacts if: always() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: test-results-${{ matrix.os }} path: standalone/test-results - name: Upload jdaviz standalone (OSX) if: ${{ always() && (matrix.os == 'macos') }} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: jdaviz-standlone-${{ matrix.os }} path: standalone/dist/jdaviz.dmg From 453ecc80abdbb7462a95b7a160ca74fa3fa07b59 Mon Sep 17 00:00:00 2001 From: Ricky O'Steen <39831871+rosteen@users.noreply.github.com> Date: Fri, 2 Aug 2024 15:06:23 -0400 Subject: [PATCH 007/127] Unit conversion bugfix and variable name change (#3131) * Only show PIXAR_SR warning in cubeviz * More descriptive variable names * Fix codestyle --- .../unit_conversion/unit_conversion.py | 42 +++++++++---------- .../unit_conversion/unit_conversion.vue | 2 +- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/jdaviz/configs/specviz/plugins/unit_conversion/unit_conversion.py b/jdaviz/configs/specviz/plugins/unit_conversion/unit_conversion.py index 363da07ec5..bff50f481f 100644 --- a/jdaviz/configs/specviz/plugins/unit_conversion/unit_conversion.py +++ b/jdaviz/configs/specviz/plugins/unit_conversion/unit_conversion.py @@ -119,29 +119,29 @@ def user_api(self): expose = ('spectral_unit', 'flux_unit', 'angle_unit') return PluginUserApi(self, expose=expose) - def _on_glue_x_display_unit_changed(self, x_unit): - if x_unit is None: + def _on_glue_x_display_unit_changed(self, x_unit_str): + if x_unit_str is None: return self.spectrum_viewer.set_plot_axes() - if x_unit != self.spectral_unit.selected: - x_unit = _valid_glue_display_unit(x_unit, self.spectrum_viewer, 'x') - x_u = u.Unit(x_unit) - choices = create_spectral_equivalencies_list(x_u) + if x_unit_str != self.spectral_unit.selected: + x_unit_str = _valid_glue_display_unit(x_unit_str, self.spectrum_viewer, 'x') + x_unit = u.Unit(x_unit_str) + choices = create_spectral_equivalencies_list(x_unit) # ensure that original entry is in the list of choices - if not np.any([x_u == u.Unit(choice) for choice in choices]): - choices = [x_unit] + choices + if not np.any([x_unit == u.Unit(choice) for choice in choices]): + choices = [x_unit_str] + choices self.spectral_unit.choices = choices # in addition to the jdaviz options, allow the user to set any glue-valid 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 + self.spectral_unit.selected = x_unit_str if not len(self.flux_unit.choices) or not len(self.angle_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) - def _on_glue_y_display_unit_changed(self, y_unit): - if y_unit is None: + def _on_glue_y_display_unit_changed(self, y_unit_str): + if y_unit_str is None: return if self.spectral_unit.selected == "": # no spectral unit set yet, cannot determine equivalencies @@ -150,23 +150,23 @@ def _on_glue_y_display_unit_changed(self, y_unit): return self.spectrum_viewer.set_plot_axes() - 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) + x_unit = u.Unit(self.spectral_unit.selected) + y_unit_str = _valid_glue_display_unit(y_unit_str, self.spectrum_viewer, 'y') + y_unit = u.Unit(y_unit_str) - if not check_if_unit_is_per_solid_angle(y_unit) and y_unit != self.flux_unit.selected: - flux_choices = create_flux_equivalencies_list(y_u, x_u) + if not check_if_unit_is_per_solid_angle(y_unit_str) and y_unit_str != self.flux_unit.selected: # noqa + flux_choices = create_flux_equivalencies_list(y_unit, x_unit) # ensure that original entry is in the list of choices - if not np.any([y_u == u.Unit(choice) for choice in flux_choices]): - flux_choices = [y_unit] + flux_choices + if not np.any([y_unit == u.Unit(choice) for choice in flux_choices]): + flux_choices = [y_unit_str] + flux_choices self.flux_unit.choices = flux_choices - self.flux_unit.selected = y_unit + self.flux_unit.selected = 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): - updated_flux_choices = list(set(create_flux_equivalencies_list(y_u * u.sr, x_u)) + 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 diff --git a/jdaviz/configs/specviz/plugins/unit_conversion/unit_conversion.vue b/jdaviz/configs/specviz/plugins/unit_conversion/unit_conversion.vue index ab6fc041c4..60637d83f2 100644 --- a/jdaviz/configs/specviz/plugins/unit_conversion/unit_conversion.vue +++ b/jdaviz/configs/specviz/plugins/unit_conversion/unit_conversion.vue @@ -72,7 +72,7 @@ Translation is not available due to current unit selection. - + PIXAR_SR FITS header keyword not found when parsing spectral cube. Flux/Surface Brightness will use default PIXAR_SR value of 1. From 45734f626bf1960811eaf54bde7ab1ae4ecb16b7 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Mon, 5 Aug 2024 09:49:49 -0400 Subject: [PATCH 008/127] get_data(use_display_units=True) to respect flux or sb selection (#3129) * get_data(use_display_units) to respect flux or sb selection * add to existing changelog entry * test coverage --- CHANGES.rst | 2 +- .../tests/test_unit_conversion.py | 10 +++++++++- jdaviz/core/helpers.py | 16 ++++++++-------- jdaviz/core/template_mixin.py | 11 ----------- 4 files changed, 18 insertions(+), 21 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index c430cd4577..be0501a337 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] + unit conversion in Cubeviz and Specviz. [#2781, #2940, #3088, #3111, #3113, #3129] - Plugin tray is now open by default. [#2892] 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 5dd70ad6ff..4380599b02 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 @@ -137,7 +137,7 @@ def test_unit_translation(cubeviz_helper): 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) + cube = Spectrum1D(flux=flux * u.MJy / u.sr, wcs=w, meta=wcs_dict) cubeviz_helper.load_data(cube, data_label="test") center = PixCoord(5, 10) @@ -152,6 +152,10 @@ def test_unit_translation(cubeviz_helper): # data collection item units will be used for translations. assert uc_plg._obj.flux_or_sb_selected == 'Flux' + # accessing from get_data(use_display_units=True) should return flux-like units + assert cubeviz_helper.app._get_display_unit('spectral_y') == u.MJy + assert cubeviz_helper.get_data('Spectrum (sum)', use_display_units=True).unit == u.MJy + # to have access to display units viewer_1d = cubeviz_helper.app.get_viewer( cubeviz_helper._default_spectrum_viewer_reference_name) @@ -165,6 +169,10 @@ def test_unit_translation(cubeviz_helper): # check if units translated assert y_display_unit == u.MJy / u.sr + # get_data(use_display_units=True) should return surface brightness-like units + assert cubeviz_helper.app._get_display_unit('spectral_y') == u.MJy / u.sr + assert cubeviz_helper.get_data('Spectrum (sum)', use_display_units=True).unit == u.MJy / u.sr + def test_sb_unit_conversion(cubeviz_helper): # custom cube to have Surface Brightness units diff --git a/jdaviz/core/helpers.py b/jdaviz/core/helpers.py index e4e4c377b0..bff94ab08f 100644 --- a/jdaviz/core/helpers.py +++ b/jdaviz/core/helpers.py @@ -470,7 +470,7 @@ def _handle_display_units(self, data, use_display_units=True): spectral_unit = self.app._get_display_unit('spectral') if not spectral_unit: return data - flux_unit = self.app._get_display_unit('flux') + y_unit = self.app._get_display_unit('spectral_y') # TODO: any other attributes (meta, wcs, etc)? # TODO: implement uncertainty.to upstream uncertainty = data.uncertainty @@ -488,26 +488,26 @@ def _handle_display_units(self, data, use_display_units=True): new_uncert = uncertainty if ('_pixel_scale_factor' in data.meta): new_uncert_converted = flux_conversion(data, new_uncert.quantity.value, - new_uncert.unit, flux_unit) - new_uncert = StdDevUncertainty(new_uncert_converted, unit=flux_unit) + new_uncert.unit, y_unit) + new_uncert = StdDevUncertainty(new_uncert_converted, unit=y_unit) else: new_uncert = StdDevUncertainty(new_uncert, unit=data.flux.unit) else: new_uncert = None if ('_pixel_scale_factor' in data.meta): - new_flux = flux_conversion(data, data.flux.value, data.flux.unit, - flux_unit) * u.Unit(flux_unit) + new_y = flux_conversion(data, data.flux.value, data.flux.unit, + y_unit) * u.Unit(y_unit) else: - new_flux = flux_conversion(data, data.flux.value, data.flux.unit, - data.flux.unit) * u.Unit(data.flux.unit) + new_y = flux_conversion(data, data.flux.value, data.flux.unit, + data.flux.unit) * u.Unit(data.flux.unit) new_spec = (spectral_axis_conversion(data.spectral_axis.value, data.spectral_axis.unit, spectral_unit) * u.Unit(spectral_unit)) data = Spectrum1D(spectral_axis=new_spec, - flux=new_flux, + flux=new_y, uncertainty=new_uncert, mask=data.mask) else: # pragma: nocover diff --git a/jdaviz/core/template_mixin.py b/jdaviz/core/template_mixin.py index a5f1a775ed..bf196988ba 100644 --- a/jdaviz/core/template_mixin.py +++ b/jdaviz/core/template_mixin.py @@ -2955,17 +2955,6 @@ def _get_continuum(self, dataset, spectral_subset, update_marks=False, per_pixel raise ValueError("per-pixel only supported for cubeviz") full_spectrum = self.app._jdaviz_helper.get_data(self.dataset.selected, use_display_units=True) - # TODO: Something like the following code may be needed to get continuum - # with display units working - # temp_spec = self.app._jdaviz_helper.get_data(self.dataset.selected, - # use_display_units=True) - # 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 - # full_spectrum = self._specviz_helper._handle_display_units(temp_spec, - # use_display_units=True) - else: full_spectrum = dataset.get_selected_spectrum(use_display_units=True) From f4a68899661d5fd115ce47af78395adf2c6f2458 Mon Sep 17 00:00:00 2001 From: Ricky O'Steen <39831871+rosteen@users.noreply.github.com> Date: Mon, 5 Aug 2024 11:29:17 -0400 Subject: [PATCH 009/127] Update orientation API to match UI (#3128) * Replace wcs_use_affine with wcs_fast_approximation * Changelog and codestyle * Add deprecation warnings * Update concept notebooks * Use Astropy deprecation instead * Fix failing test * Make deprecation ignores explicit --- CHANGES.rst | 3 + jdaviz/app.py | 12 +- .../configs/default/plugins/export/export.py | 6 +- .../plugins/export/tests/test_export.py | 2 +- .../default/plugins/markers/markers.py | 6 +- .../plugins/plot_options/plot_options.py | 2 +- .../plugins/subset_plugin/subset_plugin.py | 8 +- .../subset_plugin/tests/test_subset_plugin.py | 2 +- jdaviz/configs/imviz/helper.py | 37 +++-- .../imviz/plugins/footprints/footprints.py | 6 +- .../imviz/plugins/orientation/orientation.py | 137 ++++++++++-------- .../imviz/plugins/orientation/orientation.vue | 12 +- jdaviz/configs/imviz/plugins/viewers.py | 22 +-- .../imviz/tests/test_astrowidgets_api.py | 2 +- .../configs/imviz/tests/test_delete_data.py | 4 +- jdaviz/configs/imviz/tests/test_footprints.py | 4 +- jdaviz/configs/imviz/tests/test_helper.py | 3 + jdaviz/configs/imviz/tests/test_linking.py | 35 ++--- .../configs/imviz/tests/test_orientation.py | 50 +++---- .../imviz/tests/test_simple_aper_phot.py | 6 +- .../imviz/tests/test_subset_centroid.py | 2 +- jdaviz/configs/imviz/tests/test_tools.py | 8 +- jdaviz/configs/imviz/tests/test_wcs_utils.py | 2 +- jdaviz/core/events.py | 8 +- jdaviz/core/helpers.py | 4 +- jdaviz/core/region_translators.py | 6 +- jdaviz/tests/test_subsets.py | 2 +- notebooks/ImvizExample.ipynb | 4 +- .../concepts/imviz_advanced_aper_phot.ipynb | 2 +- notebooks/concepts/imviz_compass_mpl.ipynb | 2 +- notebooks/concepts/imviz_dithered_gwcs.ipynb | 4 +- notebooks/concepts/imviz_roman_asdf.ipynb | 2 +- .../concepts/imviz_simple_aper_phot.ipynb | 2 +- 33 files changed, 220 insertions(+), 187 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index be0501a337..270d17759c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -36,6 +36,9 @@ Imviz - Added Gaia catalog to Catalog plugin. [#3090] +- Updated ``link_type`` to ``align_by`` and ``wcs_use_affine`` to ``wcs_fast_approximation`` in + Orientation plugin API to better match UI text. [#3128] + Mosviz ^^^^^^ diff --git a/jdaviz/app.py b/jdaviz/app.py index 775d9169c0..47f7a9646d 100644 --- a/jdaviz/app.py +++ b/jdaviz/app.py @@ -325,9 +325,9 @@ def __init__(self, configuration=None, *args, **kwargs): self.auto_link = kwargs.pop('auto_link', True) # Imviz linking - self._link_type = 'pixels' + self._align_by = 'pixels' if self.config == "imviz": - self._wcs_use_affine = None + self._wcs_fast_approximation = None # Subscribe to messages indicating that a new viewer needs to be # created. When received, information is passed to the application @@ -1938,7 +1938,7 @@ def _reparent_subsets(self, old_parent, new_parent=None): # Translate bounds through WCS if needed if (self.config == "imviz" and - self._jdaviz_helper.plugins["Orientation"].link_type == "WCS"): + self._jdaviz_helper.plugins["Orientation"].align_by == "WCS"): # Default shape for WCS-only layers is 10x10, but it doesn't really matter # since we only need the angles. @@ -2219,7 +2219,7 @@ def vue_data_item_remove(self, event): # Make sure the data isn't loaded in any viewers and isn't the selected orientation for viewer_id, viewer in self._viewer_store.items(): - if orientation_plugin is not None and self._link_type == 'wcs': + if orientation_plugin is not None and self._align_by == 'wcs': if viewer.state.reference_data.label == data_label: self._change_reference_data(base_wcs_layer_label, viewer_id) self.remove_data_from_viewer(viewer_id, data_label) @@ -2506,12 +2506,12 @@ def _on_new_viewer(self, msg, vid=None, name=None, add_layers_to_viewer=False, msg.cls, data=msg.data, show=False) viewer.figure_widget.layout.height = '100%' - linked_by_wcs = self._link_type == 'wcs' + linked_by_wcs = self._align_by == 'wcs' if hasattr(viewer.state, 'linked_by_wcs'): orientation_plugin = self._jdaviz_helper.plugins.get('Orientation', None) if orientation_plugin is not None: - linked_by_wcs = orientation_plugin.link_type.selected == 'WCS' + linked_by_wcs = orientation_plugin.align_by.selected == 'WCS' elif len(self._viewer_store) and hasattr(self._jdaviz_helper, 'default_viewer'): # The plugin would only not exist for instances of Imviz where the user has # intentionally removed the Orientation plugin, but in that case we will diff --git a/jdaviz/configs/default/plugins/export/export.py b/jdaviz/configs/default/plugins/export/export.py index f62fd8cb6c..5734e63f66 100644 --- a/jdaviz/configs/default/plugins/export/export.py +++ b/jdaviz/configs/default/plugins/export/export.py @@ -728,12 +728,12 @@ def save_subset_as_region(self, selected_subset_label, filename): """ # type of region saved depends on link type - link_type = getattr(self.app, '_link_type', None) + align_by = getattr(self.app, '_align_by', None) region = self.app.get_subsets(subset_name=selected_subset_label, - include_sky_region=link_type == 'wcs') + include_sky_region=align_by == 'wcs') - region = region[0][f'{"sky_" if link_type == "wcs" else ""}region'] + region = region[0][f'{"sky_" if align_by == "wcs" else ""}region'] region.write(str(filename), overwrite=True) diff --git a/jdaviz/configs/default/plugins/export/tests/test_export.py b/jdaviz/configs/default/plugins/export/tests/test_export.py index 701f07aca9..ebec3820d9 100644 --- a/jdaviz/configs/default/plugins/export/tests/test_export.py +++ b/jdaviz/configs/default/plugins/export/tests/test_export.py @@ -105,7 +105,7 @@ def test_export_subsets_wcs(self, imviz_helper, spectral_cube_wcs): imviz_helper.load_data(data) # load data twice so we can link them imviz_helper.load_data(data) - imviz_helper.link_data(link_type='wcs') + imviz_helper.link_data(align_by='wcs') imviz_helper.app.get_viewer('imviz-0').apply_roi(CircularROI(xc=8, yc=6, diff --git a/jdaviz/configs/default/plugins/markers/markers.py b/jdaviz/configs/default/plugins/markers/markers.py index 59dc0067f3..8f3a27c749 100644 --- a/jdaviz/configs/default/plugins/markers/markers.py +++ b/jdaviz/configs/default/plugins/markers/markers.py @@ -129,7 +129,7 @@ def _recompute_mark_positions(self, viewer): orig_world_x = np.asarray(self.table._qtable['world_ra'][in_viewer]) orig_world_y = np.asarray(self.table._qtable['world_dec'][in_viewer]) - if self.app._link_type.lower() == 'wcs': + if self.app._align_by.lower() == 'wcs': # convert from the sky coordinates in the table to pixels via the WCS of the current # reference data new_wcs = viewer.state.reference_data.coords @@ -139,7 +139,7 @@ def _recompute_mark_positions(self, viewer): except Exception: # fail gracefully new_x, new_y = [], [] - elif self.app._link_type == 'pixels': + elif self.app._align_by == 'pixels': # we need to convert based on the WCS of the individual data layers on which each mark # was first created new_x, new_y = np.zeros_like(orig_world_x), np.zeros_like(orig_world_y) @@ -156,7 +156,7 @@ def _recompute_mark_positions(self, viewer): new_x, new_y = [], [] break else: - raise NotImplementedError(f"link_type {self.app._link_type} not implemented") + raise NotImplementedError(f"align_by {self.app._align_by} not implemented") # check for entries that do not correspond to a layer or only have pixel coordinates pixel_only_inds = data_labels == '' diff --git a/jdaviz/configs/default/plugins/plot_options/plot_options.py b/jdaviz/configs/default/plugins/plot_options/plot_options.py index d77aa30451..e457061fa1 100644 --- a/jdaviz/configs/default/plugins/plot_options/plot_options.py +++ b/jdaviz/configs/default/plugins/plot_options/plot_options.py @@ -741,7 +741,7 @@ def _on_global_display_unit_changed(self, *args): self.send_state('display_units') def _on_refdata_change(self, *args): - if self.app._link_type.lower() == 'wcs': + if self.app._align_by.lower() == 'wcs': self.display_units['image'] = 'deg' else: self.display_units['image'] = 'pix' diff --git a/jdaviz/configs/default/plugins/subset_plugin/subset_plugin.py b/jdaviz/configs/default/plugins/subset_plugin/subset_plugin.py index 891fb57846..d963bdd12d 100644 --- a/jdaviz/configs/default/plugins/subset_plugin/subset_plugin.py +++ b/jdaviz/configs/default/plugins/subset_plugin/subset_plugin.py @@ -90,8 +90,8 @@ def __init__(self, *args, **kwargs): self.subset_states = [] self.spectral_display_unit = None - link_type = getattr(self.app, '_link_type', None) - self.display_sky_coordinates = (link_type == 'wcs' and not self.multiselect) + align_by = getattr(self.app, '_align_by', None) + self.display_sky_coordinates = (align_by == 'wcs' and not self.multiselect) def _on_link_update(self, *args): """When linking is changed pixels<>wcs, change display units of the @@ -100,8 +100,8 @@ def _on_link_update(self, *args): to the UI upon link change by calling _get_subset_definition, which will re-determine how to display subset information.""" - link_type = getattr(self.app, '_link_type', None) - self.display_sky_coordinates = (link_type == 'wcs') + align_by = getattr(self.app, '_align_by', None) + self.display_sky_coordinates = (align_by == 'wcs') if self.subset_selected != self.subset_select.default_text: self._get_subset_definition(*args) diff --git a/jdaviz/configs/default/plugins/subset_plugin/tests/test_subset_plugin.py b/jdaviz/configs/default/plugins/subset_plugin/tests/test_subset_plugin.py index b9edeba99d..0c1e56df08 100644 --- a/jdaviz/configs/default/plugins/subset_plugin/tests/test_subset_plugin.py +++ b/jdaviz/configs/default/plugins/subset_plugin/tests/test_subset_plugin.py @@ -138,7 +138,7 @@ def test_circle_recenter_linking(roi_class, subset_info, imviz_helper, image_2d_ # remove subsets and change link type to wcs dc = imviz_helper.app.data_collection dc.remove_subset_group(dc.subset_groups[0]) - imviz_helper.link_data(link_type='wcs') + imviz_helper.link_data(align_by='wcs') assert plugin.display_sky_coordinates # linking change should trigger change to True # apply original subset. transform sky coord of original subset to new pixels diff --git a/jdaviz/configs/imviz/helper.py b/jdaviz/configs/imviz/helper.py index 92c7b93c6d..dbd97f369c 100644 --- a/jdaviz/configs/imviz/helper.py +++ b/jdaviz/configs/imviz/helper.py @@ -3,6 +3,7 @@ import warnings from copy import deepcopy +from astropy.utils import deprecated import numpy as np from glue.core.link_helpers import LinkSame @@ -190,7 +191,7 @@ def load_data(self, data, data_label=None, show_in_viewer=True, **kwargs): show_in_viewer = f"{self.app.config}-0" if show_in_viewer: - linked_by_wcs = self.app._link_type == 'wcs' + linked_by_wcs = self.app._align_by == 'wcs' if linked_by_wcs: for applied_label, visible, is_wcs_only, has_wcs in zip( applied_labels, applied_visible, layer_is_wcs_only, layer_has_wcs @@ -211,7 +212,7 @@ def load_data(self, data, data_label=None, show_in_viewer=True, **kwargs): else: if 'Orientation' not in self.plugins.keys(): # otherwise plugin will handle linking automatically with DataCollectionAddMessage - self.link_data(link_type='wcs') + self.link_data(align_by='wcs') # One input might load into multiple Data objects. # NOTE: If the batch_load context manager was used, it will @@ -223,35 +224,39 @@ def load_data(self, data, data_label=None, show_in_viewer=True, **kwargs): if (has_wcs and linked_by_wcs) or not linked_by_wcs: self.app.add_data_to_viewer(show_in_viewer, applied_label, visible=visible) - def link_data(self, link_type='pixels', wcs_fallback_scheme=None, wcs_use_affine=True): + def link_data(self, align_by='pixels', wcs_fallback_scheme=None, wcs_fast_approximation=True): """(Re)link loaded data in Imviz with the desired link type. All existing links will be replaced. Parameters ---------- - link_type : {'pixels', 'wcs'} + align_by : {'pixels', 'wcs'} Choose to link by pixels or WCS. wcs_fallback_scheme : {None, 'pixels'} If WCS linking failed, choose to fall back to linking by pixels or not at all. - This is only used when ``link_type='wcs'``. + This is only used when ``align_by='wcs'``. Choosing `None` may result in some Imviz functionality not working properly. - wcs_use_affine : bool + wcs_fast_approximation : bool Use an affine transform to represent the offset between images if possible (requires that the approximation is accurate to within 1 pixel with the full WCS transformations). If approximation fails, it will automatically - fall back to full WCS transformation. This is only used when ``link_type='wcs'``. + fall back to full WCS transformation. This is only used when ``align_by='wcs'``. Affine approximation is much more performant at the cost of accuracy. """ - from jdaviz.configs.imviz.plugins.orientation.orientation import link_type_msg_to_trait + from jdaviz.configs.imviz.plugins.orientation.orientation import align_by_msg_to_trait plg = self.plugins["Orientation"] plg._obj.wcs_use_fallback = wcs_fallback_scheme == 'pixels' - plg.wcs_use_affine = wcs_use_affine - plg.link_type = link_type_msg_to_trait[link_type] + plg.wcs_fast_approximation = wcs_fast_approximation + plg.align_by = align_by_msg_to_trait[align_by] + @deprecated(since="4.0", alternative="get_alignment_method") def get_link_type(self, data_label_1, data_label_2): + return self.get_alignment_method(data_label_1, data_label_2) + + def get_alignment_method(self, data_label_1, data_label_2): """Find the type of ``glue`` linking between the given data labels. A link is bi-directional. If there are more than 2 data in the collection, one of the given @@ -264,7 +269,7 @@ def get_link_type(self, data_label_1, data_label_2): Returns ------- - link_type : {'pixels', 'wcs', 'self'} + align_by : {'pixels', 'wcs', 'self'} One of the link types accepted by the Orientation plugin or ``'self'`` if the labels are identical. @@ -277,23 +282,23 @@ def get_link_type(self, data_label_1, data_label_2): if data_label_1 == data_label_2: return "self" - link_type = None + align_by = None for elink in self.app.data_collection.external_links: elink_labels = (elink.data1.label, elink.data2.label) if data_label_1 in elink_labels and data_label_2 in elink_labels: if isinstance(elink, LinkSame): # Assumes WCS link never uses LinkSame - link_type = 'pixels' + align_by = 'pixels' else: # If not pixels, must be WCS - link_type = 'wcs' + align_by = 'wcs' break # Might have duplicate, just grab first match - if link_type is None: + if align_by is None: avail_links = [f"({elink.data1.label}, {elink.data2.label})" for elink in self.app.data_collection.external_links] raise ValueError(f'{data_label_1} and {data_label_2} combo not found ' f'in data collection external links: {avail_links}') - return link_type + return align_by def get_aperture_photometry_results(self): """Return aperture photometry results, if any. diff --git a/jdaviz/configs/imviz/plugins/footprints/footprints.py b/jdaviz/configs/imviz/plugins/footprints/footprints.py index 6df321eef6..8e082ae60e 100644 --- a/jdaviz/configs/imviz/plugins/footprints/footprints.py +++ b/jdaviz/configs/imviz/plugins/footprints/footprints.py @@ -180,7 +180,7 @@ def _ensure_sky(region): return '', {path: region} def _on_link_type_updated(self, msg=None): - self.is_pixel_linked = (getattr(self.app, '_link_type', None) == 'pixels' and + self.is_pixel_linked = (getattr(self.app, '_align_by', None) == 'pixels' and len(self.app.data_collection) > 1) # toggle visibility as necessary self._on_is_active_changed() @@ -193,10 +193,10 @@ def _on_link_type_updated(self, msg=None): self._change_overlay(overlay_selected=choice, center_the_overlay=False) def vue_link_by_wcs(self, *args): - # call other plugin so that other options (wcs_use_affine, wcs_use_fallback) + # call other plugin so that other options (wcs_fast_approximation, wcs_use_fallback) # are retained. Remove this method if support for plotting footprints # when pixel-linked is reintroduced. - self.app._jdaviz_helper.plugins['Orientation'].link_type = 'WCS' + self.app._jdaviz_helper.plugins['Orientation'].align_by = 'WCS' def _ensure_first_overlay(self): if not len(self._overlays): diff --git a/jdaviz/configs/imviz/plugins/orientation/orientation.py b/jdaviz/configs/imviz/plugins/orientation/orientation.py index 2f1a36a7e8..5b963f03a3 100644 --- a/jdaviz/configs/imviz/plugins/orientation/orientation.py +++ b/jdaviz/configs/imviz/plugins/orientation/orientation.py @@ -1,4 +1,5 @@ from astropy import units as u +from astropy.utils import deprecated from astropy.wcs.wcsapi import BaseHighLevelWCS from glue.core.link_helpers import LinkSame from glue.core.message import ( @@ -29,7 +30,7 @@ __all__ = ['Orientation'] base_wcs_layer_label = 'Default orientation' -link_type_msg_to_trait = {'pixels': 'Pixels', 'wcs': 'WCS'} +align_by_msg_to_trait = {'pixels': 'Pixels', 'wcs': 'WCS'} @tray_registry('imviz-orientation', label="Orientation", viewer_requirements="image") @@ -50,8 +51,8 @@ class Orientation(PluginTemplateMixin, ViewerSelectMixin): * :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` - * ``link_type`` (`~jdaviz.core.template_mixin.SelectPluginComponent`) - * ``wcs_use_affine`` + * ``align_by`` (`~jdaviz.core.template_mixin.SelectPluginComponent`) + * ``wcs_fast_approximation`` * ``delete_subsets`` * ``viewer`` * ``orientation`` @@ -61,10 +62,10 @@ class Orientation(PluginTemplateMixin, ViewerSelectMixin): """ template_file = __file__, "orientation.vue" - link_type_items = List().tag(sync=True) - link_type_selected = Unicode().tag(sync=True) + align_by_items = List().tag(sync=True) + align_by_selected = Unicode().tag(sync=True) wcs_use_fallback = Bool(True).tag(sync=True) - wcs_use_affine = Bool(True).tag(sync=True) + wcs_fast_approximation = Bool(True).tag(sync=True) wcs_linking_available = Bool(False).tag(sync=True) need_clear_astrowidget_markers = Bool(False).tag(sync=True) @@ -94,10 +95,10 @@ def __init__(self, *args, **kwargs): self.icons = {k: v for k, v in self.app.state.icons.items()} - self.link_type = SelectPluginComponent(self, - items='link_type_items', - selected='link_type_selected', - manual_options=['Pixels', 'WCS']) + self.align_by = SelectPluginComponent(self, + items='align_by_items', + selected='align_by_selected', + manual_options=['Pixels', 'WCS']) self.orientation = LayerSelect( self, 'orientation_layer_items', 'orientation_layer_selected', 'viewer_selected', @@ -141,28 +142,48 @@ def user_api(self): return PluginUserApi( self, expose=( - 'link_type', 'wcs_use_affine', 'delete_subsets', - 'viewer', 'orientation', + 'align_by', 'link_type', 'wcs_fast_approximation', 'wcs_use_affine', + 'delete_subsets', 'viewer', 'orientation', 'rotation_angle', 'east_left', 'add_orientation' ) ) + @property + @deprecated(since="4.0", alternative="align_by") + def link_type(self): + return self.align_by + + @link_type.setter + @deprecated(since="4.0", alternative="align_by") + def link_type(self, link_type): + self.align_by = link_type + + @property + @deprecated(since="4.0", alternative="wcs_fast_approximation") + def wcs_use_affine(self): + return self.wcs_fast_approximation + + @wcs_use_affine.setter + @deprecated(since="4.0", alternative="wcs_fast_approximation") + def wcs_use_affine(self, wcs_use_affine): + self.wcs_fast_approximation = wcs_use_affine + def _link_image_data(self): self.linking_in_progress = True try: - link_type = self.link_type.selected.lower() + align_by = self.align_by.selected.lower() link_image_data( self.app, - link_type=link_type, + align_by=align_by, wcs_fallback_scheme='pixels' if self.wcs_use_fallback else None, - wcs_use_affine=self.wcs_use_affine, + wcs_fast_approximation=self.wcs_fast_approximation, error_on_fail=False) except Exception: # pragma: no cover raise else: # Only broadcast after success. self.app.hub.broadcast(LinkUpdatedMessage( - link_type, self.wcs_use_fallback, self.wcs_use_affine, sender=self.app)) + align_by, self.wcs_use_fallback, self.wcs_fast_approximation, sender=self.app)) finally: self.linking_in_progress = False @@ -193,14 +214,14 @@ def _on_astrowidget_markers_changed(self, msg): def _on_markers_plugin_update(self, msg): self.plugin_markers_exist = msg.table_length > 0 - @observe('link_type_selected', 'wcs_use_fallback', 'wcs_use_affine') + @observe('align_by_selected', 'wcs_use_fallback', 'wcs_fast_approximation') def _update_link(self, msg={}): """Run link_image_data with the selected parameters.""" - if not hasattr(self, 'link_type'): + if not hasattr(self, 'align_by'): # could happen before plugin is fully initialized return - if msg.get('name', None) == 'wcs_use_affine' and self.link_type.selected == 'Pixels': + if msg.get('name', None) == 'wcs_fast_approximation' and self.align_by.selected == 'Pixels': # noqa # approximation doesn't apply, avoid updating when not necessary! return @@ -222,8 +243,8 @@ def _update_link(self, msg={}): raise ValueError(f"cannot change linking with markers present (value reverted to " f"'{msg.get('old')}'), call viewer.reset_markers()") - if self.link_type.selected == 'Pixels': - self.wcs_use_affine = True + if self.align_by.selected == 'Pixels': + self.wcs_fast_approximation = True self.linking_in_progress = False self._link_image_data() @@ -231,7 +252,7 @@ def _update_link(self, msg={}): # load data into the viewer that are now compatible with the # new link type, remove data from the viewer that are now # incompatible: - wcs_linked = self.link_type.selected == 'WCS' + wcs_linked = self.align_by.selected == 'WCS' viewer_selected = self.app.get_viewer(self.viewer.selected) data_in_viewer = self.app.get_viewer(viewer_selected.reference).data() @@ -332,7 +353,7 @@ def add_orientation(self, rotation_angle=None, east_left=None, label=None, def _add_orientation(self, rotation_angle=None, east_left=None, label=None, set_on_create=True, wrt_data=None, from_ui=False): - if self.link_type_selected != 'WCS': + if self.align_by_selected != 'WCS': raise ValueError("must be aligned by WCS to add orientation options") if wrt_data is None: @@ -406,14 +427,14 @@ def _send_wcs_layers_to_all_viewers(self, *args, **kwargs): ) for viewer_ref in viewers_to_update: self.viewer.selected = viewer_ref - self.orientation.update_wcs_only_filter(wcs_only=self.link_type_selected == 'WCS') + self.orientation.update_wcs_only_filter(wcs_only=self.align_by_selected == 'WCS') for wcs_layer in wcs_only_layers: if wcs_layer not in self.viewer.selected_obj.layers: self.app.add_data_to_viewer(viewer_ref, wcs_layer) if ( self.orientation.selected not in self.viewer.selected_obj.state.wcs_only_layers and - self.link_type_selected == 'WCS' + self.align_by_selected == 'WCS' ): self.orientation.selected = base_wcs_layer_label @@ -448,11 +469,11 @@ def _on_refdata_change(self, msg): # don't select until reference data are available: if ref_data is not None: - link_type = viewer.get_link_type(ref_data.label) - if link_type != 'self': - self.link_type_selected = link_type_msg_to_trait[link_type] + align_by = viewer.get_alignment_method(ref_data.label) + if align_by != 'self': + self.align_by_selected = align_by_msg_to_trait[align_by] elif not len(viewer.data()): - self.link_type_selected = link_type_msg_to_trait['pixels'] + self.align_by_selected = align_by_msg_to_trait['pixels'] if msg.data.label not in self.orientation.choices: return @@ -545,7 +566,7 @@ def _update_layer_label_default(self, event={}): ) -def link_image_data(app, link_type='pixels', wcs_fallback_scheme=None, wcs_use_affine=True, +def link_image_data(app, align_by='pixels', wcs_fallback_scheme=None, wcs_fast_approximation=True, error_on_fail=False): """(Re)link loaded data in Imviz with the desired link type. @@ -561,19 +582,19 @@ def link_image_data(app, link_type='pixels', wcs_fallback_scheme=None, wcs_use_a app : `~jdaviz.app.Application` Application associated with Imviz, e.g., ``imviz.app``. - link_type : {'pixels', 'wcs'} + align_by : {'pixels', 'wcs'} Choose to link by pixels or WCS. wcs_fallback_scheme : {None, 'pixels'} If WCS linking failed, choose to fall back to linking by pixels or not at all. - This is only used when ``link_type='wcs'``. + This is only used when ``align_by='wcs'``. Choosing `None` may result in some Imviz functionality not working properly. - wcs_use_affine : bool + wcs_fast_approximation : bool Use an affine transform to represent the offset between images if possible (requires that the approximation is accurate to within 1 pixel with the full WCS transformations). If approximation fails, it will automatically - fall back to full WCS transformation. This is only used when ``link_type='wcs'``. + fall back to full WCS transformation. This is only used when ``align_by='wcs'``. Affine approximation is much more performant at the cost of accuracy. error_on_fail : bool @@ -588,15 +609,15 @@ def link_image_data(app, link_type='pixels', wcs_fallback_scheme=None, wcs_use_a Invalid inputs or reference data. """ - if len(app.data_collection) <= 1 and link_type != 'wcs': # No need to link, we are done. + if len(app.data_collection) <= 1 and align_by != 'wcs': # No need to link, we are done. return - if link_type not in ('pixels', 'wcs'): # pragma: no cover - raise ValueError(f"link_type must be 'pixels' or 'wcs', got {link_type}") - if link_type == 'wcs' and wcs_fallback_scheme not in (None, 'pixels'): # pragma: no cover + if align_by not in ('pixels', 'wcs'): # pragma: no cover + raise ValueError(f"align_by must be 'pixels' or 'wcs', got {align_by}") + if align_by == 'wcs' and wcs_fallback_scheme not in (None, 'pixels'): # pragma: no cover raise ValueError("wcs_fallback_scheme must be None or 'pixels', " f"got {wcs_fallback_scheme}") - if link_type == 'wcs': + if align_by == 'wcs': at_least_one_data_have_wcs = len([ hasattr(d, 'coords') and isinstance(d.coords, BaseHighLevelWCS) for d in app.data_collection @@ -604,15 +625,15 @@ def link_image_data(app, link_type='pixels', wcs_fallback_scheme=None, wcs_use_a if not at_least_one_data_have_wcs: # pragma: no cover if wcs_fallback_scheme is None: if error_on_fail: - raise ValueError("link_type can only be 'wcs' when wcs_fallback_scheme " + raise ValueError("align_by can only be 'wcs' when wcs_fallback_scheme " "is 'None' if at least one image has a valid WCS.") else: return else: # fall back on pixel linking - link_type = 'pixels' + align_by = 'pixels' - old_link_type = getattr(app, '_link_type', None) + old_align_by = getattr(app, '_align_by', None) # In WCS linking, changing orientation layer is done within Orientation plugin, # so here we assume viewer.state.reference_data is already the desired @@ -622,8 +643,8 @@ def link_image_data(app, link_type='pixels', wcs_fallback_scheme=None, wcs_use_a # # data1 = reference, data2 = actual data data_already_linked = [] - if (link_type == old_link_type and - (link_type == "pixels" or wcs_use_affine == app._wcs_use_affine)): + if (align_by == old_align_by and + (align_by == "pixels" or wcs_fast_approximation == app._wcs_fast_approximation)): # We are only here to link new data with existing configuration, # so no need to relink existing data. for link in app.data_collection.external_links: @@ -632,17 +653,17 @@ def link_image_data(app, link_type='pixels', wcs_fallback_scheme=None, wcs_use_a # Everything has to be relinked. for viewer in app._viewer_store.values(): if len(viewer._marktags): - raise ValueError(f"cannot change link_type (from '{app._link_type}' to " - f"'{link_type}') when markers are present. " + raise ValueError(f"cannot change align_by (from '{app._align_by}' to " + f"'{align_by}') when markers are present. " f" Clear markers with viewer.reset_markers() first") - # set internal tracking of link_type before changing reference data for anything that is + # set internal tracking of align_by before changing reference data for anything that is # subscribed to a change in reference data - app._link_type = link_type - app._wcs_use_affine = wcs_use_affine + app._align_by = align_by + app._wcs_fast_approximation = wcs_fast_approximation # wcs -> pixels: First loaded real data will be reference. - if link_type == 'pixels' and old_link_type == 'wcs': + if align_by == 'pixels' and old_align_by == 'wcs': # default reference layer is the first-loaded image in default viewer: refdata = app._jdaviz_helper.default_viewer._obj.first_loaded_data if refdata is None: # No data in viewer, just use first in collection # pragma: no cover @@ -656,7 +677,7 @@ def link_image_data(app, link_type='pixels', wcs_fallback_scheme=None, wcs_use_a app._change_reference_data(refdata.label, viewer_id=viewer_id) # pixels -> wcs: Always the default orientation - elif link_type == 'wcs' and old_link_type == 'pixels': + elif align_by == 'wcs' and old_align_by == 'pixels': # Have to create the default orientation first. if base_wcs_layer_label not in app.data_collection.labels: default_reference_layer = (app._jdaviz_helper.default_viewer._obj.first_loaded_data @@ -699,18 +720,18 @@ def link_image_data(app, link_type='pixels', wcs_fallback_scheme=None, wcs_use_a # 4. We are not touching fake WCS layers in pixel linking. # 5. We are not touching data without WCS in WCS linking. if ((i == iref) or (not layer_is_2d(data)) or (data in data_already_linked) or - (link_type == "pixels" and data.meta.get(_wcs_only_label)) or - (link_type == "wcs" and not hasattr(data.coords, 'pixel_to_world'))): + (align_by == "pixels" and data.meta.get(_wcs_only_label)) or + (align_by == "wcs" and not hasattr(data.coords, 'pixel_to_world'))): continue ids1 = data.pixel_component_ids new_links = [] try: - if link_type == 'pixels': + if align_by == 'pixels': new_links = [LinkSame(ids0[i], ids1[i]) for i in ndim_range] else: # wcs wcslink = WCSLink(data1=refdata, data2=data, cids1=ids0, cids2=ids1) - if wcs_use_affine: + if wcs_fast_approximation: try: new_links = [wcslink.as_affine_link()] except NoAffineApproximation: # pragma: no cover @@ -718,7 +739,7 @@ def link_image_data(app, link_type='pixels', wcs_fallback_scheme=None, wcs_use_a else: new_links = [wcslink] except Exception as e: # pragma: no cover - if link_type == 'wcs' and wcs_fallback_scheme == 'pixels': + if align_by == 'wcs' and wcs_fallback_scheme == 'pixels': try: new_links = [LinkSame(ids0[i], ids1[i]) for i in ndim_range] except Exception as e: # pragma: no cover @@ -750,7 +771,7 @@ def link_image_data(app, link_type='pixels', wcs_fallback_scheme=None, wcs_use_a 'Images successfully relinked', color='success', timeout=8000, sender=app)) for viewer in app._viewer_store.values(): - wcs_linked = link_type == 'wcs' + wcs_linked = align_by == 'wcs' # viewer-state needs to know link type for reset_limits behavior viewer.state.linked_by_wcs = wcs_linked # also need to store a copy in the viewer item for the data dropdown to access @@ -760,5 +781,5 @@ def link_image_data(app, link_type='pixels', wcs_fallback_scheme=None, wcs_use_a viewer_item['linked_by_wcs'] = wcs_linked # if changing from one link type to another, reset the limits: - if link_type != old_link_type: + if align_by != old_align_by: viewer.state.reset_limits() diff --git a/jdaviz/configs/imviz/plugins/orientation/orientation.vue b/jdaviz/configs/imviz/plugins/orientation/orientation.vue index 7299148f3d..dc3aa251e0 100644 --- a/jdaviz/configs/imviz/plugins/orientation/orientation.vue +++ b/jdaviz/configs/imviz/plugins/orientation/orientation.vue @@ -40,13 +40,13 @@ @@ -73,7 +73,7 @@ -
+
Orientation
Date: Thu, 8 Aug 2024 11:08:40 -0400 Subject: [PATCH 010/127] remove magnitude units from display units list --- jdaviz/app.py | 3 +-- jdaviz/core/validunits.py | 7 +++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/jdaviz/app.py b/jdaviz/app.py index 47f7a9646d..546a7d48ef 100644 --- a/jdaviz/app.py +++ b/jdaviz/app.py @@ -80,14 +80,13 @@ def equivalent_units(self, data, cid, units): '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)', 'bol', 'AB', 'ST' + 'ph / (Hz 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)', - 'AB / sr' ]) else: # spectral axis # prefer Hz over Bq and um over micron diff --git a/jdaviz/core/validunits.py b/jdaviz/core/validunits.py index 8ba718bff4..413b26c3c0 100644 --- a/jdaviz/core/validunits.py +++ b/jdaviz/core/validunits.py @@ -67,6 +67,10 @@ def create_flux_equivalencies_list(flux_unit, spectral_axis_unit): except u.core.UnitConversionError: return [] + mag_units = ['bol', 'AB', 'ST'] + # remove magnitude units from list + curr_flux_unit_equivalencies = [unit for unit in curr_flux_unit_equivalencies if not any(mag in unit.name for mag in mag_units)] # noqa + # Get local flux units. locally_defined_flux_units = ['Jy', 'mJy', 'uJy', 'MJy', 'W / (Hz m2)', @@ -75,13 +79,12 @@ def create_flux_equivalencies_list(flux_unit, spectral_axis_unit): 'erg / (s cm2 Angstrom)', 'ph / (Angstrom s cm2)', 'ph / (Hz s cm2)', - 'bol', 'AB', 'ST' ] 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) - - set(local_units)) + - set(local_units) - set(mag_units)) # Convert equivalencies into readable versions of the units and sort them alphabetically. flux_unit_equivalencies_titles = sorted(units_to_strings(curr_flux_unit_equivalencies)) From 7254756800613c36a0b702201ee8cd3e3f168730 Mon Sep 17 00:00:00 2001 From: gibsongreen Date: Thu, 8 Aug 2024 11:12:19 -0400 Subject: [PATCH 011/127] remove set subtraction line --- jdaviz/core/validunits.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jdaviz/core/validunits.py b/jdaviz/core/validunits.py index 413b26c3c0..8aad68ac63 100644 --- a/jdaviz/core/validunits.py +++ b/jdaviz/core/validunits.py @@ -84,7 +84,7 @@ def create_flux_equivalencies_list(flux_unit, spectral_axis_unit): # Remove overlap units. curr_flux_unit_equivalencies = list(set(curr_flux_unit_equivalencies) - - set(local_units) - set(mag_units)) + - set(local_units)) # Convert equivalencies into readable versions of the units and sort them alphabetically. flux_unit_equivalencies_titles = sorted(units_to_strings(curr_flux_unit_equivalencies)) From 74a107297ca262ec9145ad8a6d2d73823b9b8232 Mon Sep 17 00:00:00 2001 From: Clare Shanahan Date: Thu, 8 Aug 2024 11:41:39 -0400 Subject: [PATCH 012/127] observe display traitlets separately in unit conversion, remove redundant GlobalDisplayUnitChange broadcast (#3138) * observe display traitlets separately * . * remove redundant broadcast * code style --- .../imviz/plugins/coords_info/coords_info.py | 3 +- .../unit_conversion/unit_conversion.py | 89 +++++++++++-------- 2 files changed, 55 insertions(+), 37 deletions(-) diff --git a/jdaviz/configs/imviz/plugins/coords_info/coords_info.py b/jdaviz/configs/imviz/plugins/coords_info/coords_info.py index 26b8b75b91..b5600b2f23 100644 --- a/jdaviz/configs/imviz/plugins/coords_info/coords_info.py +++ b/jdaviz/configs/imviz/plugins/coords_info/coords_info.py @@ -120,7 +120,8 @@ def _on_viewer_added(self, msg): self._create_viewer_callbacks(self.app.get_viewer_by_id(msg.viewer_id)) def _on_global_display_unit_changed(self, msg): - if msg.axis == "sb": + # eventually should observe change in flux OR angle + if msg.axis == "flux": self.image_unit = u.Unit(msg.unit) @property diff --git a/jdaviz/configs/specviz/plugins/unit_conversion/unit_conversion.py b/jdaviz/configs/specviz/plugins/unit_conversion/unit_conversion.py index bff50f481f..5ac7778f74 100644 --- a/jdaviz/configs/specviz/plugins/unit_conversion/unit_conversion.py +++ b/jdaviz/configs/specviz/plugins/unit_conversion/unit_conversion.py @@ -179,9 +179,6 @@ def _on_glue_y_display_unit_changed(self, y_unit_str): self.flux_unit.selected, self.angle_unit.selected ) - self.hub.broadcast(GlobalDisplayUnitChanged('sb', - self.sb_unit_selected, - sender=self)) if not self.flux_unit.selected: y_display_unit = self.spectrum_viewer.state.y_display_unit @@ -196,9 +193,34 @@ def _on_spectral_unit_changed(self, *args): self.spectral_unit.selected, sender=self)) - @observe('flux_or_sb_selected', 'flux_unit_selected') + @observe('flux_or_sb_selected') + def _on_flux_or_sb_selected(self, msg): + """ + Observes toggle between surface brightness or flux selection for + spectrum viewer to trigger translation. + """ + + if msg.get('name') == 'flux_or_sb_selected': + self._translate(self.flux_or_sb_selected) + + @observe('flux_unit_selected') def _on_flux_unit_changed(self, msg): - # may need to be updated if translations in other configs going to be supported + + """ + Observes changes in selected flux unit. + + When the selected flux unit changes, a GlobalDisplayUnitChange needs + to be broadcasted indicating that the flux unit has changed. + + Note: The 'axis' of the broadcast should always be 'flux', even though a + change in flux unit indicates a change in surface brightness unit, because + SB is read only, so anything observing for changes in surface brightness + should be looking for a change in 'flux' (as well as angle). + """ + + if msg.get('name') != 'flux_unit_selected': + # not sure when this would be encountered but keeping as a safeguard + return if not hasattr(self, 'flux_unit'): return if not self.flux_unit.choices and self.app.config == 'cubeviz': @@ -206,44 +228,39 @@ def _on_flux_unit_changed(self, msg): flux_or_sb = None - name = msg.get('name') - # determine if flux or surface brightness unit was changed by user - if name == 'flux_unit_selected': - # when the configuration is Specviz, translation is not currently supported. - # If in Cubeviz, all spectra pass through Spectral Extraction plugin and will - # have a scale factor assigned in the metadata, enabling translation. - current_y_unit = self.spectrum_viewer.state.y_display_unit - if self.angle_unit.selected and check_if_unit_is_per_solid_angle(current_y_unit): - flux_or_sb = self._append_angle_correctly( - self.flux_unit.selected, - self.angle_unit.selected - ) - else: - flux_or_sb = self.flux_unit.selected - untranslatable_units = self._untranslatable_units - # disable translator if flux unit is untranslatable, - # still can convert flux units, this just disables flux - # to surface brightnes translation for units in list. - if flux_or_sb in untranslatable_units: - self.can_translate = False - else: - self.can_translate = True - elif name == 'flux_or_sb_selected': - self._translate(self.flux_or_sb_selected) - return + # when the configuration is Specviz, translation is not currently supported. + # If in Cubeviz, all spectra pass through Spectral Extraction plugin and will + # have a scale factor assigned in the metadata, enabling translation. + current_y_unit = self.spectrum_viewer.state.y_display_unit + + # if the current y display unit is a surface brightness unit, + if self.angle_unit.selected and check_if_unit_is_per_solid_angle(current_y_unit): + flux_or_sb = self._append_angle_correctly( + self.flux_unit.selected, + self.angle_unit.selected + ) else: - return + flux_or_sb = self.flux_unit.selected + + 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 flux_or_sb in untranslatable_units: + self.can_translate = False + else: + self.can_translate = True yunit = _valid_glue_display_unit(flux_or_sb, self.spectrum_viewer, 'y') + # update spectrum viewer with new y display unit if self.spectrum_viewer.state.y_display_unit != yunit: self.spectrum_viewer.state.y_display_unit = yunit self.spectrum_viewer.reset_limits() - self.hub.broadcast( - GlobalDisplayUnitChanged( - "flux" if name == "flux_unit_selected" else "sb", flux_or_sb, sender=self - ) - ) + + # and broacast that there has been a change in flux + self.hub.broadcast(GlobalDisplayUnitChanged("flux", flux_or_sb, sender=self)) + if not check_if_unit_is_per_solid_angle(self.spectrum_viewer.state.y_display_unit): self.flux_or_sb_selected = 'Flux' else: From b9b4b1a3b1b0991dc687353cee6fdfd02bc05c67 Mon Sep 17 00:00:00 2001 From: Ricky O'Steen <39831871+rosteen@users.noreply.github.com> Date: Thu, 8 Aug 2024 12:24:45 -0400 Subject: [PATCH 013/127] Debug cubeviz spectrum1d parser (#3133) * Remove incorrect moveaxis call in spectrum1d_3d parser * Working on correcting expected test results * Continuing to work on tests * Update spectral extraction tests * Update aperphot test * Changelog * Make one cubeviz mouseover test sensitive to axis swap * Update comment * Update jdaviz/configs/cubeviz/plugins/tests/test_parsers.py --- CHANGES.rst | 2 ++ .../cubeviz/plugins/moment_maps/tests/test_moment_maps.py | 2 +- jdaviz/configs/cubeviz/plugins/parsers.py | 2 -- .../spectral_extraction/tests/test_spectral_extraction.py | 8 ++++---- .../cubeviz/plugins/tests/test_cubeviz_aperphot.py | 8 ++++---- .../configs/cubeviz/plugins/tests/test_cubeviz_helper.py | 4 ++-- jdaviz/configs/cubeviz/plugins/tests/test_parsers.py | 2 +- jdaviz/configs/cubeviz/plugins/tests/test_tools.py | 8 ++++---- .../plugins/gaussian_smooth/tests/test_gaussian_smooth.py | 4 ++-- .../default/plugins/model_fitting/tests/test_fitting.py | 4 ++-- .../default/plugins/model_fitting/tests/test_plugin.py | 4 ++-- jdaviz/conftest.py | 4 ++-- 12 files changed, 26 insertions(+), 26 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 270d17759c..9aa1967a77 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -216,6 +216,8 @@ Bug Fixes Cubeviz ^^^^^^^ +- No longer incorrectly swap RA and Dec axes when loading Spectrum1D objects. [#3133] + Imviz ^^^^^ diff --git a/jdaviz/configs/cubeviz/plugins/moment_maps/tests/test_moment_maps.py b/jdaviz/configs/cubeviz/plugins/moment_maps/tests/test_moment_maps.py index 81727d67b0..859ec48b37 100644 --- a/jdaviz/configs/cubeviz/plugins/moment_maps/tests/test_moment_maps.py +++ b/jdaviz/configs/cubeviz/plugins/moment_maps/tests/test_moment_maps.py @@ -109,7 +109,7 @@ def test_moment_calculation(cubeviz_helper, spectrum1d_cube, tmp_path): ) result = dc[-1].get_object(cls=CCDData) - assert result.shape == (4, 2) # Cube shape is (2, 2, 4) + assert result.shape == (2, 4) # Cube shape is (2, 2, 4), moment transposes assert isinstance(dc[-1].coords, WCS) # Make sure coordinate display now show moment map info (no WCS) diff --git a/jdaviz/configs/cubeviz/plugins/parsers.py b/jdaviz/configs/cubeviz/plugins/parsers.py index 9bbc932865..fcdeb44f1c 100644 --- a/jdaviz/configs/cubeviz/plugins/parsers.py +++ b/jdaviz/configs/cubeviz/plugins/parsers.py @@ -430,8 +430,6 @@ def _parse_spectrum1d_3d(app, file_obj, data_label=None, else: flux = val - flux = np.moveaxis(flux, 1, 0) - with warnings.catch_warnings(): warnings.filterwarnings( 'ignore', message='Input WCS indicates that the spectral axis is not last', 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 4d6d9a96b3..295432051e 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 @@ -496,8 +496,8 @@ def test_extraction_composite_subset(cubeviz_helper, spectrum1d_cube): subset_plugin = cubeviz_helper.plugins['Subset Tools']._obj spec_extr_plugin = cubeviz_helper.plugins['Spectral Extraction']._obj - lower_aperture = RectangularROI(-0.5, 1.5, -0.5, 0.5) - upper_aperture = RectangularROI(-0.5, 1.5, 2.5, 3.5) + lower_aperture = RectangularROI(-0.5, 0.5, -0.5, 1.5) + upper_aperture = RectangularROI(2.5, 3.5, -0.5, 1.5) flux_viewer.toolbar.active_tool = flux_viewer.toolbar.tools['bqplot:rectangle'] flux_viewer.apply_roi(lower_aperture) @@ -512,14 +512,14 @@ def test_extraction_composite_subset(cubeviz_helper, spectrum1d_cube): spectrum_2 = spec_extr_plugin.extract() subset_plugin.subset_selected = 'Create New' - rectangle = RectangularROI(-0.5, 1.5, -0.5, 3.5) + rectangle = RectangularROI(-0.5, 3.5, -0.5, 1.5) flux_viewer.toolbar.active_tool = flux_viewer.toolbar.tools['bqplot:rectangle'] flux_viewer.apply_roi(rectangle) flux_viewer.toolbar.active_tool = flux_viewer.toolbar.tools['bqplot:truecircle'] subset_plugin.subset_selected = 'Subset 3' cubeviz_helper.app.session.edit_subset_mode.mode = AndNotMode - circle = CircularROI(0.5, 1.5, 1.1) + circle = CircularROI(1.5, 0.5, 1.1) flux_viewer.apply_roi(circle) spec_extr_plugin.aperture_selected = 'Subset 3' diff --git a/jdaviz/configs/cubeviz/plugins/tests/test_cubeviz_aperphot.py b/jdaviz/configs/cubeviz/plugins/tests/test_cubeviz_aperphot.py index 20f31702d5..9d922f6a57 100644 --- a/jdaviz/configs/cubeviz/plugins/tests/test_cubeviz_aperphot.py +++ b/jdaviz/configs/cubeviz/plugins/tests/test_cubeviz_aperphot.py @@ -166,8 +166,8 @@ def test_cubeviz_aperphot_cube_orig_flux_mjysr(cubeviz_helper, spectrum1d_cube_c cube = spectrum1d_cube_custom_fluxunit(fluxunit=u.MJy / u.sr) cubeviz_helper.load_data(cube, data_label="test") - aper = RectanglePixelRegion(center=PixCoord(x=1, y=3), width=1, height=1) - bg = RectanglePixelRegion(center=PixCoord(x=0, y=2), width=1, height=1) + aper = RectanglePixelRegion(center=PixCoord(x=3, y=1), width=1, height=1) + bg = RectanglePixelRegion(center=PixCoord(x=2, y=0), width=1, height=1) cubeviz_helper.load_regions([aper, bg]) plg = cubeviz_helper.plugins["Aperture Photometry"]._obj @@ -183,8 +183,8 @@ def test_cubeviz_aperphot_cube_orig_flux_mjysr(cubeviz_helper, spectrum1d_cube_c row = cubeviz_helper.get_aperture_photometry_results()[0] # Basically, we should recover the input rectangle here, minus background. - assert_allclose(row["xcenter"], 1 * u.pix) - assert_allclose(row["ycenter"], 3 * u.pix) + assert_allclose(row["xcenter"], 3 * u.pix) + assert_allclose(row["ycenter"], 1 * u.pix) assert_allclose(row["sum"], 1.1752215e-12 * u.MJy) # (15 - 10) MJy/sr x 2.3504431e-13 sr assert_allclose(row["sum_aper_area"], 1 * (u.pix * u.pix)) assert_allclose(row["pixarea_tot"], 2.350443053909789e-13 * u.sr) diff --git a/jdaviz/configs/cubeviz/plugins/tests/test_cubeviz_helper.py b/jdaviz/configs/cubeviz/plugins/tests/test_cubeviz_helper.py index ecc66c4ca3..e5e90b881c 100644 --- a/jdaviz/configs/cubeviz/plugins/tests/test_cubeviz_helper.py +++ b/jdaviz/configs/cubeviz/plugins/tests/test_cubeviz_helper.py @@ -51,5 +51,5 @@ def test_get_data_spatial_and_spectral(cubeviz_helper, spectrum1d_cube_larger): assert spatial_with_spec.flux.ndim == 1 assert list(spatial_with_spec.mask) == [True, True, False, False, True, True, True, True, True, True] - assert max(list(spatial_with_spec.flux.value)) == 157. - assert min(list(spatial_with_spec.flux.value)) == 13. + assert max(list(spatial_with_spec.flux.value)) == 232. + assert min(list(spatial_with_spec.flux.value)) == 16. diff --git a/jdaviz/configs/cubeviz/plugins/tests/test_parsers.py b/jdaviz/configs/cubeviz/plugins/tests/test_parsers.py index f70415c392..8946cc283e 100644 --- a/jdaviz/configs/cubeviz/plugins/tests/test_parsers.py +++ b/jdaviz/configs/cubeviz/plugins/tests/test_parsers.py @@ -149,7 +149,7 @@ def test_spectrum3d_no_wcs_parse(cubeviz_helper): data = cubeviz_helper.app.data_collection[0] flux = data.get_component('flux') assert data.label.endswith('[FLUX]') - assert data.shape == (3, 2, 4) # y, x, z + assert data.shape == (2, 3, 4) # x, y, z assert isinstance(data.coords, PaddedSpectrumWCS) assert_array_equal(flux.data, 1) assert flux.units == 'nJy' diff --git a/jdaviz/configs/cubeviz/plugins/tests/test_tools.py b/jdaviz/configs/cubeviz/plugins/tests/test_tools.py index 7c1129ff59..8f08d55319 100644 --- a/jdaviz/configs/cubeviz/plugins/tests/test_tools.py +++ b/jdaviz/configs/cubeviz/plugins/tests/test_tools.py @@ -87,10 +87,10 @@ def test_spectrum_at_spaxel_altkey_true(cubeviz_helper, spectrum1d_cube): assert flux_viewer.slice == 1 label_mouseover = cubeviz_helper.app.session.application._tools['g-coords-info'] label_mouseover._viewer_mouse_event(flux_viewer, - {'event': 'mousemove', 'domain': {'x': 1, 'y': 1}}) - assert label_mouseover.as_text() == ('Pixel x=01.0 y=01.0 Value +1.30000e+01 Jy', - 'World 13h39m59.9461s +27d00m00.7200s (ICRS)', - '204.9997755344 27.0001999998 (deg)') + {'event': 'mousemove', 'domain': {'x': 2, 'y': 1}}) + assert label_mouseover.as_text() == ('Pixel x=02.0 y=01.0 Value +1.40000e+01 Jy', + 'World 13h39m59.9192s +27d00m00.7200s (ICRS)', + '204.9996633015 27.0001999996 (deg)') # Click on spaxel location x = 1 diff --git a/jdaviz/configs/default/plugins/gaussian_smooth/tests/test_gaussian_smooth.py b/jdaviz/configs/default/plugins/gaussian_smooth/tests/test_gaussian_smooth.py index c60e5ca2b9..32dbffa23a 100644 --- a/jdaviz/configs/default/plugins/gaussian_smooth/tests/test_gaussian_smooth.py +++ b/jdaviz/configs/default/plugins/gaussian_smooth/tests/test_gaussian_smooth.py @@ -121,10 +121,10 @@ def test_spatial_convolution(cubeviz_helper, spectrum1d_cube): assert len(dc) == 3 assert dc[-1].label == f'{data_label}[FLUX] spatial-smooth stddev-3.0' - assert dc[-1].shape == (2, 4, 2) # specutils moved spectral axis to last + assert dc[-1].shape == (4, 2, 2) # specutils moved spectral axis to last assert (dc[f'{data_label}[FLUX] spatial-smooth stddev-3.0'].get_object(cls=Spectrum1D, statistic=None).shape - == (2, 4, 2)) + == (4, 2, 2)) def test_specviz_smooth(specviz_helper, spectrum1d): diff --git a/jdaviz/configs/default/plugins/model_fitting/tests/test_fitting.py b/jdaviz/configs/default/plugins/model_fitting/tests/test_fitting.py index 49f2d0715d..cbddd12c59 100644 --- a/jdaviz/configs/default/plugins/model_fitting/tests/test_fitting.py +++ b/jdaviz/configs/default/plugins/model_fitting/tests/test_fitting.py @@ -88,10 +88,10 @@ def test_parameter_retrieval(cubeviz_helper, spectral_cube_wcs): plugin.calculate_fit() params = cubeviz_helper.get_model_parameters() - slope_res = np.zeros((4, 3)) + slope_res = np.zeros((3, 4)) slope_res[2, 2] = 1.0 slope_res = slope_res * u.nJy / u.Hz - intercept_res = np.ones((4, 3)) + intercept_res = np.ones((3, 4)) intercept_res[2, 2] = 0 intercept_res = intercept_res * u.nJy assert_quantity_allclose(params['model']['slope'], slope_res, diff --git a/jdaviz/configs/default/plugins/model_fitting/tests/test_plugin.py b/jdaviz/configs/default/plugins/model_fitting/tests/test_plugin.py index 92304ea52b..e5f95611bf 100644 --- a/jdaviz/configs/default/plugins/model_fitting/tests/test_plugin.py +++ b/jdaviz/configs/default/plugins/model_fitting/tests/test_plugin.py @@ -158,7 +158,7 @@ def test_register_cube_model(cubeviz_helper, spectrum1d_cube): def test_fit_cube_no_wcs(cubeviz_helper): # This is like when user do something to a cube outside of Jdaviz # and then load it back into a new instance of Cubeviz for further analysis. - sp = Spectrum1D(flux=np.ones((7, 8, 9)) * u.nJy) # ny, nx, nz + sp = Spectrum1D(flux=np.ones((7, 8, 9)) * u.nJy) # nx, ny, nz cubeviz_helper.load_data(sp, data_label="test_cube") mf = cubeviz_helper.plugins['Model Fitting'] mf.create_model_component('Linear1D') @@ -169,7 +169,7 @@ def test_fit_cube_no_wcs(cubeviz_helper): assert len(fitted_model) == 56 # ny * nx # Make sure shapes are all self-consistent within Cubeviz instance. fitted_data = cubeviz_helper.app.data_collection["model"] - assert fitted_data.shape == (8, 7, 9) # nx, ny, nz + assert fitted_data.shape == (7, 8, 9) # nx, ny, nz assert fitted_data.shape == cubeviz_helper.app.data_collection[0].shape assert fitted_data.shape == output_cube.shape diff --git a/jdaviz/conftest.py b/jdaviz/conftest.py index fb8b1b9a68..8a62132da2 100644 --- a/jdaviz/conftest.py +++ b/jdaviz/conftest.py @@ -246,8 +246,8 @@ def spectrum1d_cube_largest(): "CDELT1": 8e-11, "CDELT2": 0.0001, "CDELT3": -0.0001, "CRPIX1": 0, "CRPIX2": 0, "CRPIX3": 0} w = WCS(wcs_dict) - flux = np.zeros((30, 20, 3001), dtype=np.float32) # nx=20 ny=30 nz=3001 - flux[5:15, 1:11, :] = 1 # Bright corner + flux = np.zeros((20, 30, 3001), dtype=np.float32) # nx=20 ny=30 nz=3001 + flux[1:11, 5:15, :] = 1 # Bright corner return Spectrum1D(flux=flux * u.Jy, wcs=w, meta=wcs_dict) From b06fddc1dfadd19c280de07028039bef9ac561cd Mon Sep 17 00:00:00 2001 From: Ricky O'Steen Date: Thu, 8 Aug 2024 13:37:47 -0400 Subject: [PATCH 014/127] Move to 4.0 section --- CHANGES.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 9aa1967a77..713480767f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -125,6 +125,8 @@ Cubeviz - Fixed spectral axis value display in Markers plugin. Previously, it failed to display very small values, resulting in zeroes. [#3119] +- No longer incorrectly swap RA and Dec axes when loading Spectrum1D objects. [#3133] + Imviz ^^^^^ @@ -216,8 +218,6 @@ Bug Fixes Cubeviz ^^^^^^^ -- No longer incorrectly swap RA and Dec axes when loading Spectrum1D objects. [#3133] - Imviz ^^^^^ From 6009e6632cd349373af58562bbd936f80e06ef2d Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Thu, 8 Aug 2024 15:08:56 -0400 Subject: [PATCH 015/127] BUG: Remove snackbar message because it is spamming the app --- .../plugins/spectral_extraction/spectral_extraction.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py index 809fe08e07..51314c31c6 100644 --- a/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py +++ b/jdaviz/configs/cubeviz/plugins/spectral_extraction/spectral_extraction.py @@ -546,15 +546,6 @@ def extract(self, return_bg=False, add_data=True, **kwargs): pix_scale_factor = self.cube.meta.get('PIXAR_SR', 1.0) spec.meta['_pixel_scale_factor'] = pix_scale_factor - # inform the user if scale factor keyword not in metadata - if 'PIXAR_SR' not in self.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.extraction_available = True From 7826a998d8447d6835b6598aee4d08b0b0997f33 Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Thu, 8 Aug 2024 17:29:05 -0400 Subject: [PATCH 016/127] BUG: Fix get_subsets when XOR is involved (#3124) * TST: Modify test to expose bug in main * Fix the bug and add more tests * Maybe should not backport * Fix XOR logic flaw and add more tests. Co-authored-by: Jesse Averbukh * Simplify lo/hi logic Co-authored-by: Brett M. Morris --------- Co-authored-by: Jesse Averbukh Co-authored-by: Brett M. Morris --- CHANGES.rst | 2 + jdaviz/app.py | 132 +++++++++++------- .../plugins/subset_plugin/subset_plugin.py | 8 +- jdaviz/tests/test_subsets.py | 86 ++++++++++-- 4 files changed, 158 insertions(+), 70 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 713480767f..b2f5d5668e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -45,6 +45,8 @@ Mosviz Specviz ^^^^^^^ +- Fixed ``viz.app.get_subsets()`` for XOR mode. [#3124] + Specviz2d ^^^^^^^^^ diff --git a/jdaviz/app.py b/jdaviz/app.py index 546a7d48ef..018807d00b 100644 --- a/jdaviz/app.py +++ b/jdaviz/app.py @@ -1102,6 +1102,11 @@ def get_sub_regions(self, subset_state, simplify_spectral=True, two = self.get_sub_regions(subset_state.state2, simplify_spectral, use_display_units, get_sky_regions=get_sky_regions) + if simplify_spectral and isinstance(two, SpectralRegion): + merge_func = self._merge_overlapping_spectral_regions_worker + else: + def merge_func(spectral_region): # noop + return spectral_region if isinstance(one, list) and "glue_state" in one[0]: one[0]["glue_state"] = subset_state.__class__.__name__ @@ -1135,9 +1140,7 @@ def get_sub_regions(self, subset_state, simplify_spectral=True, else: if isinstance(two, list): two[0]['glue_state'] = "AndNotState" - # Return two first so that we preserve the chronology of how - # subset regions are applied. - return one + two + return merge_func(one + two) elif subset_state.op is operator.and_: # This covers the AND subset mode @@ -1163,18 +1166,18 @@ def get_sub_regions(self, subset_state, simplify_spectral=True, return oppo.invert(one.lower, one.upper) else: - return two + one + return merge_func(two + one) elif subset_state.op is operator.or_: # This covers the ADD subset mode # one + two works for both Range and ROI subsets if one and two: - return two + one + return merge_func(two + one) elif one: return one elif two: return two elif subset_state.op is operator.xor: - # This covers the ADD subset mode + # This covers the XOR subset mode # Example of how this works, with "one" acting # as the XOR region and "two" as two ranges joined @@ -1198,34 +1201,55 @@ def get_sub_regions(self, subset_state, simplify_spectral=True, # (4.0 um, 4.5 um) (5.0 um, 6.0 um) (9.0 um, 12.0 um) if isinstance(two, SpectralRegion): + # This is the main application of XOR to other regions + if one.lower > two.lower and one.upper < two.upper: + if len(two) < 2: + inverted_region = one.invert(two.lower, two.upper) + else: + two_2 = None + for subregion in two: + temp_region = None + # No overlap + if subregion.lower > one.upper or subregion.upper < one.lower: + continue + temp_lo = max(subregion.lower, one.lower) + temp_hi = min(subregion.upper, one.upper) + temp_region = SpectralRegion(temp_lo, temp_hi) + if two_2: + two_2 += temp_region + else: + two_2 = temp_region + inverted_region = two_2.invert(one.lower, one.upper) + else: + inverted_region = two.invert(one.lower, one.upper) + new_region = None - temp_region = None for subregion in two: + temp_region = None # Add all subregions that do not intersect with XOR region # to a new SpectralRegion object if subregion.lower > one.upper or subregion.upper < one.lower: - if not new_region: - new_region = subregion - else: - new_region += subregion - # All other subregions are added to temp_region + temp_region = subregion + # Partial overlap + elif subregion.lower < one.lower and subregion.upper < one.upper: + temp_region = SpectralRegion(subregion.lower, one.lower) + elif subregion.upper > one.upper and subregion.lower > one.lower: + temp_region = SpectralRegion(one.upper, subregion.upper) + + if not temp_region: + continue + + if new_region: + new_region += temp_region else: - if not temp_region: - temp_region = subregion - else: - temp_region += subregion - # This is the main application of XOR to other regions - if not new_region: - new_region = temp_region.invert(one.lower, one.upper) - else: - new_region = new_region + temp_region.invert(one.lower, one.upper) + new_region = temp_region + # This adds the edge regions that are otherwise not included - if not (one.lower == temp_region.lower and one.upper == temp_region.upper): - new_region = new_region + one.invert(temp_region.lower, - temp_region.upper) - return new_region + if new_region: + return merge_func(new_region + inverted_region) + return inverted_region else: - return two + one + return merge_func(two + one) else: # This gets triggered in the InvertState case where state1 # is an object and state2 is None @@ -1319,14 +1343,33 @@ def is_there_overlap_spectral_subset(self, subset_name): Returns True if the spectral subset with subset_name has overlapping subregions. """ - spectral_region = self.get_subsets(subset_name, spectral_only=True) - if not spectral_region or len(spectral_region) < 2: + spectral_region = self.get_subsets(subset_name, simplify_spectral=False, spectral_only=True) + n_reg = len(spectral_region) + if not spectral_region or n_reg < 2: return False - for index in range(0, len(spectral_region) - 1): - if spectral_region[index].upper.value >= spectral_region[index + 1].lower.value: + for index in range(n_reg - 1): + if spectral_region[index]['region'].upper.value >= spectral_region[index + 1]['region'].lower.value: # noqa: E501 return True return False + @staticmethod + def _merge_overlapping_spectral_regions_worker(spectral_region): + if len(spectral_region) < 2: # noop + return spectral_region + + merged_regions = spectral_region[0] # Instantiate merged regions + for cur_reg in spectral_region[1:]: + # If the lower value of the current subregion is less than or equal to the upper + # value of the last subregion added to merged_regions, update last region in + # merged_regions with the max upper value between the two regions. + if cur_reg.lower <= merged_regions.upper: + merged_regions._subregions[-1] = ( + merged_regions._subregions[-1][0], + max(cur_reg.upper, merged_regions._subregions[-1][1])) + else: + merged_regions += cur_reg + return merged_regions + def merge_overlapping_spectral_regions(self, subset_name, att): """ Takes a spectral subset with subset_name and returns an ``OrState`` object @@ -1339,34 +1382,15 @@ def merge_overlapping_spectral_regions(self, subset_name, att): att : str Attribute that the subset uses to apply to data. """ - spectral_region = self.get_subsets(subset_name, spectral_only=True) - merged_regions = None - # Convert SpectralRegion object into a list with tuples representing - # the lower and upper values of each region. - reg_as_tup = [(sr.lower.value, sr.upper.value) for sr in spectral_region] - for index in range(0, len(spectral_region)): - # Instantiate merged regions - if not merged_regions: - merged_regions = [reg_as_tup[index]] - else: - last_merged = merged_regions[-1] - # If the lower value of the current subregion is less than or equal to the upper - # value of the last subregion added to merged_regions, update last_merged - # with the max upper value between the two regions. - if reg_as_tup[index][0] <= last_merged[1]: - last_merged = (last_merged[0], max(last_merged[1], reg_as_tup[index][1])) - merged_regions = merged_regions[:-1] - merged_regions.append(last_merged) - else: - merged_regions.append(reg_as_tup[index]) + merged_regions = self.get_subsets(subset_name, spectral_only=True) new_state = None for region in reversed(merged_regions): - convert_to_range = RangeSubsetState(region[0], region[1], att) - if new_state is None: - new_state = convert_to_range - else: + convert_to_range = RangeSubsetState(region.lower.value, region.upper.value, att) + if new_state: new_state = new_state | convert_to_range + else: + new_state = convert_to_range return new_state diff --git a/jdaviz/configs/default/plugins/subset_plugin/subset_plugin.py b/jdaviz/configs/default/plugins/subset_plugin/subset_plugin.py index d963bdd12d..50e8e092bc 100644 --- a/jdaviz/configs/default/plugins/subset_plugin/subset_plugin.py +++ b/jdaviz/configs/default/plugins/subset_plugin/subset_plugin.py @@ -305,11 +305,9 @@ def _unpack_get_subsets_for_ui(self): # types cannot be simplified so subsets contained that are skipped. if 'Mask' in self.subset_types: self.can_simplify = False - elif (len(self.subset_states) > 1 and isinstance(self.subset_states[0], RangeSubsetState) - and len(simplifiable_states - set(self.glue_state_types)) < 3): - self.can_simplify = True - elif (len(self.subset_states) > 1 and isinstance(self.subset_states[0], RangeSubsetState) - and self.app.is_there_overlap_spectral_subset(self.subset_selected)): + elif ((len(self.subset_states) > 1) and isinstance(self.subset_states[0], RangeSubsetState) + and ((len(simplifiable_states - set(self.glue_state_types)) < 3) + or self.app.is_there_overlap_spectral_subset(self.subset_selected))): self.can_simplify = True else: self.can_simplify = False diff --git a/jdaviz/tests/test_subsets.py b/jdaviz/tests/test_subsets.py index f7bef099a5..6f0bec6ce9 100644 --- a/jdaviz/tests/test_subsets.py +++ b/jdaviz/tests/test_subsets.py @@ -5,7 +5,7 @@ from astropy.utils.data import get_pkg_data_filename from glue.core.roi import CircularROI, CircularAnnulusROI, EllipticalROI, RectangularROI, XRangeROI from glue.core.subset_group import GroupedSubset -from glue.core.edit_subset_mode import AndMode, AndNotMode, OrMode, XorMode +from glue.core.edit_subset_mode import AndMode, AndNotMode, NewMode, OrMode, XorMode from regions import (PixCoord, CirclePixelRegion, CircleSkyRegion, RectanglePixelRegion, EllipsePixelRegion, CircleAnnulusPixelRegion) from numpy.testing import assert_allclose @@ -562,24 +562,88 @@ def test_edit_composite_spectral_subset(specviz_helper, spectrum1d): specviz_helper.app.get_subsets("Subset 1") -def test_edit_composite_spectral_with_xor(specviz_helper, spectrum1d): +def test_composite_spectral_with_xor(specviz_helper, spectrum1d): specviz_helper.load_data(spectrum1d) viewer = specviz_helper.app.get_viewer(specviz_helper._default_spectrum_viewer_reference_name) - viewer.apply_roi(XRangeROI(6400, 6600)) + viewer.apply_roi(XRangeROI(6200, 6800)) specviz_helper.app.session.edit_subset_mode.mode = OrMode - viewer.apply_roi(XRangeROI(7200, 7400)) - - viewer.apply_roi(XRangeROI(7600, 7800)) + viewer.apply_roi(XRangeROI(7200, 7600)) specviz_helper.app.session.edit_subset_mode.mode = XorMode - viewer.apply_roi(XRangeROI(6700, 7700)) + viewer.apply_roi(XRangeROI(6100, 7600)) reg = specviz_helper.app.get_subsets("Subset 1") - assert reg[0].lower.value == 6400 and reg[0].upper.value == 6600 - assert reg[1].lower.value == 6700 and reg[1].upper.value == 7200 - assert reg[2].lower.value == 7400 and reg[2].upper.value == 7600 - assert reg[3].lower.value == 7700 and reg[3].upper.value == 7800 + assert reg[0].lower.value == 6100 and reg[0].upper.value == 6200 + assert reg[1].lower.value == 6800 and reg[1].upper.value == 7200 + + specviz_helper.app.session.edit_subset_mode.mode = NewMode + viewer.apply_roi(XRangeROI(7000, 7200)) + specviz_helper.app.session.edit_subset_mode.mode = XorMode + viewer.apply_roi(XRangeROI(7100, 7300)) + specviz_helper.app.session.edit_subset_mode.mode = OrMode + viewer.apply_roi(XRangeROI(6900, 7105)) + reg = specviz_helper.app.get_subsets("Subset 2") + assert reg[0].lower.value == 6900 and reg[0].upper.value == 7105 + assert reg[1].lower.value == 7200 and reg[1].upper.value == 7300 + + specviz_helper.app.session.edit_subset_mode.mode = NewMode + viewer.apply_roi(XRangeROI(6000, 6500)) + specviz_helper.app.session.edit_subset_mode.mode = XorMode + viewer.apply_roi(XRangeROI(6100, 6200)) + reg = specviz_helper.app.get_subsets("Subset 3") + assert reg[0].lower.value == 6000 and reg[0].upper.value == 6100 + assert reg[1].lower.value == 6200 and reg[1].upper.value == 6500 + + specviz_helper.app.session.edit_subset_mode.mode = NewMode + viewer.apply_roi(XRangeROI(6100, 6200)) + specviz_helper.app.session.edit_subset_mode.mode = XorMode + viewer.apply_roi(XRangeROI(6000, 6500)) + reg = specviz_helper.app.get_subsets("Subset 4") + assert reg[0].lower.value == 6000 and reg[0].upper.value == 6100 + assert reg[1].lower.value == 6200 and reg[1].upper.value == 6500 + + specviz_helper.app.session.edit_subset_mode.mode = NewMode + viewer.apply_roi(XRangeROI(7500, 7600)) + specviz_helper.app.session.edit_subset_mode.mode = XorMode + viewer.apply_roi(XRangeROI(6000, 6010)) + reg = specviz_helper.app.get_subsets("Subset 5") + assert reg[0].lower.value == 6000 and reg[0].upper.value == 6010 + assert reg[1].lower.value == 7500 and reg[1].upper.value == 7600 + + +def test_composite_spectral_with_xor_complicated(specviz_helper, spectrum1d): + specviz_helper.load_data(spectrum1d) + viewer = specviz_helper.app.get_viewer(specviz_helper._default_spectrum_viewer_reference_name) + + viewer.apply_roi(XRangeROI(6100, 6700)) + + # (6100, 6200), (6300, 6700) + specviz_helper.app.session.edit_subset_mode.mode = AndNotMode + viewer.apply_roi(XRangeROI(6200, 6300)) + + # (6050, 6100), (6200, 6300), (6700, 6800) + specviz_helper.app.session.edit_subset_mode.mode = XorMode + viewer.apply_roi(XRangeROI(6050, 6800)) + + # (6050, 6100), (6200, 6300), (6700, 6800), (7000, 7200) + specviz_helper.app.session.edit_subset_mode.mode = OrMode + viewer.apply_roi(XRangeROI(7000, 7200)) + + # (6010, 6020), (6050, 6100), (6200, 6300), (6700, 6800), (7000, 7200) + viewer.apply_roi(XRangeROI(6010, 6020)) + + # (6010, 6020), (6050, 6090), (6100, 6200), (6300, 6700), (6800, 6850), (7000, 7200) + specviz_helper.app.session.edit_subset_mode.mode = XorMode + viewer.apply_roi(XRangeROI(6090, 6850)) + + reg = specviz_helper.app.get_subsets("Subset 1") + assert reg[0].lower.value == 6010 and reg[0].upper.value == 6020 + assert reg[1].lower.value == 6050 and reg[1].upper.value == 6090 + assert reg[2].lower.value == 6100 and reg[2].upper.value == 6200 + assert reg[3].lower.value == 6300 and reg[3].upper.value == 6700 + assert reg[4].lower.value == 6800 and reg[4].upper.value == 6850 + assert reg[5].lower.value == 7000 and reg[5].upper.value == 7200 def test_overlapping_spectral_regions(specviz_helper, spectrum1d): From 60c9ae604819f29aba9299204f23f44ce858c30f Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Fri, 9 Aug 2024 11:46:03 -0400 Subject: [PATCH 017/127] markers: ensure units stored as string (#3135) --- jdaviz/configs/imviz/plugins/coords_info/coords_info.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/jdaviz/configs/imviz/plugins/coords_info/coords_info.py b/jdaviz/configs/imviz/plugins/coords_info/coords_info.py index b5600b2f23..6d8b15f19e 100644 --- a/jdaviz/configs/imviz/plugins/coords_info/coords_info.py +++ b/jdaviz/configs/imviz/plugins/coords_info/coords_info.py @@ -496,7 +496,7 @@ def _image_viewer_update(self, viewer, x, y): dq_text = '' self.row1b_text = f'{value:+10.5e} {unit}{dq_text}' self._dict['value'] = float(value) - self._dict['value:unit'] = unit + self._dict['value:unit'] = str(unit) self._dict['value:unreliable'] = unreliable_pixel else: self.row1b_title = '' @@ -519,9 +519,9 @@ def _image_viewer_update(self, viewer, x, y): def _spectrum_viewer_update(self, viewer, x, y): def _cursor_fallback(): self._dict['axes_x'] = x - self._dict['axes_x:unit'] = viewer.state.x_display_unit + self._dict['axes_x:unit'] = str(viewer.state.x_display_unit) self._dict['axes_y'] = y - self._dict['axes_y:unit'] = viewer.state.y_display_unit + self._dict['axes_y:unit'] = str(viewer.state.y_display_unit) self._dict['data_label'] = '' def _copy_axes_to_spectral(): @@ -643,7 +643,7 @@ def _copy_axes_to_spectral(): self.row3_title = 'Flux' 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._dict['axes_y:unit'] = str(viewer.state.y_display_unit) if closest_icon is not None: self.icon = closest_icon From f5c93ab385441fe4dedbfc3b6d435097038dd48a Mon Sep 17 00:00:00 2001 From: Clare Shanahan Date: Fri, 9 Aug 2024 14:03:05 -0400 Subject: [PATCH 018/127] Aperture photometry plugin reflects display unit selection (#3118) * aperture photometry plugin listens to unit conversion * pllim minor edits review comment Allow per-wave to/from per-freq for most cases Co-authored-by: Clare Shanahan * TST: Update result that changed because of #3133 --------- Co-authored-by: P. L. Lim <2090236+pllim@users.noreply.github.com> --- CHANGES.rst | 2 + .../plugins/tests/test_cubeviz_aperphot.py | 156 ++++++--- .../aper_phot_simple/aper_phot_simple.py | 316 ++++++++++++++++-- .../aper_phot_simple/aper_phot_simple.vue | 20 +- 4 files changed, 407 insertions(+), 87 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index b2f5d5668e..f0410f2be2 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -27,6 +27,8 @@ Cubeviz - Background subtraction support within Spectral Extraction. [#2859] +- Aperture photometry plugin now listens to changes in display unit. [#3118] + Imviz ^^^^^ diff --git a/jdaviz/configs/cubeviz/plugins/tests/test_cubeviz_aperphot.py b/jdaviz/configs/cubeviz/plugins/tests/test_cubeviz_aperphot.py index 9d922f6a57..032c9fa2ad 100644 --- a/jdaviz/configs/cubeviz/plugins/tests/test_cubeviz_aperphot.py +++ b/jdaviz/configs/cubeviz/plugins/tests/test_cubeviz_aperphot.py @@ -1,13 +1,12 @@ import numpy as np import pytest from astropy import units as u +from astropy.table import Table from astropy.tests.helper import assert_quantity_allclose from astropy.utils.exceptions import AstropyUserWarning from numpy.testing import assert_allclose from regions import RectanglePixelRegion, PixCoord -from jdaviz.configs.cubeviz.plugins.moment_maps.moment_maps import SPECUTILS_LT_1_15_1 - def test_cubeviz_aperphot_cube_orig_flux(cubeviz_helper, image_cube_hdu_obj_microns): cubeviz_helper.load_data(image_cube_hdu_obj_microns, data_label="test") @@ -82,53 +81,6 @@ def test_cubeviz_aperphot_cube_orig_flux(cubeviz_helper, image_cube_hdu_obj_micr assert np.isnan(row["slice_wave"]) -def test_cubeviz_aperphot_generated_2d_moment(cubeviz_helper, image_cube_hdu_obj_microns): - cubeviz_helper.load_data(image_cube_hdu_obj_microns, data_label="test") - flux_unit = u.Unit("1E-17 erg*s^-1*cm^-2*Angstrom^-1") - - moment_plg = cubeviz_helper.plugins["Moment Maps"] - _ = moment_plg.calculate_moment() - - # Need this to make it available for photometry data drop-down. - cubeviz_helper.app.add_data_to_viewer("uncert-viewer", "test[FLUX] moment 0") - - aper = RectanglePixelRegion(center=PixCoord(x=1, y=2), width=3, height=5) - cubeviz_helper.load_regions(aper) - - plg = cubeviz_helper.plugins["Aperture Photometry"]._obj - plg.dataset_selected = "test[FLUX] moment 0" - plg.aperture_selected = "Subset 1" - plg.vue_do_aper_phot() - row = cubeviz_helper.get_aperture_photometry_results()[0] - - # Basically, we should recover the input rectangle here. - if SPECUTILS_LT_1_15_1: - moment_sum = 540 * flux_unit - moment_mean = 36 * flux_unit - else: - moment_unit = flux_unit * u.um - moment_sum = 0.54 * moment_unit - moment_mean = 0.036 * moment_unit - assert_allclose(row["xcenter"], 1 * u.pix) - assert_allclose(row["ycenter"], 2 * u.pix) - sky = row["sky_center"] - assert_allclose(sky.ra.deg, 205.43985906934287) - assert_allclose(sky.dec.deg, 27.003490103642033) - assert_allclose(row["sum"], moment_sum) # 3 (w) x 5 (h) x 36 (v) - assert_allclose(row["sum_aper_area"], 15 * (u.pix * u.pix)) # 3 (w) x 5 (h) - assert_allclose(row["mean"], moment_mean) - assert np.isnan(row["slice_wave"]) - - # Moment 1 has no compatible unit, so should not be available for photometry. - moment_plg.n_moment = 1 - moment_plg.reference_wavelength = 5 - _ = moment_plg.calculate_moment() - m1_lbl = "test[FLUX] moment 1" - cubeviz_helper.app.add_data_to_viewer("uncert-viewer", m1_lbl) - assert (m1_lbl in cubeviz_helper.app.data_collection.labels and - m1_lbl not in plg.dataset.choices) - - def test_cubeviz_aperphot_generated_3d_gaussian_smooth(cubeviz_helper, image_cube_hdu_obj_microns): cubeviz_helper.load_data(image_cube_hdu_obj_microns, data_label="test") flux_unit = u.Unit("1E-17 erg*s^-1*cm^-2*Angstrom^-1") @@ -192,3 +144,109 @@ def test_cubeviz_aperphot_cube_orig_flux_mjysr(cubeviz_helper, spectrum1d_cube_c assert_allclose(row["mean"], 5 * (u.MJy / u.sr)) # TODO: check if slice plugin has value_unit set correctly assert_quantity_allclose(row["slice_wave"], 0.46236 * u.um) + + +def _compare_table_units(orig_tab, new_tab, orig_flux_unit=None, + new_flux_unit=None): + + # compare two photometry tables with different units and make sure that the + # units are as expected, and that they are equivalent once translated + + for i, row in enumerate(orig_tab): + new_unit = new_tab[i]['unit'] or '-' + orig_unit = row['unit'] or '-' + if new_unit != '-' and orig_unit != '-': + + new_unit = u.Unit(new_unit) + new = float(new_tab[i]['result']) * new_unit + + orig_unit = u.Unit(orig_unit) + orig = float(row['result']) * orig_unit + + # first check that the actual units differ as expected, + # as comparing them would pass if they were the same unit + if orig_flux_unit in orig_unit.bases: + assert new_flux_unit in new_unit.bases + + orig_converted = orig.to(new_unit) + assert_quantity_allclose(orig_converted, new) + + +def test_cubeviz_aperphot_unit_conversion(cubeviz_helper, spectrum1d_cube_custom_fluxunit): + """Make sure outputs of the aperture photometry plugin in Cubeviz + reflect the correct choice of display units from the Unit + Conversion plugin. + """ + + # create cube with units of MJy / sr + mjy_sr_cube = spectrum1d_cube_custom_fluxunit(fluxunit=u.MJy / u.sr, + shape=(5, 5, 4)) + + # create apertures for photometry and background + aper = RectanglePixelRegion(center=PixCoord(x=2, y=3), width=1, height=1) + bg = RectanglePixelRegion(center=PixCoord(x=1, y=2), width=1, height=1) + + cubeviz_helper.load_data(mjy_sr_cube, data_label="test") + cubeviz_helper.load_regions([aper, bg]) + + ap = cubeviz_helper.plugins['Aperture Photometry']._obj + + ap.dataset_selected = "test[FLUX]" + ap.aperture_selected = "Subset 1" + ap.background_selected = "Subset 2" + ap.vue_do_aper_phot() + + uc = cubeviz_helper.plugins['Unit Conversion']._obj + + # check that initial units are synced between plugins + assert uc.flux_unit.selected == 'MJy' + assert uc.angle_unit.selected == 'sr' + assert ap.display_flux_or_sb_unit == 'MJy / sr' + assert ap.flux_scaling_display_unit == 'MJy' + + # and defaults for inputs are in the correct unit + assert_allclose(ap.flux_scaling, 0.003631) + assert_allclose(ap.background_value, 49) + + # output table in original units to compare to + # outputs after converting units + orig_tab = Table(ap.results) + + # change units, which will change the numerator of the current SB unit + uc.flux_unit.selected = 'Jy' + + # make sure inputs were re-computed in new units + # after the unit change + assert_allclose(ap.flux_scaling, 3631) + assert_allclose(ap.background_value, 4.9e7) + + # re-do photometry and make sure table is in new units + # and consists of the same results as before converting units + ap.vue_do_aper_phot() + new_tab = Table(ap.results) + + _compare_table_units(orig_tab, new_tab, orig_flux_unit=u.MJy, + new_flux_unit=u.Jy) + + # test manual background and flux scaling option input in current + # units (Jy / sr) will be used correctly and converted to data units + ap.background_selected == 'Manual' + ap.background_value = 1.0e7 + ap.flux_scaling = 1000 + ap.vue_do_aper_phot() + orig_tab = Table(ap.results) + + # change units back to MJy/sr from Jy/sr + uc.flux_unit.selected = 'MJy' + + # make sure background input in Jy/sr is now in MJy/sr + assert_allclose(ap.background_value, 10) + assert_allclose(ap.flux_scaling, 0.001) + + # and that photometry results match those before unit converson, + # but with units converted + ap.vue_do_aper_phot() + new_tab = Table(ap.results) + + _compare_table_units(orig_tab, new_tab, orig_flux_unit=u.Jy, + new_flux_unit=u.MJy) diff --git a/jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.py b/jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.py index e4b29a2b6e..bf9d63be8e 100644 --- a/jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.py +++ b/jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.py @@ -15,12 +15,14 @@ from traitlets import Any, Bool, Integer, List, Unicode, observe from jdaviz.core.custom_traitlets import FloatHandleEmpty -from jdaviz.core.events import SnackbarMessage, LinkUpdatedMessage, SliceValueUpdatedMessage +from jdaviz.core.events import (GlobalDisplayUnitChanged, SnackbarMessage, + LinkUpdatedMessage, SliceValueUpdatedMessage) from jdaviz.core.region_translators import regions2aperture, _get_region_from_spatial_subset from jdaviz.core.registries import tray_registry from jdaviz.core.template_mixin import (PluginTemplateMixin, DatasetMultiSelectMixin, SubsetSelect, ApertureSubsetSelectMixin, TableMixin, PlotMixin, MultiselectMixin, with_spinner) +from jdaviz.core.validunits import check_if_unit_is_per_solid_angle from jdaviz.utils import PRIHDR_KEY __all__ = ['SimpleAperturePhotometry'] @@ -66,6 +68,8 @@ class SimpleAperturePhotometry(PluginTemplateMixin, ApertureSubsetSelectMixin, # Cubeviz only cube_slice = Unicode("").tag(sync=True) is_cube = Bool(False).tag(sync=True) + display_flux_or_sb_unit = Unicode("").tag(sync=True) + flux_scaling_display_unit = Unicode("").tag(sync=True) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -109,8 +113,7 @@ def valid_cubeviz_datasets(data): acceptable_types = ['spectral flux density wav', 'photon flux density wav', 'spectral flux density', - 'photon flux density', - 'energy flux'] # Moment map 0 + 'photon flux density'] return ((data.ndim in (2, 3)) and ((img_unit == (u.MJy / u.sr)) or (img_unit.physical_type in acceptable_types))) @@ -119,6 +122,9 @@ def valid_cubeviz_datasets(data): self.session.hub.subscribe(self, SliceValueUpdatedMessage, handler=self._on_slice_changed) + self.hub.subscribe(self, GlobalDisplayUnitChanged, + handler=self._on_display_units_changed) + # TODO: expose public API once finalized # @property # def user_api(self): @@ -144,6 +150,87 @@ def _on_dataset_selected_changed(self, event={}): else: self.is_cube = False + def _on_display_units_changed(self, event={}): + + """ + Handle change of display units from Unit Conversion plugin (for now, + cubeviz only). If new display units differ from input data units, input + parameters for ap. phot. (i.e background, flux scaling) are converted + to the new units. Photometry will remain in previous unit until + 'calculate' is pressed again. + """ + + if self.config == 'cubeviz': + + # get previously selected display units + prev_display_flux_or_sb_unit = self.display_flux_or_sb_unit + prev_flux_scale_unit = self.flux_scaling_display_unit + + # update display unit traitlets to new selection + self._set_display_unit_of_selected_dataset() + + # convert the previous background and flux scaling values to new unit so + # re-calculating photometry with the current selections will produce + # the previous output with the new unit. + if prev_display_flux_or_sb_unit != '': + + # convert background to new unit + if self.background_value is not None: + + prev_unit = u.Unit(prev_display_flux_or_sb_unit) + new_unit = u.Unit(self.display_flux_or_sb_unit) + + bg = self.background_value * prev_unit + self.background_value = bg.to_value( + new_unit, u.spectral_density(self._cube_wave)) + + # convert flux scaling to new unit + if self.flux_scaling is not None: + prev_unit = u.Unit(prev_flux_scale_unit) + new_unit = u.Unit(self.flux_scaling_display_unit) + + fs = self.flux_scaling * prev_unit + self.flux_scaling = fs.to_value( + new_unit, u.spectral_density(self._cube_wave)) + + def _set_display_unit_of_selected_dataset(self): + + """ + Set the display_flux_or_sb_unit and flux_scaling_display_unit traitlets, + which depend on if the selected data set is flux or surface brightness, + and the corresponding global display unit for either flux or + surface brightness. + """ + + if not self.dataset_selected or not self.aperture_selected: + self.display_flux_or_sb_unit = '' + self.flux_scaling_display_unit = '' + return + + data = self.dataset.selected_dc_item + comp = data.get_component(data.main_components[0]) + if comp.units: + # if data is something-per-solid-angle, its a SB unit and we should + # use the selected global display unit for SB + if check_if_unit_is_per_solid_angle(comp.units): + flux_or_sb = 'sb' + else: + flux_or_sb = 'flux' + + disp_unit = self.app._get_display_unit(flux_or_sb) + + self.display_flux_or_sb_unit = disp_unit + + # now get display unit for flux_scaling_display_unit. this unit will always + # be in flux, but it will not be derived from the global flux display unit + # note : need to generalize this for non-sr units eventually + fs_unit = u.Unit(disp_unit) * u.sr + self.flux_scaling_display_unit = fs_unit.to_string() + + else: + self.display_flux_or_sb_unit = '' + self.flux_scaling_display_unit = '' + def _get_defaults_from_metadata(self, dataset=None): defaults = {} if dataset is None: @@ -162,6 +249,12 @@ def _get_defaults_from_metadata(self, dataset=None): if telescope == 'JWST': # Hardcode the flux conversion factor from MJy to ABmag mjy2abmag = 0.003631 + + # if display unit is different, translate + if (self.config == 'cubeviz') and (self.display_flux_or_sb_unit != ''): + disp_unit = u.Unit(self.display_flux_or_sb_unit) + mjy2abmag = (mjy2abmag * u.Unit("MJy/sr")).to_value(disp_unit) + if 'photometry' in meta and 'pixelarea_arcsecsq' in meta['photometry']: defaults['pixel_area'] = meta['photometry']['pixelarea_arcsecsq'] if 'bunit_data' in meta and meta['bunit_data'] == u.Unit("MJy/sr"): @@ -242,6 +335,11 @@ def _dataset_selected_changed(self, event={}): f"Failed to extract {self.dataset_selected}: {repr(e)}", color='error', sender=self)) + # get correct display unit for newly selected dataset + if self.config == 'cubeviz': + # set display_flux_or_sb_unit and flux_scaling_display_unit + self._set_display_unit_of_selected_dataset() + # auto-populate background, if applicable. self._aperture_selected_changed() @@ -276,6 +374,10 @@ def _aperture_selected_changed(self, event={}): if self.multiselect: self._background_selected_changed() return + + if self.config == 'cubeviz': + self._set_display_unit_of_selected_dataset() + # NOTE: aperture_selected can be triggered here before aperture_selected_validity is updated # so we'll still allow the snackbar to be raised as a second warning to the user and to # avoid acting on outdated information @@ -331,7 +433,15 @@ def _calc_background_median(self, reg, data=None): img_stat = aper_mask_stat.get_values(comp_data, mask=None) # photutils/background/_utils.py --> nanmedian() - return np.nanmedian(img_stat) # Naturally in data unit + bg_md = np.nanmedian(img_stat) # Naturally in data unit + + # convert to display unit, if necessary (cubeviz only) + + if (self.config == 'cubeviz') and (self.display_flux_or_sb_unit != '') and comp.units: + bg_md = (bg_md * u.Unit(comp.units)).to_value( + u.Unit(self.display_flux_or_sb_unit), u.spectral_density(self._cube_wave)) + + return bg_md @observe('background_selected') def _background_selected_changed(self, event={}): @@ -359,8 +469,13 @@ def calculate_photometry(self, dataset=None, aperture=None, background=None, background_value=None, pixel_area=None, counts_factor=None, flux_scaling=None, add_to_table=True, update_plots=True): """ - Calculate aperture photometry given the values set in the plugin or any overrides provided - as arguments here (which will temporarily override plugin values for this calculation only). + Calculate aperture photometry given the values set in the plugin or + any overrides provided as arguments here (which will temporarily + override plugin values for this calculation only). + + Note: Values set in the plugin in Cubeviz are in the selected display unit + from the Unit conversion plugin. Overrides are, as the docstrings note, + assumed to be in the units of the selected dataset. Parameters ---------- @@ -421,6 +536,13 @@ def calculate_photometry(self, dataset=None, aperture=None, background=None, del self.app.fitted_models[self._fitted_model_name] comp = data.get_component(data.main_components[0]) + if comp.units: + img_unit = u.Unit(comp.units) + else: + img_unit = None + + if self.config == 'cubeviz': + display_unit = u.Unit(self.display_flux_or_sb_unit) if background is not None and background not in self.background.choices: # pragma: no cover raise ValueError(f"background must be one of {self.background.choices}") @@ -430,14 +552,35 @@ def calculate_photometry(self, dataset=None, aperture=None, background=None, raise ValueError("cannot provide background_value with background!='Manual'") elif (background == 'Manual' or (background is None and self.background.selected == 'Manual')): + background_value = self.background_value + + # cubeviz: background_value set in plugin is in display units + # convert temporarily to image units for calculations + if (self.config == 'cubeviz') and (img_unit is not None): + background_value = (background_value * display_unit).to_value( + img_unit, u.spectral_density(self._cube_wave)) + elif background is None and dataset is None: + # use the previously-computed value in the plugin background_value = self.background_value + + # cubeviz: background_value set in plugin is in display units + # convert temporarily to image units for calculations + if (self.config == 'cubeviz') and (img_unit is not None): + background_value = (background_value * display_unit).to_value( + img_unit, u.spectral_density(self._cube_wave)) else: bg_reg = self.aperture._get_spatial_region(subset=background if background is not None else self.background.selected, # noqa dataset=dataset if dataset is not None else self.dataset.selected) # noqa background_value = self._calc_background_median(bg_reg, data=data) + + # cubeviz: computed background median will be in display units, + # convert temporarily back to image units for calculations + if (self.config == 'cubeviz') and (img_unit is not None): + background_value = (background_value * display_unit).to_value( + img_unit, u.spectral_density(self._cube_wave)) try: bg = float(background_value) except ValueError: # Clearer error message @@ -476,11 +619,16 @@ def calculate_photometry(self, dataset=None, aperture=None, background=None, include_counts_fac = False include_flux_scale = False if comp.units: - img_unit = u.Unit(comp.units) - bg = bg * img_unit + + # work for now in units of currently selected dataset (which may or + # may not be the desired output units, depending on the display + # units selected in the Unit Conversion plugin. background value + # has already been converted to image units above, and flux scaling + # will be converted from display unit > img_unit comp_data = comp_data << img_unit + bg = bg * img_unit - if u.sr in img_unit.bases: # TODO: Better way to detect surface brightness unit? + if check_if_unit_is_per_solid_angle(img_unit): # if units are surface brightness try: pixarea = float(pixel_area if pixel_area is not None else self.pixel_area) except ValueError: # Clearer error message @@ -494,12 +642,30 @@ def calculate_photometry(self, dataset=None, aperture=None, background=None, raise ValueError('Missing or invalid counts conversion factor') if not np.allclose(ctfac, 0): include_counts_fac = True + + # if cubeviz and flux_scaling is provided as override, it is in the data units + # if set in the app, it is in the display units and needs to be converted + # if provided as an override keyword arg, it is assumed to be in the + # data units and does not need to be converted + if ((self.config == 'cubeviz') and (flux_scaling is None) and + (self.flux_scaling is not None)): + # update eventaully to handle non-sr SB units + flux_scaling = (self.flux_scaling * u.Unit(self.flux_scaling_display_unit)).to_value( # noqa: E501 + img_unit * u.sr, u.spectral_density(self._cube_wave)) + try: flux_scale = float(flux_scaling if flux_scaling is not None else self.flux_scaling) except ValueError: # Clearer error message raise ValueError('Missing or invalid flux scaling') if not np.allclose(flux_scale, 0): include_flux_scale = True + + # from now, we will just need the image unit as a string for display + img_unit = img_unit.to_string() + + else: + img_unit = None + phot_aperstats = ApertureStats(comp_data, aperture, wcs=data.coords, local_bkg=bg) phot_table = phot_aperstats.to_table(columns=( 'id', 'sum', 'sum_aper_area', @@ -511,6 +677,8 @@ def calculate_photometry(self, dataset=None, aperture=None, background=None, if include_pixarea_fac: pixarea = pixarea * (u.arcsec * u.arcsec / (u.pix * u.pix)) # NOTE: Sum already has npix value encoded, so we simply apply the npix unit here. + + # note: need to generalize this to non-steradian surface brightness units pixarea_fac = (u.pix * u.pix) * pixarea.to(u.sr / (u.pix * u.pix)) phot_table['sum'] = [rawsum * pixarea_fac] else: @@ -548,8 +716,34 @@ def calculate_photometry(self, dataset=None, aperture=None, background=None, slice_val = self._cube_wave else: slice_val = u.Quantity(np.nan, self._cube_wave.unit) + phot_table.add_column(slice_val, name="slice_wave", index=29) + if comp.units: # convert phot. results from image unit to display unit + display_unit = u.Unit(self.display_flux_or_sb_unit) + # convert units of certain columns in aperture phot. output table + # to reflect display units (i.e if data units are MJy / sr, but + # Jy / sr is selected in Unit Conversion plugin) + phot_table['background'] = phot_table['background'].to( + display_unit, u.spectral_density(self._cube_wave)) + + if include_pixarea_fac: + phot_table['sum'] = phot_table['sum'].to( + (display_unit * pixarea_fac).unit, u.spectral_density(self._cube_wave)) + else: + phot_table['sum'] = phot_table['sum'].to( + display_unit, u.spectral_density(self._cube_wave)) + for key in ['min', 'max', 'mean', 'median', 'mode', 'std', + 'mad_std', 'biweight_location']: + phot_table[key] = phot_table[key].to( + display_unit, u.spectral_density(self._cube_wave)) + for key in ['var', 'biweight_midvariance']: + try: + phot_table[key] = phot_table[key].to(display_unit**2) + # FIXME: Can fail going between per-wave and per-freq + except u.UnitConversionError: + pass + if add_to_table: try: phot_table['id'][0] = self.table._qtable['id'].max() + 1 @@ -564,14 +758,24 @@ def calculate_photometry(self, dataset=None, aperture=None, background=None, # Plots. if update_plots: + + # for cubeviz unit conversion display units + if self.display_flux_or_sb_unit != '': + plot_display_unit = self.display_flux_or_sb_unit + else: + plot_display_unit = None + if self.current_plot_type == "Curve of Growth": if self.config == "cubeviz" and data.ndim > 2: self.plot.figure.title = f'Curve of growth from aperture center at {slice_val:.4e}' # noqa: E501 + eqv = u.spectral_density(self._cube_wave) else: self.plot.figure.title = 'Curve of growth from aperture center' + eqv = [] x_arr, sum_arr, x_label, y_label = _curve_of_growth( comp_data, (xcenter, ycenter), aperture, phot_table['sum'][0], - wcs=data.coords, background=bg, pixarea_fac=pixarea_fac) + wcs=data.coords, background=bg, pixarea_fac=pixarea_fac, + display_unit=plot_display_unit, equivalencies=eqv) self.plot._update_data('profile', x=x_arr, y=sum_arr, reset_lims=True) self.plot.update_style('profile', line_visible=True, color='gray', size=32) self.plot.update_style('fit', visible=False) @@ -580,16 +784,22 @@ def calculate_photometry(self, dataset=None, aperture=None, background=None, else: # Radial profile self.plot.figure.axes[0].label = 'pix' - self.plot.figure.axes[1].label = comp.units or 'Value' + if plot_display_unit: + self.plot.figure.axes[1].label = plot_display_unit + else: + self.plot.figure.axes[1].label = img_unit or 'Value' if self.current_plot_type == "Radial Profile": if self.config == "cubeviz" and data.ndim > 2: self.plot.figure.title = f'Radial profile from aperture center at {slice_val:.4e}' # noqa: E501 + eqv = u.spectral_density(self._cube_wave) else: self.plot.figure.title = 'Radial profile from aperture center' + eqv = [] x_data, y_data = _radial_profile( phot_aperstats.data_cutout, phot_aperstats.bbox, (xcenter, ycenter), - raw=False) + raw=False, display_unit=plot_display_unit, image_unit=img_unit, + equivalencies=eqv) self.plot._update_data('profile', x=x_data, y=y_data, reset_lims=True) self.plot.update_style('profile', line_visible=True, color='gray', size=32) @@ -600,7 +810,7 @@ def calculate_photometry(self, dataset=None, aperture=None, background=None, self.plot.figure.title = 'Raw radial profile from aperture center' x_data, y_data = _radial_profile( phot_aperstats.data_cutout, phot_aperstats.bbox, (xcenter, ycenter), - raw=True) + raw=True, display_unit=plot_display_unit, image_unit=img_unit) self.plot._update_data('profile', x=x_data, y=y_data, reset_lims=True) self.plot.update_style('profile', line_visible=False, color='gray', size=10) @@ -637,26 +847,40 @@ def calculate_photometry(self, dataset=None, aperture=None, background=None, if key in ('id', 'data_label', 'subset_label', 'background', 'pixarea_tot', 'counts_fac', 'aperture_sum_counts_err', 'flux_scaling', 'timestamp'): continue + x = phot_table[key][0] - if (isinstance(x, (int, float, u.Quantity)) and + + if isinstance(x, u.Quantity): # split up unit and value to put in different cols + unit = x.unit.to_string() + if unit == '': # for eccentricity which is a quantity with an empty unit + unit = '-' + x = x.value + else: + unit = '-' + + if (isinstance(x, (int, float)) and key not in ('xcenter', 'ycenter', 'sky_center', 'sum_aper_area', 'aperture_sum_counts', 'aperture_sum_mag', 'slice_wave')): - tmp.append({'function': key, 'result': f'{x:.4e}'}) + if x == 0: + tmp.append({'function': key, 'result': f'{x:.1f}', 'unit': unit}) + else: + tmp.append({'function': key, 'result': f'{x:.3e}', 'unit': unit}) elif key == 'sky_center' and x is not None: - tmp.append({'function': 'RA center', 'result': f'{x.ra.deg:.6f} deg'}) - tmp.append({'function': 'Dec center', 'result': f'{x.dec.deg:.6f} deg'}) + tmp.append({'function': 'RA center', 'result': f'{x.ra.deg:.6f}', 'unit': 'deg'}) + tmp.append({'function': 'Dec center', 'result': f'{x.dec.deg:.6f}', 'unit': 'deg'}) elif key in ('xcenter', 'ycenter', 'sum_aper_area'): - tmp.append({'function': key, 'result': f'{x:.1f}'}) + tmp.append({'function': key, 'result': f'{x:.1f}', 'unit': unit}) elif key == 'aperture_sum_counts' and x is not None: tmp.append({'function': key, 'result': - f'{x:.4e} ({phot_table["aperture_sum_counts_err"][0]:.4e})'}) + f'{x:.4e} ({phot_table["aperture_sum_counts_err"][0]:.4e})', + 'unit': unit}) elif key == 'aperture_sum_mag' and x is not None: - tmp.append({'function': key, 'result': f'{x:.3f}'}) + tmp.append({'function': key, 'result': f'{x:.3f}', 'unit': unit}) elif key == 'slice_wave': if data.ndim > 2: - tmp.append({'function': key, 'result': f'{slice_val:.4e}'}) + tmp.append({'function': key, 'result': f'{slice_val.value:.4e}', 'unit': slice_val.unit.to_string()}) # noqa: E501 else: - tmp.append({'function': key, 'result': str(x)}) + tmp.append({'function': key, 'result': str(x), 'unit': unit}) if update_plots: # Also display fit results @@ -855,7 +1079,8 @@ def calculate_batch_photometry(self, options=[], add_to_table=True, update_plots # NOTE: These are hidden because the APIs are for internal use only # but we need them as a separate functions for unit testing. -def _radial_profile(radial_cutout, reg_bb, centroid, raw=False): +def _radial_profile(radial_cutout, reg_bb, centroid, raw=False, + image_unit=None, display_unit=None, equivalencies=[]): """Calculate radial profile. Parameters @@ -873,6 +1098,15 @@ def _radial_profile(radial_cutout, reg_bb, centroid, raw=False): If `True`, returns raw data points for scatter plot. Otherwise, use ``imexam`` algorithm for a clean plot. + image_unit : str or None + (For cubeviz only to deal with display unit conversion). Unit of input + 'radial cutout', used with `display_unit` to convert output to desired + display unit. + + display_unit : str or None + (For cubeviz only to deal with display unit conversion). Desired unit + for output. + """ reg_ogrid = np.ogrid[reg_bb.iymin:reg_bb.iymax, reg_bb.ixmin:reg_bb.ixmax] radial_dx = reg_ogrid[1] - centroid[0] @@ -897,11 +1131,16 @@ def _radial_profile(radial_cutout, reg_bb, centroid, raw=False): y_arr = np.bincount(radial_r, radial_img) / np.bincount(radial_r) x_arr = np.arange(y_arr.size) + if display_unit is not None: + if image_unit is None: + raise ValueError('Must provide image_unit with display_unit.') + y_arr = (y_arr * u.Unit(image_unit)).to_value(u.Unit(display_unit), equivalencies) + return x_arr, y_arr -def _curve_of_growth(data, centroid, aperture, final_sum, wcs=None, background=0, n_datapoints=10, - pixarea_fac=None): +def _curve_of_growth(data, centroid, aperture, final_sum, wcs=None, background=0, + n_datapoints=10, pixarea_fac=None, display_unit=None, equivalencies=[]): """Calculate curve of growth for aperture photometry. Parameters @@ -934,6 +1173,11 @@ def _curve_of_growth(data, centroid, aperture, final_sum, wcs=None, background=0 pixarea_fac : float or `None` For ``flux_unit/sr`` to ``flux_unit`` conversion. + display_unit : str or None + (For cubeviz only to deal with display unit conversion). Desired unit + for output. If unit is a surface brightness, a Flux unit will be + returned if pixarea_fac is provided. + Returns ------- x_arr : ndarray @@ -953,6 +1197,18 @@ def _curve_of_growth(data, centroid, aperture, final_sum, wcs=None, background=0 """ n_datapoints += 1 # n + 1 + # determined desired unit for output sum array and y label + # cubeviz only to handle unit conversion display unit changes + if display_unit is not None: + sum_unit = u.Unit(display_unit) + else: + if isinstance(data, u.Quantity): + sum_unit = data.unit + else: + sum_unit = None + if sum_unit and pixarea_fac is not None: + sum_unit *= pixarea_fac.unit + if hasattr(aperture, 'to_pixel'): aperture = aperture.to_pixel(wcs) @@ -985,12 +1241,14 @@ def _curve_of_growth(data, centroid, aperture, final_sum, wcs=None, background=0 sum_arr = np.array(sum_arr) if pixarea_fac is not None: sum_arr = sum_arr * pixarea_fac + if isinstance(final_sum, u.Quantity): + final_sum = final_sum.to(sum_arr.unit, equivalencies) sum_arr = np.append(sum_arr, final_sum) - if isinstance(sum_arr, u.Quantity): - y_label = sum_arr.unit.to_string() - sum_arr = sum_arr.value # bqplot does not like Quantity - else: + if sum_unit is None: y_label = 'Value' + else: + y_label = sum_unit.to_string() + sum_arr = sum_arr.to_value(sum_unit, equivalencies) # bqplot does not like Quantity return x_arr, sum_arr, x_label, y_label diff --git a/jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.vue b/jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.vue index 64f18a95a4..c6b673187b 100644 --- a/jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.vue +++ b/jdaviz/configs/imviz/plugins/aper_phot_simple/aper_phot_simple.vue @@ -5,7 +5,7 @@ :uses_active_status="uses_active_status" @plugin-ping="plugin_ping($event)" :keep_active.sync="keep_active" - :popout_button="popout_button" + :popout_button="popout_button" :scroll_to.sync="scroll_to"> @@ -99,7 +100,7 @@ label="Pixel area" v-model.number="pixel_area" type="number" - hint="Pixel area in arcsec squared, only used if sr in data unit" + hint="Pixel area in arcsec squared, only used if data is in units of surface brightness." persistent-hint > @@ -129,7 +130,8 @@ label="Flux scaling" v-model.number="flux_scaling" type="number" - hint="Same unit as data, used in -2.5 * log(flux / flux_scaling)" + :suffix="flux_scaling_display_unit" + hint="Used in -2.5 * log(flux / flux_scaling)" persistent-hint > @@ -213,16 +215,16 @@ Result - Value + Value + Unit - - {{ item.function }} - - {{ item.result }} + {{ item.function }} + {{ item.result }} + {{ item.unit }}
From 43228d08643c7f20f977f56c572c0bc57108997c Mon Sep 17 00:00:00 2001 From: "P. L. Lim" <2090236+pllim@users.noreply.github.com> Date: Fri, 9 Aug 2024 20:56:21 -0400 Subject: [PATCH 019/127] TST: photutils nightly wheel and devdeps clean-up --- tox.ini | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tox.ini b/tox.ini index f0748a8a87..632d21fcc4 100644 --- a/tox.ini +++ b/tox.ini @@ -39,16 +39,16 @@ description = # The following provides some specific pinnings for key packages deps = # NOTE: Add/remove as needed - #devdeps: numpy>=0.0.dev0 + devdeps: numpy>=0.0.dev0 devdeps: scipy>=0.0.dev0 devdeps: matplotlib>=0.0.dev0 devdeps: pandas>=0.0.dev0 devdeps: scikit-image>=0.0.dev0 devdeps: pyerfa>=0.0.dev0 devdeps: astropy>=0.0.dev0 + devdeps: photutils>=0.0.dev0 devdeps: git+https://github.com/astropy/regions.git devdeps: git+https://github.com/astropy/specutils.git - devdeps: git+https://github.com/astropy/photutils.git devdeps: git+https://github.com/spacetelescope/gwcs.git devdeps: git+https://github.com/asdf-format/asdf.git devdeps: git+https://github.com/astropy/asdf-astropy.git @@ -71,8 +71,6 @@ extras = alldeps: all commands = - # Force numpy-dev after matplotlib downgrades it (https://github.com/matplotlib/matplotlib/issues/26847) - #devdeps: python -m pip install --pre --upgrade --extra-index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple numpy jupyter --paths pip freeze !cov: pytest --pyargs jdaviz {toxinidir}/docs {posargs} From 9da9f7d64d7e99ca2e980de2e8dc52674a61c41e Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Mon, 12 Aug 2024 13:39:01 -0400 Subject: [PATCH 020/127] plugin API hints (#3137) * POC: plugin API hints demo * store plugin state in metadata "history" * button to toggle API hints (only hooked up for gaussian smooth for now) * implement to_script method * might want to move to the UI side if we want to control the order and have a copy button * include data definition and write to script in metadata * rename show_api_hints > api_hints * improved styling and connect add_to_viewer * rename api_hints > api_hints_enabled * generalize and implement across multiple plugins * codestyle and fix rebase * .ignore-api-hint > explicitly set .api-hint * use plugin-switch component for visibility switches in plot options * implement for moment maps and specviz2d spec extract * tooltip for toggle button * fix behavior for switches * separate component for slider header * fix styling for editable select * brief mention in docs * changelog entry * don't show toggle button when not in notebook/lab * defer to_script functionality * implement/update additional plugins * partial implementation for dq plugin * plugin-color-picker and plugin-slider components --- CHANGES.rst | 2 + docs/plugin_api.rst | 2 + jdaviz/app.py | 4 + jdaviz/components/plugin_action_button.vue | 3 +- jdaviz/components/plugin_add_results.vue | 44 +- jdaviz/components/plugin_auto_label.vue | 5 +- jdaviz/components/plugin_color_picker.vue | 39 ++ jdaviz/components/plugin_dataset_select.vue | 16 +- jdaviz/components/plugin_editable_select.vue | 47 +- .../components/plugin_file_import_select.vue | 5 +- jdaviz/components/plugin_inline_select.vue | 7 +- jdaviz/components/plugin_input_header.vue | 18 + jdaviz/components/plugin_layer_select.vue | 9 +- .../plugin_previews_temp_disabled.vue | 5 +- jdaviz/components/plugin_slider.vue | 25 + jdaviz/components/plugin_subset_select.vue | 9 +- jdaviz/components/plugin_switch.vue | 37 ++ jdaviz/components/plugin_viewer_select.vue | 15 +- jdaviz/components/tooltip.vue | 1 + jdaviz/components/tray_plugin.vue | 60 ++- .../plugins/moment_maps/moment_maps.vue | 26 +- .../configs/cubeviz/plugins/slice/slice.vue | 36 +- .../spectral_extraction.py | 2 +- .../spectral_extraction.vue | 59 ++- .../default/plugins/collapse/collapse.vue | 14 +- .../plugins/data_quality/data_quality.py | 20 +- .../plugins/data_quality/data_quality.vue | 102 ++-- .../configs/default/plugins/export/export.py | 2 - .../configs/default/plugins/export/export.vue | 47 +- .../gaussian_smooth/gaussian_smooth.vue | 14 +- .../default/plugins/line_lists/line_lists.vue | 16 +- .../default/plugins/markers/markers.vue | 4 +- .../metadata_viewer/metadata_viewer.vue | 23 +- .../plugins/model_fitting/model_fitting.vue | 49 +- .../plugins/plot_options/plot_options.py | 5 +- .../plugins/plot_options/plot_options.vue | 472 +++++++++++------- .../imviz/plugins/footprints/footprints.vue | 108 ++-- .../imviz/plugins/orientation/orientation.vue | 87 +++- .../plugins/line_analysis/line_analysis.vue | 17 +- .../unit_conversion/unit_conversion.py | 6 +- .../unit_conversion/unit_conversion.vue | 19 +- .../spectral_extraction.vue | 101 +++- jdaviz/core/template_mixin.py | 1 + jdaviz/core/user_api.py | 5 +- jdaviz/main_styles.vue | 37 ++ 45 files changed, 1153 insertions(+), 472 deletions(-) create mode 100644 jdaviz/components/plugin_color_picker.vue create mode 100644 jdaviz/components/plugin_input_header.vue create mode 100644 jdaviz/components/plugin_slider.vue create mode 100644 jdaviz/components/plugin_switch.vue diff --git a/CHANGES.rst b/CHANGES.rst index f0410f2be2..ee87712bb6 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -16,6 +16,8 @@ New Features - The colormap menu for image layers now shows in-line previews of the colormaps. [#2900] +- Plugins can now expose in-UI API hints. [#3137] + Cubeviz ^^^^^^^ diff --git a/docs/plugin_api.rst b/docs/plugin_api.rst index c6e61a9f8d..dfbf4154d5 100644 --- a/docs/plugin_api.rst +++ b/docs/plugin_api.rst @@ -13,3 +13,5 @@ For example: plugin = viz.plugins['Plot Options'] plugin.open_in_tray() plugin.show('popout') + +When running in a notebook, some plugins provide API hints directly in the UI. To enable these, toggle the ``<>`` button in the top of the plugin. \ No newline at end of file diff --git a/jdaviz/app.py b/jdaviz/app.py index 018807d00b..7674ec5c51 100644 --- a/jdaviz/app.py +++ b/jdaviz/app.py @@ -142,10 +142,14 @@ def to_unit(self, data, cid, values, original_units, target_units): 'plugin-editable-select': 'components/plugin_editable_select.vue', 'plugin-inline-select': 'components/plugin_inline_select.vue', 'plugin-inline-select-item': 'components/plugin_inline_select_item.vue', + 'plugin-switch': 'components/plugin_switch.vue', 'plugin-action-button': 'components/plugin_action_button.vue', 'plugin-add-results': 'components/plugin_add_results.vue', 'plugin-auto-label': 'components/plugin_auto_label.vue', 'plugin-file-import-select': 'components/plugin_file_import_select.vue', + 'plugin-slider': 'components/plugin_slider.vue', + 'plugin-color-picker': 'components/plugin_color_picker.vue', + 'plugin-input-header': 'components/plugin_input_header.vue', 'glue-state-sync-wrapper': 'components/glue_state_sync_wrapper.vue'} _verbosity_levels = ('debug', 'info', 'warning', 'error') diff --git a/jdaviz/components/plugin_action_button.vue b/jdaviz/components/plugin_action_button.vue index 4baa47f2bc..62b8b415a2 100644 --- a/jdaviz/components/plugin_action_button.vue +++ b/jdaviz/components/plugin_action_button.vue @@ -2,6 +2,7 @@ module.exports = { - props: ['spinner', 'disabled', 'results_isolated_to_plugin'], + props: ['spinner', 'disabled', 'results_isolated_to_plugin', 'api_hints_enabled'], computed: { buttonColor() { if (this.results_isolated_to_plugin) { diff --git a/jdaviz/components/plugin_add_results.vue b/jdaviz/components/plugin_add_results.vue index 9d576b17ee..58fbde4ccc 100644 --- a/jdaviz/components/plugin_add_results.vue +++ b/jdaviz/components/plugin_add_results.vue @@ -9,6 +9,8 @@ @update:auto="$emit('update:label_auto', $event)" :invalid_msg="label_invalid_msg" :label="label_label ? label_label : 'Output Data Label'" + :api_hint="add_results_api_hint && add_results_api_hint + '.label ='" + :api_hints_enabled="api_hints_enabled && add_results_api_hint" :hint="label_hint ? label_hint : 'Label for the resulting data item.'" > @@ -26,15 +28,17 @@ :selected="add_to_viewer_selected" @update:selected="$emit('update:add_to_viewer_selected', $event)" show_if_single_entry="true" - label='Plot in Viewer' + label="Plot in Viewer" + :api_hint="add_results_api_hint && add_results_api_hint+'.viewer ='" + :api_hints_enabled="api_hints_enabled && add_results_api_hint" :hint="add_to_viewer_hint ? add_to_viewer_hint : 'Plot results in the specified viewer. Data entry will be available in the data dropdown for all applicable viewers.'" >
@@ -72,9 +77,9 @@ :spinner="action_spinner" :disabled="label_invalid_msg.length > 0 || action_disabled" :results_isolated_to_plugin="false" + :api_hints_enabled="api_hints_enabled && action_api_hint" @click="$emit('click:action')"> - - {{action_label}}{{label_overwrite ? ' (Overwrite)' : ''}} + {{ actionButtonText }} @@ -88,9 +93,28 @@ diff --git a/jdaviz/components/plugin_auto_label.vue b/jdaviz/components/plugin_auto_label.vue index 2423a4a11b..b0a099734c 100644 --- a/jdaviz/components/plugin_auto_label.vue +++ b/jdaviz/components/plugin_auto_label.vue @@ -7,7 +7,8 @@ @keyup="if(auto) {if ($event.srcElement._value === displayValue) {return}; $emit('update:auto', false)}; $emit('update:value', $event.srcElement._value)" @mouseenter="showIcon = true" @mouseleave="showIcon = false" - :label="label" + :label="api_hints_enabled && api_hint ? api_hint : label" + :class="api_hints_enabled && api_hint ? 'api-hint' : null" :hint="hint" :rules="[(e) => invalid_msg || true]" persistent-hint @@ -25,7 +26,7 @@ diff --git a/jdaviz/components/plugin_dataset_select.vue b/jdaviz/components/plugin_dataset_select.vue index e0e73635dd..df5f5ad0eb 100644 --- a/jdaviz/components/plugin_dataset_select.vue +++ b/jdaviz/components/plugin_dataset_select.vue @@ -1,13 +1,13 @@ diff --git a/jdaviz/components/plugin_editable_select.vue b/jdaviz/components/plugin_editable_select.vue index 15466086a4..97694883be 100644 --- a/jdaviz/components/plugin_editable_select.vue +++ b/jdaviz/components/plugin_editable_select.vue @@ -8,7 +8,8 @@ :items="items" v-model="selected" @change="$emit('update:selected', $event)" - :label="label" + :label="api_hints_enabled && api_hint ? api_hint : label" + :class="api_hints_enabled && api_hint ? 'api-hint' : null" :hint="hint" :rules="rules ? rules : []" item-text="label" @@ -33,7 +34,12 @@ type="warning" style="width: 100%; padding-top: 16px; padding-bottom: 16px" > - remove '{{selected}}' {{label.toLowerCase()}}? + + {{api_hint_remove}}('{{selected}}') + + + remove '{{selected}}' {{label.toLowerCase()}}? + @@ -60,13 +67,43 @@ + + + Applying changes... + +
diff --git a/jdaviz/components/plugin_inline_select.vue b/jdaviz/components/plugin_inline_select.vue index 1712d06626..e249228c00 100644 --- a/jdaviz/components/plugin_inline_select.vue +++ b/jdaviz/components/plugin_inline_select.vue @@ -1,5 +1,10 @@