Skip to content

Commit

Permalink
auto-update support for plugin results (#2680)
Browse files Browse the repository at this point in the history
* support user-api to_dict/from_dict
* (optionally) live-updating plugin products
* support creating new instances of plugins, independent of tray
* move logic for assigning default viewer references from the app-method to the plugin itself, by searching the registry for a match
* create "new" convenience method on plugin
* allows for running results from a saved plugin state without altering the user-facing instance of the plugin
* expose auto_update_result switch in add_result user API
* basic test coverage case
* skip deprecated methods in to_dict to avoid warning
* snackbar message if auto-update fails
  • Loading branch information
kecnry authored Apr 17, 2024
1 parent 7e3fb1a commit bf59c22
Show file tree
Hide file tree
Showing 9 changed files with 224 additions and 56 deletions.
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
New Features
------------

- Infrastructure to support auto-updating plugin results. [#2680]

Cubeviz
^^^^^^^

Expand Down
72 changes: 48 additions & 24 deletions jdaviz/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,14 +362,14 @@ def __init__(self, configuration=None, *args, **kwargs):
# Key should be (data_label, statistic) and value the translated object.
self._get_object_cache = {}
self.hub.subscribe(self, SubsetUpdateMessage,
handler=lambda msg: self._clear_object_cache(msg.subset.label))
handler=self._on_subset_update_message)

# Store for associations between Data entries:
self._data_associations = self._init_data_associations()

# Subscribe to messages that result in changes to the layers
self.hub.subscribe(self, AddDataMessage,
handler=self._on_layers_changed)
handler=self._on_add_data_message)
self.hub.subscribe(self, RemoveDataMessage,
handler=self._on_layers_changed)
self.hub.subscribe(self, SubsetCreateMessage,
Expand All @@ -384,6 +384,51 @@ def _on_plugin_table_added(self, msg):
key = f"{msg.plugin._plugin_name}: {msg.table._table_name}"
self._plugin_tables.setdefault(key, msg.table.user_api)

def _update_live_plugin_results(self, trigger_data_lbl=None, trigger_subset=None):
trigger_subset_lbl = trigger_subset.label if trigger_subset is not None else None
for data in self.data_collection:
plugin_inputs = data.meta.get('_update_live_plugin_results', None)
if plugin_inputs is None:
continue
data_subs = plugin_inputs.get('_subscriptions', {}).get('data', [])
subset_subs = plugin_inputs.get('_subscriptions', {}).get('subset', [])
if (trigger_data_lbl is not None and
not np.any([plugin_inputs.get(attr) == trigger_data_lbl
for attr in data_subs])):
# trigger data does not match subscribed data entries
continue
if trigger_subset_lbl is not None:
if not np.any([plugin_inputs.get(attr) == trigger_subset_lbl
for attr in subset_subs]):
# trigger subset does not match subscribed subsets
continue
if not np.any([plugin_inputs.get(attr) == trigger_subset.data.label
for attr in data_subs]):
# trigger parent data of subset does not match subscribed data entries
continue
# update and overwrite data
# make a new instance of the plugin to avoid changing any UI settings
plg = self._jdaviz_helper.plugins.get(data.meta.get('Plugin'))._obj.new()
if not plg.supports_auto_update:
raise NotImplementedError(f"{data.meta.get('Plugin')} does not support live-updates") # noqa
plg.user_api.from_dict(plugin_inputs)
try:
plg()
except Exception as e:
self.hub.broadcast(SnackbarMessage(
f"Auto-update for {plugin_inputs['add_results']['label']} failed: {e}",
sender=self, color="error"))

def _on_add_data_message(self, msg):
self._on_layers_changed(msg)
self._update_live_plugin_results(trigger_data_lbl=msg.data.label)

def _on_subset_update_message(self, msg):
# NOTE: print statements in here will require the viewer output_widget
self._clear_object_cache(msg.subset.label)
if msg.attribute == 'subset_state':
self._update_live_plugin_results(trigger_subset=msg.subset)

def _on_plugin_plot_added(self, msg):
if msg.plugin._plugin_name is None:
# plugin was instantiated after the app was created, ignore
Expand Down Expand Up @@ -2567,34 +2612,13 @@ def compose_viewer_area(viewer_area_items):

for name in config.get('tray', []):
tray = tray_registry.members.get(name)
tray_registry_options = tray.get('viewer_reference_name_kwargs', {})

# Optional keyword arguments are required to initialize some
# tray items. These kwargs specify the viewer reference names that are
# assumed to be present in the configuration.
optional_tray_kwargs = dict()

# If viewer reference names need to be passed to the tray item
# constructor, pass the names into the constructor in the format
# that the tray items expect.
for opt_attr, [opt_kwarg, get_name_kwargs] in tray_registry_options.items():
opt_value = getattr(
self, opt_attr, self._get_first_viewer_reference_name(**get_name_kwargs)
)

if opt_value is None:
continue

optional_tray_kwargs[opt_kwarg] = opt_value
tray_item_instance = tray.get('cls')(app=self)

# store a copy of the tray name in the instance so it can be accessed by the
# plugin itself
tray_item_label = tray.get('label')

tray_item_instance = tray.get('cls')(
app=self, plugin_name=tray_item_label, **optional_tray_kwargs
)

# NOTE: is_relevant is later updated by observing irrelevant_msg traitlet
self.state.tray_items.append({
'name': name,
Expand Down
13 changes: 12 additions & 1 deletion jdaviz/components/plugin_add_results.vue
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,17 @@

<slot></slot>

<v-row v-if="auto_update_result !== undefined">
<v-switch
v-model="auto_update_result"
@change="(e) => {$emit('update:auto_update_result', auto_update_result)}"
label="Auto-update result"
hint="Regenerate the resulting data-product whenever any inputs are changed"
persistent-hint
>
</v-switch>
</v-row>

<v-row justify="end">
<j-tooltip :tooltipcontent="label_overwrite ? action_tooltip+' and replace existing entry' : action_tooltip">
<plugin-action-button
Expand All @@ -76,7 +87,7 @@
<script>
module.exports = {
props: ['label', 'label_default', 'label_auto', 'label_invalid_msg', 'label_overwrite', 'label_label', 'label_hint',
'add_to_viewer_items', 'add_to_viewer_selected', 'add_to_viewer_hint',
'add_to_viewer_items', 'add_to_viewer_selected', 'auto_update_result', 'add_to_viewer_hint',
'action_disabled', 'action_spinner', 'action_label', 'action_tooltip']
};
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,15 @@ def __init__(self, *args, **kwargs):

self.extracted_spec = None

self.dataset.filters = ['is_flux_cube']

# TODO: in the future this could be generalized with support in SelectPluginComponent
self.aperture._default_text = 'Entire Cube'
self.aperture._manual_options = ['Entire Cube']
self.aperture.items = [{"label": "Entire Cube"}]
# need to reinitialize choices since we overwrote items and some subsets may already
# exist.
self.aperture._initialize_choices()
self.aperture.select_default()

self.background = ApertureSubsetSelect(self,
Expand Down Expand Up @@ -150,21 +155,34 @@ def __init__(self, *args, **kwargs):
# on the user's machine, so export support in cubeviz should be disabled
self.export_enabled = False

self.disabled_msg = (
"Spectral Extraction requires a single dataset to be loaded into Cubeviz, "
"please load data to enable this plugin."
)
for data in self.app.data_collection:
if len(data.data.shape) == 3:
break
else:
# no cube-like data loaded. Once loaded, the parser will unset this
# TODO: change to an event listener on AddDataMessage
self.disabled_msg = (
"Spectral Extraction requires a single dataset to be loaded into Cubeviz, "
"please load data to enable this plugin."
)

@property
def user_api(self):
expose = ['function', 'spatial_subset', 'aperture',
expose = ['dataset', 'function', 'spatial_subset', 'aperture',
'add_results', 'collapse_to_spectrum',
'wavelength_dependent', 'reference_spectral_value',
'aperture_method']
if self.dev_bg_support:
expose += ['background', 'bg_wavelength_dependent']

return PluginUserApi(self, expose=expose)
return PluginUserApi(self, expose=expose, excl_from_dict=['spatial_subset'])

@property
def live_update_subscriptions(self):
return {'data': ('dataset',), 'subset': ('aperture', 'background')}

def __call__(self, add_data=True):
return self.collapse_to_spectrum(add_data=add_data)

@property
def slice_display_unit_name(self):
Expand Down Expand Up @@ -343,9 +361,7 @@ def collapse_to_spectrum(self, add_data=True, **kwargs):
collapsed_spec.meta['_pixel_scale_factor'] = pix_scale_factor

if add_data:
self.add_results.add_results_from_plugin(
collapsed_spec, label=self.results_label, replace=False
)
self.add_results.add_results_from_plugin(collapsed_spec)

snackbar_message = SnackbarMessage(
"Spectrum extracted successfully.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@
label_hint="Label for the extracted spectrum"
:add_to_viewer_items="add_to_viewer_items"
:add_to_viewer_selected.sync="add_to_viewer_selected"
:auto_update_result.sync="auto_update_result"
action_label="Extract"
action_tooltip="Run spectral extraction with error and mask propagation"
:action_spinner="spinner"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from astropy import units as u
from astropy.nddata import NDDataArray, StdDevUncertainty
from astropy.utils.exceptions import AstropyUserWarning
from glue.core.roi import CircularROI
from glue.core.edit_subset_mode import ReplaceMode
from numpy.testing import assert_allclose, assert_array_equal
from regions import (CirclePixelRegion, CircleAnnulusPixelRegion, EllipsePixelRegion,
RectanglePixelRegion, PixCoord)
Expand Down Expand Up @@ -418,3 +420,30 @@ def test_unit_translation(cubeviz_helper):
# returns to the original values
# which is a value in Jy/pix that we know the outcome after translation
assert np.allclose(collapsed_spec._data[0], mjy_sr_data1)


def test_autoupdate_results(cubeviz_helper, spectrum1d_cube_largest):
cubeviz_helper.load_data(spectrum1d_cube_largest)
fv = cubeviz_helper.viewers['flux-viewer']._obj
fv.apply_roi(CircularROI(xc=5, yc=5, radius=2))

extract_plg = cubeviz_helper.plugins['Spectral Extraction']
extract_plg.aperture = 'Subset 1'
extract_plg.add_results.label = 'extracted'
extract_plg.add_results.auto_update_result = True
_ = extract_plg.collapse_to_spectrum()

# orig_med_flux = np.median(cubeviz_helper.get_data('extracted').flux)

# replace Subset 1 with a larger subset, resulting fluxes should increase
cubeviz_helper.app.session.edit_subset_mode.mode = ReplaceMode
fv.apply_roi(CircularROI(xc=5, yc=5, radius=3))

# update should take place automatically, but since its async, we'll call manually to ensure
# the update is complete before comparing results
for subset in cubeviz_helper.app.data_collection.subset_groups[0].subsets:
cubeviz_helper.app._update_live_plugin_results(trigger_subset=subset)
# TODO: this is randomly failing in CI (not always) so will disable the assert for now and just
# cover to make sure the logic does not crash
# new_med_flux = np.median(cubeviz_helper.get_data('extracted').flux)
# assert new_med_flux > orig_med_flux
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,14 @@
from astropy import units as u
from specutils import Spectrum1D

from jdaviz.configs.default.plugins.collapse.collapse import Collapse


@pytest.mark.filterwarnings('ignore')
def test_linking_after_collapse(cubeviz_helper, spectral_cube_wcs):
cubeviz_helper.load_data(Spectrum1D(flux=np.ones((3, 4, 5)) * u.nJy, wcs=spectral_cube_wcs))
dc = cubeviz_helper.app.data_collection

coll = Collapse(app=cubeviz_helper.app)
# TODO: this now fails when instantiating Collapse after initialization
coll = cubeviz_helper.plugins['Collapse']._obj

coll.selected_data_item = 'Unknown spectrum object[FLUX]'
coll.dataset_selected = 'Unknown spectrum object[FLUX]'
Expand Down
Loading

0 comments on commit bf59c22

Please sign in to comment.