Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add API to rename subsets #3356

Merged
merged 37 commits into from
Dec 30, 2024
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
644c6fa
adding check for subset name
cshanahan1 Dec 12, 2023
33c29a4
Create list of reserved labels at app level to check against
rosteen Dec 10, 2024
d276f7c
Force labels to strings now so don't need None
rosteen Dec 11, 2024
8a760f9
Move validation function, minimize layer reprocessing, start adding t…
rosteen Dec 12, 2024
354d9f8
Warn if label is not alphanumberic, adding code from #1291
rosteen Dec 12, 2024
68bcf15
Add SubsetRenameMessage
rosteen Dec 13, 2024
fa75705
Update all SubsetSelects in plugins when subset is renamed.
rosteen Dec 13, 2024
ff18e05
allow plugin components to call `send_state`
kecnry Dec 6, 2024
dafdd7c
Working on updating subset items in LayerSelect on rename, buggy righ…
rosteen Dec 13, 2024
5bf5c72
Add passing test, refactor app.get_subsets for better efficiency in s…
rosteen Dec 17, 2024
9ce2c0d
Add changelog
rosteen Dec 17, 2024
b1afe39
Remove leftover lower() from rebase
rosteen Dec 18, 2024
7b7466f
Fix tests
rosteen Dec 18, 2024
575bdee
Remove debugging prints
rosteen Dec 18, 2024
394afd0
Add docstrings, remove kwarg that isn't relevant to users
rosteen Dec 19, 2024
6eb5156
Don't allow renaming to Subset N
rosteen Dec 19, 2024
1a195ae
Raise instead of error, add test
rosteen Dec 19, 2024
2aa8afc
Remove debugging prints
rosteen Dec 19, 2024
c1c857d
Raise error if trying to rename a subset that doesn't exist
rosteen Dec 19, 2024
f579ab1
Remove prints
rosteen Dec 23, 2024
ecdaf94
codesty.e
rosteen Dec 23, 2024
03ba4ce
Don't check for hardcoded 'Subset' in label
rosteen Dec 24, 2024
4e1a694
Fix failing test
rosteen Dec 24, 2024
79ee99a
Fix check to be more general
rosteen Dec 24, 2024
4c431a9
Move changelog entry
rosteen Dec 24, 2024
fe0f198
Restore updated subset to layer select menus with icon
rosteen Dec 26, 2024
5b69487
Don't duplicate renamed subset in export plugin
rosteen Dec 26, 2024
70d4058
Update extracted spectrum data collection item as well
rosteen Dec 26, 2024
5e4b424
Update old data menu items
rosteen Dec 26, 2024
2f35bc1
Address review comments
rosteen Dec 27, 2024
1f1978e
Codestyle
rosteen Dec 27, 2024
cbc0698
Check old label
rosteen Dec 27, 2024
c64656e
Add simple test
rosteen Dec 27, 2024
cf79558
Test rename_selected
rosteen Dec 27, 2024
8e95d39
Codestyle
rosteen Dec 27, 2024
851f200
Cover another case
rosteen Dec 27, 2024
0d74275
Add methods to class docstring
rosteen Dec 30, 2024
File filter

Filter by extension

Filter by extension

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

