Source code for jdaviz.configs.default.plugins.subset_plugin.subset_plugin

import numpy as np
from glue.core.message import EditSubsetMessage, SubsetUpdateMessage
from glue.core.edit_subset_mode import (AndMode, AndNotMode, OrMode,
                                        ReplaceMode, XorMode)
from glue.core.roi import CircularROI, EllipticalROI, RectangularROI
from glue.core.subset import RoiSubsetState, RangeSubsetState, CompositeSubsetState
from glue_jupyter.widgets.subset_mode_vuetify import SelectionModeMenu
from traitlets import Any, List, Unicode, Bool, observe

from jdaviz.core.events import SnackbarMessage
from jdaviz.core.registries import tray_registry
from jdaviz.core.template_mixin import PluginTemplateMixin, DatasetSelectMixin, SubsetSelect

__all__ = ['SubsetPlugin']

SUBSET_MODES = {
    'replace': ReplaceMode,
    'add': OrMode,
    'and': AndMode,
    'xor': XorMode,
    'remove': AndNotMode
}


[docs]@tray_registry('g-subset-plugin', label="Subset Tools") class SubsetPlugin(PluginTemplateMixin, DatasetSelectMixin): template_file = __file__, "subset_plugin.vue" select = List([]).tag(sync=True) subset_items = List([]).tag(sync=True) subset_selected = Unicode("Create New").tag(sync=True) mode_selected = Unicode('add').tag(sync=True) show_region_info = Bool(True).tag(sync=True) subset_types = List([]).tag(sync=True) subset_definitions = List([]).tag(sync=True) has_subset_details = Bool(False).tag(sync=True) subplugins_opened = Any().tag(sync=True) is_editable = Bool(False).tag(sync=True) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.components = { 'g-subset-mode': SelectionModeMenu(session=self.session) } self.session.hub.subscribe(self, EditSubsetMessage, handler=self._sync_selected_from_state) self.session.hub.subscribe(self, SubsetUpdateMessage, handler=self._on_subset_update) self.subset_select = SubsetSelect(self, 'subset_items', 'subset_selected', default_text="Create New") def _sync_selected_from_state(self, *args): if not hasattr(self, 'subset_select'): # during initial init, this can trigger before the component is initialized return if self.session.edit_subset_mode.edit_subset == []: if self.subset_selected != self.subset_select.default_text: self.subset_selected = self.subset_select.default_text self.show_region_info = False else: new_label = self.session.edit_subset_mode.edit_subset[0].label if new_label != self.subset_selected: if new_label not in [s['label'] for s in self.subset_items]: self._sync_available_from_state() self.subset_selected = self.session.edit_subset_mode.edit_subset[0].label self.show_region_info = True def _on_subset_update(self, *args): self._sync_selected_from_state(*args) if self.subset_selected == 'Create New': return self._get_subset_definition(*args) subset_to_update = self.session.edit_subset_mode.edit_subset[0] self.subset_select._update_subset(subset_to_update, attribute="type") def _sync_available_from_state(self, *args): if not hasattr(self, 'subset_select'): # during initial init, this can trigger before the component is initialized return self.subset_items = [{'label': self.subset_select.default_text}] + [ self.subset_select._subset_to_dict(subset) for subset in self.data_collection.subset_groups] @observe('subset_selected') def _sync_selected_from_ui(self, change): self.subset_definitions = [] self.subset_types = [] self.is_editable = False if not hasattr(self, 'subset_select'): # during initial init, this can trigger before the component is initialized return if change['new'] != self.subset_select.default_text: self._get_subset_definition(change['new']) self.show_region_info = change['new'] != self.subset_select.default_text m = [s for s in self.app.data_collection.subset_groups if s.label == change['new']] if m != self.session.edit_subset_mode.edit_subset: self.session.edit_subset_mode.edit_subset = m ''' # This will be needed once we use a dropdown instead of the actual # g-subset-mode component @observe("mode_selected") def _mode_selected_changed(self, event={}): if self.session.edit_subset_mode != self.mode_selected: self.session.edit_subset_mode = self.mode_selected ''' def _unpack_nested_subset(self, subset_state): ''' Navigate through the tree of subset states for composite subsets made up of multiple regions. ''' if isinstance(subset_state, CompositeSubsetState): self._unpack_nested_subset(subset_state.state1) self._unpack_nested_subset(subset_state.state2) self.is_editable = False else: if subset_state is not None: self._get_subset_subregion_definition(subset_state) def _get_subset_subregion_definition(self, subset_state): """ Get the type and parameters for a single region in the subset. Note that the string type and operation (if in a composite subset) need to be stored separately from the float parameters for display reasons. """ subset_type = '' subset_definition = [] self.is_editable = False _around_decimals = 6 # Avoid 30 degrees from coming back as 29.999999999999996 if isinstance(subset_state, RoiSubsetState): if isinstance(subset_state.roi, CircularROI): x, y = subset_state.roi.get_center() r = subset_state.roi.radius subset_definition = [{"name": "X Center", "att": "xc", "value": x, "orig": x}, {"name": "Y Center", "att": "yc", "value": y, "orig": y}, {"name": "Radius", "att": "radius", "value": r, "orig": r}] self.is_editable = True elif isinstance(subset_state.roi, RectangularROI): for att in ("Xmin", "Xmax", "Ymin", "Ymax"): real_att = att.lower() val = getattr(subset_state.roi, real_att) subset_definition.append( {"name": att, "att": real_att, "value": val, "orig": val}) theta = np.around(np.degrees(subset_state.roi.theta), decimals=_around_decimals) subset_definition.append( {"name": "Angle", "att": "theta", "value": theta, "orig": theta}) self.is_editable = True elif isinstance(subset_state.roi, EllipticalROI): xc = subset_state.roi.xc yc = subset_state.roi.yc rx = subset_state.roi.radius_x ry = subset_state.roi.radius_y theta = np.around(np.degrees(subset_state.roi.theta), decimals=_around_decimals) subset_definition = [ {"name": "X Center", "att": "xc", "value": xc, "orig": xc}, {"name": "Y Center", "att": "yc", "value": yc, "orig": yc}, {"name": "X Radius", "att": "radius_x", "value": rx, "orig": rx}, {"name": "Y Radius", "att": "radius_y", "value": ry, "orig": ry}, {"name": "Angle", "att": "theta", "value": theta, "orig": theta}] self.is_editable = True subset_type = subset_state.roi.__class__.__name__ elif isinstance(subset_state, RangeSubsetState): lo = subset_state.lo hi = subset_state.hi subset_definition = [{"name": "Lower bound", "att": "lo", "value": lo, "orig": lo}, {"name": "Upper bound", "att": "hi", "value": hi, "orig": hi}] self.is_editable = True subset_type = "Range" if len(subset_definition) > 0 and subset_definition not in self.subset_definitions: # Note: .append() does not work for List traitlet. self.subset_definitions = self.subset_definitions + [subset_definition] self.subset_types = self.subset_types + [subset_type] def _get_subset_definition(self, *args): """ Retrieve the parameters defining the selected subset, for example the upper and lower bounds for a simple spectral subset. """ self.subset_definitions = [] self.subset_types = [] self._unpack_nested_subset(self.subset_select.selected_subset_state)
[docs] def vue_update_subset(self, *args): if not self.is_editable: # no-op return subset_state = self.subset_select.selected_subset_state # Composite region cannot be edited, so just grab first element. subset_type = self.subset_types[0] subset_definition = self.subset_definitions[0] try: if subset_type == "Range": sbst_obj = subset_state else: sbst_obj = subset_state.roi for d_att in subset_definition: if d_att["att"] == 'theta': # Humans use degrees but glue uses radians d_val = np.radians(d_att["value"]) else: d_val = d_att["value"] setattr(sbst_obj, d_att["att"], d_val) # Force glue to update the Subset. This is the same call used in # glue.core.edit_subset_mode.EditSubsetMode.update() but we do not # want to deal with all the contract stuff tied to the update() method. self.session.edit_subset_mode._combine_data(subset_state, override_mode=ReplaceMode) except Exception as err: # pragma: no cover self.hub.broadcast(SnackbarMessage( f"Failed to update Subset: {repr(err)}", color='error', sender=self))
[docs] def vue_recenter_subset(self, *args): # Composite region cannot be edited. This only works for Imviz. if not self.is_editable or self.config != 'imviz': # no-op raise NotImplementedError( f'Cannot recenter: is_editable={self.is_editable}, config={self.config}') from photutils.aperture import ApertureStats from jdaviz.core.region_translators import regions2aperture, _get_region_from_spatial_subset try: reg = _get_region_from_spatial_subset(self, self.subset_selected) aperture = regions2aperture(reg) data = self.dataset.selected_dc_item comp = data.get_component(data.main_components[0]) comp_data = comp.data phot_aperstats = ApertureStats(comp_data, aperture) # Centroid was calculated in selected data, which might or might not be # the reference data. However, Subset is always defined w.r.t. # the reference data, so we need to convert back. viewer = self.app._jdaviz_helper.default_viewer x, y, _, _ = viewer._get_real_xy( data, phot_aperstats.xcentroid, phot_aperstats.ycentroid, reverse=True) if not np.all(np.isfinite((x, y))): raise ValueError(f'Invalid centroid ({x}, {y})') except Exception as err: self.set_center(self.get_center(), update=False) self.hub.broadcast(SnackbarMessage( f"Failed to calculate centroid: {repr(err)}", color='error', sender=self)) else: self.set_center((x, y), update=True)
[docs] def get_center(self): """Return the center of the Subset. This may or may not be the centroid obtain from data. Returns ------- cen : number, tuple of numbers, or `None` The center of the Subset in ``x`` or ``(x, y)``, depending on the Subset type, if applicable. If Subset is not editable, this returns `None`. Raises ------ NotImplementedError Subset type is not supported. """ # Composite region cannot be edited. if not self.is_editable: # no-op return subset_state = self.subset_select.selected_subset_state if isinstance(subset_state, RoiSubsetState): sbst_obj = subset_state.roi if isinstance(sbst_obj, (CircularROI, EllipticalROI)): cen = sbst_obj.get_center() elif isinstance(sbst_obj, RectangularROI): cen = sbst_obj.center() else: # pragma: no cover raise NotImplementedError( f'Getting center of {sbst_obj.__class__} is not supported') elif isinstance(subset_state, RangeSubsetState): cen = (subset_state.hi - subset_state.lo) * 0.5 + subset_state.lo else: # pragma: no cover raise NotImplementedError( f'Getting center of {subset_state.__class__} is not supported') return cen
[docs] def set_center(self, new_cen, update=False): """Set the desired center for the selected Subset, if applicable. If Subset is not editable, nothing is done. Parameters ---------- new_cen : number or tuple of numbers The new center defined either as ``x`` or ``(x, y)``, depending on the Subset type. update : bool If `True`, the Subset is also moved to the new center. Otherwise, only the relevant editable fields are updated but the Subset is not moved. Raises ------ NotImplementedError Subset type is not supported. """ # Composite region cannot be edited, so just grab first element. if not self.is_editable: # no-op return subset_state = self.subset_select.selected_subset_state if isinstance(subset_state, RoiSubsetState): x, y = new_cen sbst_obj = subset_state.roi if isinstance(sbst_obj, (CircularROI, EllipticalROI)): self._set_value_in_subset_definition(0, "X Center", "value", x) self._set_value_in_subset_definition(0, "Y Center", "value", y) elif isinstance(sbst_obj, RectangularROI): cx, cy = sbst_obj.center() dx = x - cx dy = y - cy self._set_value_in_subset_definition(0, "Xmin", "value", sbst_obj.xmin + dx) self._set_value_in_subset_definition(0, "Xmax", "value", sbst_obj.xmax + dx) self._set_value_in_subset_definition(0, "Ymin", "value", sbst_obj.ymin + dy) self._set_value_in_subset_definition(0, "Ymax", "value", sbst_obj.ymax + dy) else: # pragma: no cover raise NotImplementedError(f'Recentering of {sbst_obj.__class__} is not supported') elif isinstance(subset_state, RangeSubsetState): dx = new_cen - ((subset_state.hi - subset_state.lo) * 0.5 + subset_state.lo) self._set_value_in_subset_definition(0, "Lower bound", "value", subset_state.lo + dx) self._set_value_in_subset_definition(0, "Upper bound", "value", subset_state.hi + dx) else: # pragma: no cover raise NotImplementedError( f'Getting center of {subset_state.__class__} is not supported') if update: self.vue_update_subset() else: # Force UI to update on browser without changing the subset. tmp = self.subset_definitions self.subset_definitions = [] self.subset_definitions = tmp
# List of JSON-like dict is nice for front-end but a pain to look up, # so we use these helper functions. def _get_value_from_subset_definition(self, index, name, desired_key): subset_definition = self.subset_definitions[index] value = None for item in subset_definition: if item['name'] == name: value = item[desired_key] break return value def _set_value_in_subset_definition(self, index, name, desired_key, new_value): for i in range(len(self.subset_definitions[index])): if self.subset_definitions[index][i]['name'] == name: self.subset_definitions[index][i]['value'] = new_value break