- Added API for renaming subsets to Subset Tools plugin. [#3356]

Cubeviz
^^^^^^^

Expand Down Expand Up @@ -89,9 +91,9 @@ New Features

- Improve performance while importing multiple regions. [#3321]

* API method to toggle API hints. [#3336]
- API method to toggle API hints. [#3336]

* Changing flux/SB display units no longer resets viewer zoom levels. [#3335]
- Changing flux/SB display units no longer resets viewer zoom levels. [#3335]

Cubeviz
^^^^^^^
Expand Down
126 changes: 122 additions & 4 deletions jdaviz/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
from jdaviz import style_registry
from jdaviz.core.config import read_configuration, get_configuration
from jdaviz.core.events import (LoadDataMessage, NewViewerMessage, AddDataMessage,
SnackbarMessage, RemoveDataMessage,
SnackbarMessage, RemoveDataMessage, SubsetRenameMessage,
AddDataToViewerMessage, RemoveDataFromViewerMessage,
ViewerAddedMessage, ViewerRemovedMessage,
ViewerRenamedMessage, ChangeRefDataMessage,
Expand Down Expand Up @@ -319,6 +319,9 @@
self.hub.subscribe(self, PluginPlotAddedMessage,
handler=self._on_plugin_plot_added)

# Convenient reference of all existing subset names
self._reserved_labels = set([])

# Parse the yaml configuration file used to compose the front-end UI
self.load_configuration(configuration)

Expand Down Expand Up @@ -371,10 +374,14 @@
# Internal cache so we don't have to keep calling get_object for the same Data.
# Key should be (data_label, statistic) and value the translated object.
self._get_object_cache = {}

self.hub.subscribe(self, SubsetUpdateMessage,
handler=self._on_subset_update_message)
# These both call _on_layers_changed
self.hub.subscribe(self, SubsetDeleteMessage,
handler=self._on_subset_delete_message)
self.hub.subscribe(self, SubsetCreateMessage,
handler=self._on_subset_create_message)

# Store for associations between Data entries:
self._data_associations = self._init_data_associations()
Expand All @@ -384,9 +391,6 @@
handler=self._on_add_data_message)
self.hub.subscribe(self, RemoveDataMessage,
handler=self._on_layers_changed)
self.hub.subscribe(self, SubsetCreateMessage,
handler=self._on_layers_changed)
# SubsetDeleteMessage will also call _on_layers_changed via _on_subset_delete_message

# Emit messages when icons are updated
self.state.add_callback('viewer_icons',
Expand Down Expand Up @@ -464,6 +468,13 @@

def _on_subset_delete_message(self, msg):
self._remove_live_plugin_results(trigger_subset=msg.subset)
if msg.subset.label in self._reserved_labels:
# This might already be gone in test teardowns
self._reserved_labels.remove(msg.subset.label)
self._on_layers_changed(msg)

def _on_subset_create_message(self, msg):
self._reserved_labels.add(msg.subset.label)
self._on_layers_changed(msg)

def _on_plugin_plot_added(self, msg):
Expand Down Expand Up @@ -584,6 +595,9 @@
children_layers = self._get_assoc_data_children(layer_name)

elif hasattr(msg, 'subset'):
# We don't need to reprocess the subset for every data collection entry
if msg.subset.data != self.data_collection[0]:
return
layer_name = msg.subset.label
is_wcs_only = False
is_not_child = True
Expand Down Expand Up @@ -1988,6 +2002,109 @@
viewer_item = self._viewer_item_by_id(ref_or_id)
return viewer_item

def _check_valid_subset_label(self, subset_name, raise_if_invalid=True):
"""Check that `subset_name` is a valid choice for a subset name. This
check is run when renaming subsets.

A valid subset name must not be the name of another subset in the data
collection (case insensitive? there cant be a subset 1 and a Subset 1.)
The name may match a subset in the data collection if it the current
active selection (i.e the renaming is not really a renaming, it is
just keeping the old name, which is valid).

Unlike dataset names, the attempted renaming of a subset to an existing
subset label will not append a number (e.g Subset 1 repeated because
Subset 1(1)). If the name exists, a warning will be raised and the
original subset name will be returned.

"""

# get active selection, if there is one
if self.session.edit_subset_mode.edit_subset == []:
subset_selected = None

Check warning on line 2024 in jdaviz/app.py

View check run for this annotation

Codecov / codecov/patch

jdaviz/app.py#L2024

Added line #L2024 was not covered by tests
else:
subset_selected = self.session.edit_subset_mode.edit_subset[0].label

# remove the current selection label from the set of labels, because its ok
# if the new subset shares the name of the current selection (renaming to current name)
if subset_selected in self._reserved_labels:
self._reserved_labels.remove(subset_selected)

# now check `subset_name` against list of non-active current subset labels
# and warn and return if it is
if subset_name in self._reserved_labels:
if raise_if_invalid:
raise ValueError("Cannot rename subset to name of an existing subset"
f" or data item: ({subset_name}).")
return False

Check warning on line 2039 in jdaviz/app.py

View check run for this annotation

Codecov / codecov/patch

jdaviz/app.py#L2039

Added line #L2039 was not covered by tests

elif not subset_name.replace(" ", "").isalnum():
if raise_if_invalid:
raise ValueError("Subset labels must be purely alphanumeric")
return False

Check warning on line 2044 in jdaviz/app.py

View check run for this annotation

Codecov / codecov/patch

jdaviz/app.py#L2042-L2044

Added lines #L2042 - L2044 were not covered by tests

else:
split_label = subset_name.split(" ")
if split_label[0] == "Subset" and split_label[1].isdigit():
if raise_if_invalid:
raise ValueError("The pattern 'Subset N' is reserved for "
"auto-generated labels")
return False

Check warning on line 2052 in jdaviz/app.py

View check run for this annotation

Codecov / codecov/patch

jdaviz/app.py#L2052

Added line #L2052 was not covered by tests

return True

def _rename_subset(self, old_label, new_label, subset_group=None, check_valid=True):
# Change the label of a subset, making sure it propagates to as many places as it can
# I don't think there's an easier way to get subset_group by label, it's just a tuple
if subset_group is None:
for s in self.data_collection.subset_groups:
if s.label == old_label:
subset_group = s
break
# If we couldn't find a matching subset group, raise an error
else:
raise ValueError(f"No subset named {old_label} to rename")

if check_valid:
if self._check_valid_subset_label(new_label):
subset_group.label = new_label
else:
subset_group.label = new_label

Check warning on line 2072 in jdaviz/app.py

View check run for this annotation

Codecov / codecov/patch

jdaviz/app.py#L2072

Added line #L2072 was not covered by tests

# Update layer icon
self.state.layer_icons[new_label] = self.state.layer_icons[old_label]
_ = self.state.layer_icons.pop(old_label)

# Updated derived data if applicable
for d in self.data_collection:
data_renamed = False
# Extracted spectra are named, e.g., 'Data (Subset 1, sum)'
if d.label.split("(")[-1].split(",")[0] == old_label:
old_data_label = d.label
new_data_label = d.label.replace(old_label, new_label)
d.label = new_data_label
self.state.layer_icons[new_data_label] = self.state.layer_icons[old_data_label]
_ = self.state.layer_icons.pop(old_data_label)

# Update the entries in the old data menu
for data_item in self.state.data_items:
if data_item['name'] == old_data_label:
data_item['name'] = new_data_label

# Update live plugin results subscriptions
if hasattr(d, 'meta') and '_update_live_plugin_results' in d.meta:
results_dict = d.meta['_update_live_plugin_results']
for key in results_dict.get('_subscriptions', {}).get('subset'):
if results_dict[key] == old_label:
results_dict[key] = new_label

if data_renamed:
results_dict['add_results']['label'] = new_data_label

Check warning on line 2102 in jdaviz/app.py

View check run for this annotation

Codecov / codecov/patch

jdaviz/app.py#L2102

Added line #L2102 was not covered by tests

d.meta['_update_live_plugin_results'] = results_dict

self.hub.broadcast(SubsetRenameMessage(subset_group, old_label, new_label, sender=self))

def _reparent_subsets(self, old_parent, new_parent=None):
'''
Re-parent subsets that belong to the specified data
Expand Down Expand Up @@ -2380,6 +2497,7 @@
self._link_new_data()
data_item = self._create_data_item(msg.data)
self.state.data_items.append(data_item)
self._reserved_labels.add(msg.data.label)

def _clear_object_cache(self, data_label=None):
if data_label is None:
Expand Down
56 changes: 48 additions & 8 deletions jdaviz/configs/default/plugins/subset_tools/subset_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from astropy.time import Time
import astropy.units as u
from functools import cached_property
from glue.core.message import EditSubsetMessage, SubsetUpdateMessage
from glue.core.edit_subset_mode import (AndMode, AndNotMode, OrMode,
ReplaceMode, XorMode, NewMode)
Expand Down Expand Up @@ -166,8 +167,11 @@

@property
def user_api(self):
expose = ['subset', 'combination_mode', 'recenter_dataset', 'recenter',
'get_center', 'set_center', 'import_region', 'get_regions']
expose = ['subset', 'combination_mode',
'recenter_dataset', 'recenter',
'get_center', 'set_center',
'import_region', 'get_regions',
'rename_selected', 'rename_subset']
kecnry marked this conversation as resolved.
Show resolved Hide resolved
return PluginUserApi(self, expose)

def get_regions(self, region_type=None, list_of_subset_labels=None,
Expand Down Expand Up @@ -237,10 +241,10 @@
subset_data = lyr.layer
subset_label = subset_data.label

# TODO: Remove this when Jdaviz support round-tripping, see
# https://github.com/spacetelescope/jdaviz/pull/721
if not subset_label.startswith('Subset'):
if isinstance(subset_data.subset_state, MaskSubsetState):
# Ignore MaskSubsetState here
continue

try:
if self.app.config == "imviz" and to_sky:
region = roi_subset_state_to_region(subset_data.subset_state,
Expand Down Expand Up @@ -300,16 +304,16 @@
self.show_region_info = True
self._update_combination_mode()

def _on_subset_update(self, *args):
self._sync_selected_from_state(*args)
def _on_subset_update(self, msg):
self._sync_selected_from_state()
if 'Create New' in self.subset_selected:
return
subsets_avail = [sg.label for sg in self.app.data_collection.subset_groups]
if self.subset_selected not in subsets_avail:
# subset selection should re-default after processing the deleted subset,
# for now we can safely ignore
return
self._get_subset_definition(*args)
self._get_subset_definition()
subset_to_update = self.session.edit_subset_mode.edit_subset[0]
self.subset._update_subset(subset_to_update, attribute="type")

Expand Down Expand Up @@ -839,6 +843,42 @@
self.subset_definitions[index][i]['value'] = new_value
break

@cached_property
def selected_subset_group(self):
for subset_group in self.app.data_collection.subset_groups:
if subset_group.label == self.subset.selected:
return subset_group

def rename_subset(self, old_label, new_label):
"""
Method to rename an existing subset

Parameters
----------
old_label : str
The current label of the subset to be renamed.
new_label : str
The new label to apply to the selected subset.
"""
self.app._rename_subset(old_label, new_label)
self._sync_available_from_state()

def rename_selected(self, new_label):
Copy link
Member

Choose a reason for hiding this comment

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

Is this more performant or just a shortcut (in which case I'm not sure if its worth the extra code and test-coverage or not, but don't have a strong opinion)?

Just a note to future selves: eventually we likely will want to migrate this logic into the subset dropdown itself (so these would become subset_tools.subset.rename...) since we're planning to use an editable select component, but I think it still makes sense to expose methods at the top-level in this plugin for convenience, so we can always just move the logic there and then have these methods call that instead.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This was just a shortcut that I thought would be useful once we hook it up to the front end, for the case you called out.

"""
Method to rename the subset currently selected in the Subset Tools plugin.

Parameters
----------
new_label : str
The new label to apply to the selected subset.
"""
subset_group = self.selected_subset_group
if subset_group is None:
raise TypeError("current selection is not a subset")

Check warning on line 877 in jdaviz/configs/default/plugins/subset_tools/subset_tools.py

View check run for this annotation

Codecov / codecov/patch

jdaviz/configs/default/plugins/subset_tools/subset_tools.py#L877

Added line #L877 was not covered by tests

self.app._rename_subset(self.subset.selected, new_label, subset_group=subset_group)
self._sync_available_from_state()

def import_region(self, region, combination_mode=None, max_num_regions=None,
refdata_label=None, return_bad_regions=False, **kwargs):
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -315,3 +315,50 @@ def test_get_regions_composite(cubeviz_helper, spectrum1d_cube):
# make sure the same regions are returned by app.get_subsets
get_subsets = cubeviz_helper.app.get_subsets()
assert np.all([get_subsets[ss][0]['region'] == regions[ss] for ss in ss_labels])


def test_check_valid_subset_label(imviz_helper):

# imviz instance with some data
data = NDData(np.ones((50, 50)) * u.nJy)
imviz_helper.load_data(data)

st = imviz_helper.plugins["Subset Tools"]

# apply three subsets, with their default names of `Subset 1`, `Subset 2`, and `Subset 3`
st.import_region(CircularROI(20, 20, 10))
st.subset = "Create New"
st.import_region(CircularROI(25, 25, 10))
st.subset = "Create New"
st.import_region(CircularROI(30, 30, 10))

# we should not be able to rename or add a subset named 'Subset 1'.
# Make sure this warns and returns accordingly.
with pytest.raises(ValueError, match="Cannot rename subset to name of an existing subset"):
st.rename_selected("Subset 1")

with pytest.raises(ValueError, match="The pattern 'Subset N' is reserved"):
st.rename_selected("Subset 5")


def test_rename_subset(cubeviz_helper, spectrum1d_cube):

cubeviz_helper.load_data(spectrum1d_cube)
plg = cubeviz_helper.plugins['Subset Tools']

spatial_reg = CirclePixelRegion(center=PixCoord(x=2, y=2), radius=2)
plg.import_region(spatial_reg, combination_mode='new')
spatial_reg = CirclePixelRegion(center=PixCoord(x=4, y=4), radius=1)
plg.import_region(spatial_reg, combination_mode='new')

plg.rename_subset("Subset 1", "Test Rename")

print(cubeviz_helper.app.data_collection)
assert plg.subset.choices == ['Create New', 'Test Rename', 'Subset 2']
assert cubeviz_helper.app.data_collection[-1].label == "Spectrum (Test Rename, sum)"

plg.rename_selected("Second Test")
assert plg.subset.choices == ['Create New', 'Test Rename', 'Second Test']

with pytest.raises(ValueError, match="No subset named BadLabel to rename"):
plg.rename_subset("BadLabel", "Failure")
4 changes: 2 additions & 2 deletions jdaviz/configs/imviz/plugins/orientation/orientation.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from glue.core.subset import Subset
from glue.core.subset_group import GroupedSubset
from glue.plugins.wcs_autolinking.wcs_autolinking import WCSLink, NoAffineApproximation
from glue.viewers.image.state import ImageSubsetLayerState
from traitlets import List, Unicode, Bool, Dict, observe

from jdaviz.configs.imviz.wcs_utils import (
Expand Down Expand Up @@ -501,8 +502,7 @@ def _on_refdata_change(self, msg):
# ensure that it is never visible:
for layer in viewer.state.layers:
if (
hasattr(layer.layer, 'label') and
layer.layer.label.startswith("Subset") and
isinstance(layer.layer, ImageSubsetLayerState) and
layer.layer.data.meta.get("_WCS_ONLY", False)
):
layer.visible = False
Expand Down
Loading
Loading