From 44e1e86e15c32259e331f433a29fe5f2906d08fb Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Mon, 5 Jun 2023 15:14:00 -0400 Subject: [PATCH 01/24] basic binning plugin --- lcviz/components/plugin_ephemeris_select.vue | 50 ++++++ lcviz/events.py | 28 ++-- lcviz/helper.py | 10 +- lcviz/marks.py | 8 +- lcviz/plugins/__init__.py | 1 + lcviz/plugins/binning/__init__.py | 1 + lcviz/plugins/binning/binning.py | 156 +++++++++++++++++++ lcviz/plugins/binning/binning.vue | 73 +++++++++ lcviz/plugins/ephemeris/ephemeris.py | 54 ++++++- lcviz/template_mixin.py | 149 +++++++++++++++++- 10 files changed, 512 insertions(+), 18 deletions(-) create mode 100644 lcviz/components/plugin_ephemeris_select.vue create mode 100644 lcviz/plugins/binning/__init__.py create mode 100644 lcviz/plugins/binning/binning.py create mode 100644 lcviz/plugins/binning/binning.vue diff --git a/lcviz/components/plugin_ephemeris_select.vue b/lcviz/components/plugin_ephemeris_select.vue new file mode 100644 index 00000000..c06bc90f --- /dev/null +++ b/lcviz/components/plugin_ephemeris_select.vue @@ -0,0 +1,50 @@ + + + + diff --git a/lcviz/events.py b/lcviz/events.py index 9d9922c2..ffa1b4e6 100644 --- a/lcviz/events.py +++ b/lcviz/events.py @@ -1,6 +1,6 @@ from glue.core.message import Message -__all__ = ['ViewerRenamedMessage'] +__all__ = ['ViewerRenamedMessage', 'EphemerisComponentChangedMessage'] class ViewerRenamedMessage(Message): @@ -8,13 +8,23 @@ class ViewerRenamedMessage(Message): def __init__(self, old_viewer_ref, new_viewer_ref, *args, **kwargs): super().__init__(*args, **kwargs) - self._old_viewer_ref = old_viewer_ref - self._new_viewer_ref = new_viewer_ref + self.old_viewer_ref = old_viewer_ref + self.new_viewer_ref = new_viewer_ref - @property - def old_viewer_ref(self): - return self._old_viewer_ref - @property - def new_viewer_ref(self): - return self._new_viewer_ref +class EphemerisComponentChangedMessage(Message): + """Message emitted when an ephemeris component is added/renamed/removed in the + ephemeris plugin""" + def __init__(self, old_lbl, new_lbl, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.old_lbl = old_lbl + self.new_lbl = new_lbl + if old_lbl is not None and new_lbl is not None: + self.type = 'rename' + elif old_lbl is None and new_lbl is not None: + self.type = 'add' + elif old_lbl is not None and new_lbl is None: + self.type = 'remove' + else: + raise ValueError("must provide at least one of old_lbl or new_lbl") diff --git a/lcviz/helper.py b/lcviz/helper.py index 59fc5c66..2f123ed7 100644 --- a/lcviz/helper.py +++ b/lcviz/helper.py @@ -10,7 +10,11 @@ __all__ = ['LCviz'] -custom_components = {'lcviz-editable-select': 'components/plugin_editable_select.vue'} + +_default_time_viewer_reference_name = 'flux-vs-time' + +custom_components = {'lcviz-editable-select': 'components/plugin_editable_select.vue', + 'plugin-ephemeris-select': 'components/plugin_ephemeris_select.vue'} # Register pure vue component. This allows us to do recursive component instantiation only in the # vue component file @@ -112,7 +116,7 @@ class LCviz(ConfigHelper): 'toolbar': ['g-data-tools', 'g-subset-tools', 'lcviz-coords-info'], 'tray': ['lcviz-metadata-viewer', 'lcviz-plot-options', 'lcviz-subset-plugin', 'lcviz-markers', 'flatten', 'frequency-analysis', 'ephemeris', - 'lcviz-export-plot'], + 'binning', 'lcviz-export-plot'], 'viewer_area': [{'container': 'col', 'children': [{'container': 'row', 'viewers': [{'name': 'flux-vs-time', @@ -121,7 +125,7 @@ class LCviz(ConfigHelper): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self._default_time_viewer_reference_name = 'flux-vs-time' + self._default_time_viewer_reference_name = _default_time_viewer_reference_name # override jdaviz behavior to support temporal subsets self.app._get_range_subset_bounds = ( diff --git a/lcviz/marks.py b/lcviz/marks.py index dab00654..ebac086a 100644 --- a/lcviz/marks.py +++ b/lcviz/marks.py @@ -3,7 +3,7 @@ from jdaviz.core.marks import PluginLine, PluginScatter from lcviz.viewers import PhaseScatterView -__all__ = ['LivePreviewTrend', 'LivePreviewFlattened'] +__all__ = ['LivePreviewTrend', 'LivePreviewFlattened', 'LivePreviewBinning'] class WithoutPhaseSupport: @@ -41,3 +41,9 @@ def __init__(self, viewer, *args, **kwargs): self.viewer = viewer kwargs.setdefault('default_size', 16) super().__init__(viewer, *args, **kwargs) + + +class LivePreviewBinning(PluginScatter): + def __init__(self, *args, **kwargs): + kwargs.setdefault('default_size', 16) + super().__init__(*args, **kwargs) diff --git a/lcviz/plugins/__init__.py b/lcviz/plugins/__init__.py index c9180ba3..dacdb581 100644 --- a/lcviz/plugins/__init__.py +++ b/lcviz/plugins/__init__.py @@ -1,3 +1,4 @@ +from .binning.binning import * # noqa from .coords_info.coords_info import * # noqa from .ephemeris.ephemeris import * # noqa from .export_plot.export_plot import * # noqa diff --git a/lcviz/plugins/binning/__init__.py b/lcviz/plugins/binning/__init__.py new file mode 100644 index 00000000..33cefb85 --- /dev/null +++ b/lcviz/plugins/binning/__init__.py @@ -0,0 +1 @@ +from .binning import * # noqa diff --git a/lcviz/plugins/binning/binning.py b/lcviz/plugins/binning/binning.py new file mode 100644 index 00000000..d4783dbf --- /dev/null +++ b/lcviz/plugins/binning/binning.py @@ -0,0 +1,156 @@ +from traitlets import Bool, observe + +from jdaviz.core.custom_traitlets import IntHandleEmpty +from jdaviz.core.events import (ViewerAddedMessage, ViewerRemovedMessage) +from jdaviz.core.registries import tray_registry +from jdaviz.core.template_mixin import (PluginTemplateMixin, + DatasetSelectMixin, AddResultsMixin) +from jdaviz.core.user_api import PluginUserApi + +from lcviz.helper import _default_time_viewer_reference_name +from lcviz.marks import LivePreviewBinning +from lcviz.template_mixin import EphemerisSelectMixin + + +__all__ = ['Binning'] + + +@tray_registry('binning', label="Binning") +class Binning(PluginTemplateMixin, DatasetSelectMixin, EphemerisSelectMixin, AddResultsMixin): + """ + See the :ref:`Binning Plugin Documentation ` for more details. + + Only the following attributes and methods are available through the + public plugin API. + + * ``dataset`` (:class:`~jdaviz.core.template_mixin.DatasetSelect`): + Dataset to bin. + * ``ephemeris`` (:class:`~jdaviz.core.template_mixin.SelectPluginComponent`): + Label of the component corresponding to the active ephemeris. + * :meth:`input_lc` + Data used as input to binning, based on ``dataset`` and ``ephemeris``. + * ``add_results`` (:class:`~jdaviz.core.template_mixin.AddResults`) + * :meth:`bin` + """ + template_file = __file__, "binning.vue" + + show_live_preview = Bool(True).tag(sync=True) + + n_bins = IntHandleEmpty(100).tag(sync=True) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self._set_results_viewer() + + # TODO: replace with add_filter('not_from_this_plugin') if upstream PR accepted/released + # https://github.com/spacetelescope/jdaviz/pull/2239 + def not_from_binning_plugin(data): + return data.meta.get('Plugin', None) != self.__class__.__name__ + self.dataset.add_filter(not_from_binning_plugin) + + self.hub.subscribe(self, ViewerAddedMessage, handler=self._set_results_viewer) + self.hub.subscribe(self, ViewerRemovedMessage, handler=self._set_results_viewer) + + @property + def user_api(self): + expose = ['dataset', 'ephemeris', 'input_lc', 'add_results', 'bin'] + return PluginUserApi(self, expose=expose) + + @property + def ephemeris_plugin(self): + return self.ephemeris.ephemeris_plugin + + @property + def input_lc(self): + return self.ephemeris.get_data_for_dataset(self.dataset) + + @property + def marks(self): + marks = {} + for id, viewer in self.app._viewer_store.items(): + for mark in viewer.figure.marks: + if isinstance(mark, LivePreviewBinning): + marks[id] = mark + break + else: + mark = LivePreviewBinning(viewer, visible=self.plugin_opened) + viewer.figure.marks = viewer.figure.marks + [mark] + marks[id] = mark + return marks + + def _clear_marks(self): + for mark in self.marks.values(): + if mark.visible: + mark.clear() + mark.visible = False + + @observe("dataset_selected", "ephemeris_selected") + def _set_default_results_label(self, event={}): + '''Generate a label and set the results field to that value''' + if not hasattr(self, 'ephemeris'): + return + label = f"binned {self.dataset_selected}" + if self.ephemeris_selected not in self.ephemeris._manual_options: + label += f":{self.ephemeris_selected}" + self.results_label_default = label + + @observe("ephemeris_selected") + def _set_results_viewer(self, event={}): + if not hasattr(self, 'ephemeris'): + return + + def viewer_filter(viewer): + if self.ephemeris_selected in self.ephemeris._manual_options: + return viewer.reference == _default_time_viewer_reference_name + if 'flux-vs-phase:' not in viewer.reference: + # ephemeris selected, but no active phase viewers + return False + return viewer.reference.split('flux-vs-phase:')[1] == self.ephemeris_selected + + self.add_results.viewer.filters = [viewer_filter] + + @observe('show_live_preview', 'plugin_opened', + 'dataset_selected', 'ephemeris_selected', + 'n_bins') + def _live_update(self, event={}): + if not self.show_live_preview or not self.plugin_opened: + self._clear_marks() + return + + lc = self.bin(add_data=False) + + if self.ephemeris_selected == 'No ephemeris': + ref_time = lc.meta.get('reference_time', 0) + times = lc.time - ref_time + else: + times = lc.time + + for viewer_id, mark in self.marks.items(): + if self.ephemeris_selected == 'No ephemeris': + # TODO: change to be visible in all viewers, but re-phasing on ephemeris change using markers logic + visible = viewer_id == 'lcviz-0' # TODO: fix this to be general and not rely on ugly id + else: + # TODO: re-bin on change to selected ephemeris + visible = viewer_id.split(':')[-1] == self.ephemeris_selected + + if visible: + mark.update_xy(times.value, lc.flux.value) + else: + mark.clear() + mark.visible = visible + + def bin(self, add_data=True): + lc = self.input_lc + # TODO: this is raising separate errors in time and phase-space, likely because of + # roundtripping issues in the translators + lc = lc.bin(time_bin_size=(lc.time[-1]-lc.time[0]).value/self.n_bins) + + if add_data: + # add data to the collection/viewer + self.add_results.add_results_from_plugin(lc) + + return lc + + def vue_apply(self, event={}): + self.bin(add_data=True) diff --git a/lcviz/plugins/binning/binning.vue b/lcviz/plugins/binning/binning.vue new file mode 100644 index 00000000..7b2a6143 --- /dev/null +++ b/lcviz/plugins/binning/binning.vue @@ -0,0 +1,73 @@ + diff --git a/lcviz/plugins/ephemeris/ephemeris.py b/lcviz/plugins/ephemeris/ephemeris.py index ea7a277d..8a797b97 100644 --- a/lcviz/plugins/ephemeris/ephemeris.py +++ b/lcviz/plugins/ephemeris/ephemeris.py @@ -1,4 +1,5 @@ import numpy as np +from astropy.time import Time from traitlets import Bool, Float, List, Unicode, observe from glue.core.component_id import ComponentID @@ -11,8 +12,9 @@ DatasetSelectMixin) from jdaviz.core.user_api import PluginUserApi -from lightkurve import periodogram +from lightkurve import periodogram, FoldedLightCurve +from lcviz.events import EphemerisComponentChangedMessage from lcviz.template_mixin import EditableSelectPluginComponent from lcviz.viewers import PhaseScatterView @@ -49,6 +51,7 @@ class Ephemeris(PluginTemplateMixin, DatasetSelectMixin): * :meth:`add_component` * :meth:`rename_component` * :meth:`times_to_phases` + * :meth:`get_data` * ``dataset`` (:class:`~jdaviz.core.template_mixin.DatasetSelect`): Dataset to use for determining the period. * ``method`` (:class:`~jdaviz.core.template_mixing.SelectPluginComponent`): @@ -85,6 +88,7 @@ class Ephemeris(PluginTemplateMixin, DatasetSelectMixin): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + self._default_initialized = False self._ignore_ephem_change = False self._ephemerides = {} self._prev_wrap_at = _default_wrap_at @@ -95,6 +99,7 @@ def __init__(self, *args, **kwargs): items='component_items', selected='component_selected', manual_options=['default'], + on_add=self._on_component_add, on_rename=self._on_component_rename, on_remove=self._on_component_remove) # force the original entry in ephemerides with defaults @@ -117,7 +122,7 @@ def user_api(self): 'ephemeris', 'ephemerides', 'update_ephemeris', 'create_phase_viewer', 'add_component', 'remove_component', 'rename_component', - 'times_to_phases', + 'times_to_phases', 'get_data', 'dataset', 'method'] return PluginUserApi(self, expose=expose) @@ -285,12 +290,18 @@ def vue_period_double(self, *args): def _check_if_phase_viewer_exists(self, *args): self.phase_viewer_exists = self.phase_viewer_id in self.app.get_viewer_ids() + def _on_component_add(self, lbl): + self.hub.broadcast(EphemerisComponentChangedMessage(old_lbl=None, new_lbl=lbl, + sender=self)) + def _on_component_rename(self, old_lbl, new_lbl): # this is triggered when the plugin component detects a change to the component name self._ephemerides[new_lbl] = self._ephemerides.pop(old_lbl, {}) if self._phase_viewer_id(old_lbl) in self.app.get_viewer_ids(): self.app._rename_viewer(self._phase_viewer_id(old_lbl), self._phase_viewer_id(new_lbl)) self._check_if_phase_viewer_exists() + self.hub.broadcast(EphemerisComponentChangedMessage(old_lbl=old_lbl, new_lbl=new_lbl, + sender=self)) def _on_component_remove(self, lbl): _ = self._ephemerides.pop(lbl, {}) @@ -301,6 +312,8 @@ def _on_component_remove(self, lbl): cid = viewer_item.get('id', None) if cid is not None: self.app.vue_destroy_viewer_item(cid) + self.hub.broadcast(EphemerisComponentChangedMessage(old_lbl=lbl, new_lbl=None, + sender=self)) def rename_component(self, old_lbl, new_lbl): # NOTE: the component will call _on_component_rename after updating @@ -419,6 +432,11 @@ def round_to_1(x): 1./1000000) self.t0_step = round_to_1(self.period/1000) + if not self._default_initialized: + # other plugins that use EphemerisSelect don't see the first entry yet + self._default_initialized = True + self._on_component_add(self.component_selected) + @observe('dataset_selected', 'method_selected') def _update_periodogram(self, *args): if not (hasattr(self, 'method') and hasattr(self, 'dataset')): @@ -451,3 +469,35 @@ def _update_periodogram(self, *args): def vue_adopt_period_at_max_power(self, *args): self.period = self.period_at_max_power + + def get_data(self, dataset, component=None): + # TODO: support subset_to_apply and then include a wrapper at the helper-level? + # (would need to catch when cls does not result in a lightkurve object or write + # behaviors for other cases as well) + if component is None: + component = self.component.selected + + lc = self.app._jdaviz_helper.get_data(dataset) + data = next((x for x in self.app.data_collection if x.label == dataset)) + + comps = {str(comp): comp for comp in data.components} + xcomp = f'phase:{component}' + phases = data.get_component(comps.get(xcomp)).data + + # the following code is adopted directly from lightkurve + # 2. Create the folded object + phlc = FoldedLightCurve(data=lc) + # 3. Restore the folded time + with phlc._delay_required_column_checks(): + phlc.remove_column("time") + # TODO: phased lc shouldn't have the same time format/scale, but this is needed + # in order for binning to work (until there's a fix to lightkurve) + phlc.add_column(Time(phases, format=lc.time.format, scale=lc.time.scale), name="time", index=0) + + # Add extra column and meta data specific to FoldedLightCurve + ephemeris = self.ephemerides.get(component) + phlc.meta["PERIOD"] = ephemeris.get('period') + phlc.meta["EPOCH_TIME"] = ephemeris.get('t0') + phlc.sort("time") + + return phlc diff --git a/lcviz/template_mixin.py b/lcviz/template_mixin.py index a9b4ddc3..47bddb7c 100644 --- a/lcviz/template_mixin.py +++ b/lcviz/template_mixin.py @@ -1,10 +1,17 @@ +from functools import cached_property +from traitlets import List, Unicode +from ipyvuetify import VuetifyTemplate +from glue.core import HubListener +from glue.core.data import Data + import jdaviz from jdaviz.core.events import SnackbarMessage -from jdaviz.core.template_mixin import SelectPluginComponent +from jdaviz.core.template_mixin import SelectPluginComponent, DatasetSelect from jdaviz.core.template_mixin import ViewerSelect -from lcviz.events import ViewerRenamedMessage +from lcviz.events import ViewerRenamedMessage, EphemerisComponentChangedMessage -__all__ = ['EditableSelectPluginComponent'] +__all__ = ['EditableSelectPluginComponent', + 'EphemerisSelect', 'EphemerisSelectMixin'] # TODO: remove this if/when jdaviz supports renaming viewers natively @@ -101,3 +108,139 @@ def rename_choice(self, old, new): if was_selected: self.selected = new self._on_rename(old, new) + + +class EphemerisSelect(SelectPluginComponent): + """ + Plugin select for ephemeris components defined by the Ephemeris plugin. + + Useful API methods/attributes: + + * :meth:`~SelectPluginComponent.choices` + * ``selected`` + * :attr:`selected_obj` + * :meth:`~SelectPluginComponent.select_default` + """ + + """ + Traitlets (in the object, custom traitlets in the plugin): + + * ``items`` (list of dicts with keys: label) + * ``selected`` (string) + + Properties (in the object only): + + * ``selected_obj`` + + To use in a plugin: + + * create traitlets with default values + * register with all the automatic logic in the plugin's init by passing the string names + of the respective traitlets + * use component in plugin template (see below) + * refer to properties above based on the interally stored reference to the + instantiated object of this component + + Example template (label and hint are optional):: + + + + """ + def __init__(self, plugin, items, selected, + default_text='No ephemeris', manual_options=[], + default_mode='first'): + """ + Parameters + ---------- + plugin + the parent plugin object + items : str + the name of the items traitlet defined in ``plugin`` + selected : str + the name of the selected traitlet defined in ``plugin`` + default_text : str or None + the text to show for no selection. If not provided or None, no entry will be provided + in the dropdown for no selection. + manual_options: list + list of options to provide that are not automatically populated by ephemerides. If + ``default`` text is provided but not in ``manual_options`` it will still be included as + the first item in the list. + """ + super().__init__(plugin, items=items, selected=selected, + default_text=default_text, manual_options=manual_options, + default_mode=default_mode) + self.hub.subscribe(self, EphemerisComponentChangedMessage, + handler=self._ephem_component_change) + + @cached_property + def ephemeris_plugin(self): + return self.app._jdaviz_helper.plugins.get('Ephemeris', None) + + @cached_property + def selected_obj(self): + if self.selected in self._manual_options: + return None + return self.ephemeris_plugin.ephemerides.get(self.selected, None) + + def get_data_for_dataset(self, dataset, ycomp='flux'): + if not isinstance(dataset, DatasetSelect): + raise ValueError("dataset must be DatasetSelect object") + if self.selected in self._manual_options: + return dataset.selected_obj + return self.ephemeris_plugin.get_data(dataset.selected, self.selected) + + def _ephem_component_change(self, msg=None): + type = getattr(msg, 'type', None) + if type == 'remove' and msg.old_lbl in self.choices: + self.items = [item for item in self.items if item['label'] != msg.old_lbl] + self._apply_default_selection() + elif type == 'rename' and msg.old_lbl in self.choices: + was_selected = self.selected == msg.old_lbl + self.items = [item if item['label'] != msg.old_lbl else {'label': msg.new_lbl} + for item in self.items] + if was_selected: + self.selected = msg.new_lbl + elif type == 'add' and msg.new_lbl not in self.choices: + self.items = self.items + [{'label': msg.new_lbl}] + else: + # something might be out of sync, build from scratch + manual_items = [{'label': label} for label in self.manual_options] + self.items = manual_items + [{'label': component} + for component in self.ephemeris_plugin.ephemerides.keys()] + self._apply_default_selection() + + +class EphemerisSelectMixin(VuetifyTemplate, HubListener): + """ + Applies the EphemerisSelect component as a mixin in the base plugin. This + automatically adds traitlets as well as new properties to the plugin with minimal + extra code. For multiple instances or custom traitlet names/defaults, use the + component instead. + + To use in a plugin: + + * add ``EphemerisSelectMixin`` as a mixin to the class + * use the traitlets available from the plugin or properties/methods available from + ``plugin.ephemeris``. + + Example template (label and hint are optional):: + + + + """ + ephemeris_items = List().tag(sync=True) + ephemeris_selected = Unicode().tag(sync=True) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.ephemeris = EphemerisSelect(self, 'ephemeris_items', 'ephemeris_selected') From 1d9013a13c9e1899664159a70961867b84841df5 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Tue, 6 Jun 2023 09:58:17 -0400 Subject: [PATCH 02/24] binning a phased-lc react to change in ephemeris --- lcviz/events.py | 10 +++++++++- lcviz/plugins/binning/binning.py | 12 +++++++++++- lcviz/plugins/ephemeris/ephemeris.py | 4 +++- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/lcviz/events.py b/lcviz/events.py index ffa1b4e6..708699d9 100644 --- a/lcviz/events.py +++ b/lcviz/events.py @@ -1,6 +1,7 @@ from glue.core.message import Message -__all__ = ['ViewerRenamedMessage', 'EphemerisComponentChangedMessage'] +__all__ = ['ViewerRenamedMessage', 'EphemerisComponentChangedMessage', + 'EphemerisChangedMessage'] class ViewerRenamedMessage(Message): @@ -28,3 +29,10 @@ def __init__(self, old_lbl, new_lbl, *args, **kwargs): self.type = 'remove' else: raise ValueError("must provide at least one of old_lbl or new_lbl") + + +class EphemerisChangedMessage(Message): + """Message emitted when the parameters of an ephemeris are updated/changed + in the ephemeris plugin""" + def __init__(self, ephemeris_label, *args, **kwargs): + self.ephemeris_label = ephemeris_label diff --git a/lcviz/plugins/binning/binning.py b/lcviz/plugins/binning/binning.py index d4783dbf..22bc59b7 100644 --- a/lcviz/plugins/binning/binning.py +++ b/lcviz/plugins/binning/binning.py @@ -7,6 +7,7 @@ DatasetSelectMixin, AddResultsMixin) from jdaviz.core.user_api import PluginUserApi +from lcviz.events import EphemerisChangedMessage from lcviz.helper import _default_time_viewer_reference_name from lcviz.marks import LivePreviewBinning from lcviz.template_mixin import EphemerisSelectMixin @@ -51,6 +52,7 @@ def not_from_binning_plugin(data): self.hub.subscribe(self, ViewerAddedMessage, handler=self._set_results_viewer) self.hub.subscribe(self, ViewerRemovedMessage, handler=self._set_results_viewer) + self.hub.subscribe(self, EphemerisChangedMessage, handler=self._on_ephemeris_update) @property def user_api(self): @@ -131,7 +133,6 @@ def _live_update(self, event={}): # TODO: change to be visible in all viewers, but re-phasing on ephemeris change using markers logic visible = viewer_id == 'lcviz-0' # TODO: fix this to be general and not rely on ugly id else: - # TODO: re-bin on change to selected ephemeris visible = viewer_id.split(':')[-1] == self.ephemeris_selected if visible: @@ -140,6 +141,15 @@ def _live_update(self, event={}): mark.clear() mark.visible = visible + def _on_ephemeris_update(self, msg): + if not self.show_live_preview or not self.plugin_opened: + return + + if msg.ephemeris_label != self.ephemeris_selected: + return + + self._live_update() + def bin(self, add_data=True): lc = self.input_lc # TODO: this is raising separate errors in time and phase-space, likely because of diff --git a/lcviz/plugins/ephemeris/ephemeris.py b/lcviz/plugins/ephemeris/ephemeris.py index 8a797b97..c7de269e 100644 --- a/lcviz/plugins/ephemeris/ephemeris.py +++ b/lcviz/plugins/ephemeris/ephemeris.py @@ -14,7 +14,7 @@ from lightkurve import periodogram, FoldedLightCurve -from lcviz.events import EphemerisComponentChangedMessage +from lcviz.events import EphemerisComponentChangedMessage, EphemerisChangedMessage from lcviz.template_mixin import EditableSelectPluginComponent from lcviz.viewers import PhaseScatterView @@ -390,6 +390,8 @@ def update_ephemeris(self, component=None, t0=None, period=None, dpdt=None, wrap self._ephemerides[component] = existing_ephem self._update_all_phase_arrays(component=component) + self.hub.broadcast(EphemerisChangedMessage(ephemeris_label=component, + sender=self)) return existing_ephem @observe('period', 'dpdt', 't0', 'wrap_at') From 9ec8d3c7959c57facfe1e2ee2e5fabd93d9e5e98 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Tue, 6 Jun 2023 15:26:09 -0400 Subject: [PATCH 03/24] basic test coverage --- lcviz/tests/test_plugin_binning.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 lcviz/tests/test_plugin_binning.py diff --git a/lcviz/tests/test_plugin_binning.py b/lcviz/tests/test_plugin_binning.py new file mode 100644 index 00000000..a38df26b --- /dev/null +++ b/lcviz/tests/test_plugin_binning.py @@ -0,0 +1,26 @@ +from lcviz.marks import LivePreviewBinning + + +def _get_marks_from_viewer(viewer, cls=(LivePreviewBinning)): + return [m for m in viewer.figure.marks if isinstance(m, cls) and m.visible] + + +def test_plugin_binning(helper, light_curve_like_kepler_quarter): + helper.load_data(light_curve_like_kepler_quarter) + tv = helper.app.get_viewer(helper._default_time_viewer_reference_name) + + b = helper.plugins['Binning'] + b.open_in_tray() + ephem = helper.plugins['Ephemeris'] + ephem.period = 1.2345 + pv = ephem.create_phase_viewer() + + assert b.ephemeris == 'No ephemeris' + assert len(_get_marks_from_viewer(tv)) == 1 + assert len(_get_marks_from_viewer(pv)) == 0 + + b.ephemeris = 'default' + assert len(_get_marks_from_viewer(tv)) == 0 + assert len(_get_marks_from_viewer(pv)) == 1 + + b.bin(add_data=True) From 8d0f400a1d769918be5140b6961935418f1feba4 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Wed, 14 Jun 2023 13:05:48 -0400 Subject: [PATCH 04/24] WIP: fix binning in phase-space and support map_to_times --- lcviz/marks.py | 2 +- lcviz/plugins/binning/binning.py | 77 ++++++++++++++++++++++++---- lcviz/plugins/binning/binning.vue | 9 ++++ lcviz/plugins/ephemeris/ephemeris.py | 21 +++++++- lcviz/utils.py | 14 +++-- 5 files changed, 107 insertions(+), 16 deletions(-) diff --git a/lcviz/marks.py b/lcviz/marks.py index ebac086a..b5c89da5 100644 --- a/lcviz/marks.py +++ b/lcviz/marks.py @@ -43,7 +43,7 @@ def __init__(self, viewer, *args, **kwargs): super().__init__(viewer, *args, **kwargs) -class LivePreviewBinning(PluginScatter): +class LivePreviewBinning(PluginScatter, WithPhaseSupport): def __init__(self, *args, **kwargs): kwargs.setdefault('default_size', 16) super().__init__(*args, **kwargs) diff --git a/lcviz/plugins/binning/binning.py b/lcviz/plugins/binning/binning.py index 22bc59b7..008f1486 100644 --- a/lcviz/plugins/binning/binning.py +++ b/lcviz/plugins/binning/binning.py @@ -1,3 +1,5 @@ +from astropy.time import Time +import numpy as np from traitlets import Bool, observe from jdaviz.core.custom_traitlets import IntHandleEmpty @@ -30,6 +32,8 @@ class Binning(PluginTemplateMixin, DatasetSelectMixin, EphemerisSelectMixin, Add Label of the component corresponding to the active ephemeris. * :meth:`input_lc` Data used as input to binning, based on ``dataset`` and ``ephemeris``. + * ``n_bins`` + * ``map_to_times`` * ``add_results`` (:class:`~jdaviz.core.template_mixin.AddResults`) * :meth:`bin` """ @@ -38,6 +42,7 @@ class Binning(PluginTemplateMixin, DatasetSelectMixin, EphemerisSelectMixin, Add show_live_preview = Bool(True).tag(sync=True) n_bins = IntHandleEmpty(100).tag(sync=True) + map_to_times = Bool(True).tag(sync=True) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -56,13 +61,21 @@ def not_from_binning_plugin(data): @property def user_api(self): - expose = ['dataset', 'ephemeris', 'input_lc', 'add_results', 'bin'] + expose = ['dataset', 'ephemeris', 'input_lc', + 'n_bins', 'map_to_times', + 'add_results', 'bin'] return PluginUserApi(self, expose=expose) @property def ephemeris_plugin(self): return self.ephemeris.ephemeris_plugin + @property + def ephemeris_dict(self): + if self.ephemeris_selected == 'No ephemeris': + return {} + return self.ephemeris_plugin.ephemerides.get(self.ephemeris_selected) + @property def input_lc(self): return self.ephemeris.get_data_for_dataset(self.dataset) @@ -114,7 +127,7 @@ def viewer_filter(viewer): @observe('show_live_preview', 'plugin_opened', 'dataset_selected', 'ephemeris_selected', - 'n_bins') + 'n_bins', 'map_to_times') def _live_update(self, event={}): if not self.show_live_preview or not self.plugin_opened: self._clear_marks() @@ -122,21 +135,39 @@ def _live_update(self, event={}): lc = self.bin(add_data=False) - if self.ephemeris_selected == 'No ephemeris': + if self.ephemeris_selected == 'No ephemeris' or self.map_to_times: ref_time = lc.meta.get('reference_time', 0) + ref_time = getattr(ref_time, 'value', ref_time) times = lc.time - ref_time else: times = lc.time + # TODO: remove the need for this (inconsistent quantity vs value setting in lc object) + times = getattr(times, 'value', times) for viewer_id, mark in self.marks.items(): if self.ephemeris_selected == 'No ephemeris': - # TODO: change to be visible in all viewers, but re-phasing on ephemeris change using markers logic - visible = viewer_id == 'lcviz-0' # TODO: fix this to be general and not rely on ugly id + visible = True + # TODO: fix this to be general and not rely on ugly id + do_phase = viewer_id != 'lcviz-0' else: - visible = viewer_id.split(':')[-1] == self.ephemeris_selected + if self.map_to_times: + # then the flux-vs-time viewer gets the data from binning, and all + # flux-vs-phase viewers are converted based on their current ephemeris + + visible = True + # TODO: fix this to be general and not rely on ugly id + do_phase = viewer_id != 'lcviz-0' + else: + # TODO: try to fix flashing as traitlets update + visible = viewer_id.split(':')[-1] == self.ephemeris_selected + do_phase = False if visible: - mark.update_xy(times.value, lc.flux.value) + if do_phase: + mark.update_ty(times, lc.flux.value) + else: + mark.times = [] + mark.update_xy(times, lc.flux.value) else: mark.clear() mark.visible = visible @@ -151,10 +182,34 @@ def _on_ephemeris_update(self, msg): self._live_update() def bin(self, add_data=True): - lc = self.input_lc - # TODO: this is raising separate errors in time and phase-space, likely because of - # roundtripping issues in the translators - lc = lc.bin(time_bin_size=(lc.time[-1]-lc.time[0]).value/self.n_bins) + input_lc = self.input_lc + + lc = input_lc.bin(time_bin_size=(input_lc.time[-1]-input_lc.time[0]).value/self.n_bins) + if self.ephemeris_selected != 'No ephemeris': + times = self.ephemeris_plugin.phases_to_times(lc.time.value, self.ephemeris_selected) + + if self.map_to_times: + # the original starts at time t0 + binned_lc = lc.copy() + t0 = self.ephemeris_dict.get('t0', 0.0) + period = self.ephemeris_dict.get('period', 1.0) + lc.time = times + + # extend forward and backwards in cycles for the full range of input_lc + min_time, max_time = input_lc.time_original.min().value, input_lc.time_original.max().value + for start_time in np.arange(min_time, max_time, period): + this_times = start_time + (times - t0) + if start_time == min_time: + lc.time = this_times + else: + binned_lc.time = this_times + lc = lc.append(binned_lc) + + else: + time_col = Time(times, + format=input_lc.time_original.format, + scale=input_lc.time_original.scale) + lc.add_column(time_col, name="time_original", index=len(lc._required_columns)) if add_data: # add data to the collection/viewer diff --git a/lcviz/plugins/binning/binning.vue b/lcviz/plugins/binning/binning.vue index 7b2a6143..ffef5b11 100644 --- a/lcviz/plugins/binning/binning.vue +++ b/lcviz/plugins/binning/binning.vue @@ -54,6 +54,15 @@ + + + + Date: Tue, 25 Jul 2023 14:24:36 -0400 Subject: [PATCH 05/24] fix loading phase-binned data into app --- lcviz/plugins/binning/binning.py | 13 ++++++++++--- lcviz/plugins/ephemeris/ephemeris.py | 9 ++++++++- lcviz/template_mixin.py | 1 - lcviz/utils.py | 9 +++++---- 4 files changed, 23 insertions(+), 9 deletions(-) diff --git a/lcviz/plugins/binning/binning.py b/lcviz/plugins/binning/binning.py index 008f1486..022c23c6 100644 --- a/lcviz/plugins/binning/binning.py +++ b/lcviz/plugins/binning/binning.py @@ -42,7 +42,7 @@ class Binning(PluginTemplateMixin, DatasetSelectMixin, EphemerisSelectMixin, Add show_live_preview = Bool(True).tag(sync=True) n_bins = IntHandleEmpty(100).tag(sync=True) - map_to_times = Bool(True).tag(sync=True) + map_to_times = Bool(False).tag(sync=True) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -186,6 +186,7 @@ def bin(self, add_data=True): lc = input_lc.bin(time_bin_size=(input_lc.time[-1]-input_lc.time[0]).value/self.n_bins) if self.ephemeris_selected != 'No ephemeris': + # lc.time.value are actually phases, so convert to times starting at time t0 times = self.ephemeris_plugin.phases_to_times(lc.time.value, self.ephemeris_selected) if self.map_to_times: @@ -193,19 +194,24 @@ def bin(self, add_data=True): binned_lc = lc.copy() t0 = self.ephemeris_dict.get('t0', 0.0) period = self.ephemeris_dict.get('period', 1.0) - lc.time = times + lc.time = Time(times, + format=input_lc.time.format, + scale=input_lc.time.scale) # extend forward and backwards in cycles for the full range of input_lc min_time, max_time = input_lc.time_original.min().value, input_lc.time_original.max().value for start_time in np.arange(min_time, max_time, period): this_times = start_time + (times - t0) if start_time == min_time: - lc.time = this_times + lc.time = Time(this_times, + format=input_lc.time.format, + scale=input_lc.time.scale) else: binned_lc.time = this_times lc = lc.append(binned_lc) else: + # then just set the time_original column, leaving the time column as phases time_col = Time(times, format=input_lc.time_original.format, scale=input_lc.time_original.scale) @@ -213,6 +219,7 @@ def bin(self, add_data=True): if add_data: # add data to the collection/viewer + # NOTE: lc will have _LCVIZ_EPHEMERIS set if phase-folded self.add_results.add_results_from_plugin(lc) return lc diff --git a/lcviz/plugins/ephemeris/ephemeris.py b/lcviz/plugins/ephemeris/ephemeris.py index dd2ce39c..5160b0f9 100644 --- a/lcviz/plugins/ephemeris/ephemeris.py +++ b/lcviz/plugins/ephemeris/ephemeris.py @@ -176,6 +176,8 @@ def _times_to_phases_callable(self, component): wrap_at = ephem.get('wrap_at', _default_wrap_at) def _callable(times): + if not len(times): + return [] if dpdt != 0: return np.mod(1./dpdt * np.log(1 + dpdt/period*(times-t0)) + (1-wrap_at), 1.0) - (1-wrap_at) # noqa else: @@ -220,6 +222,10 @@ def _update_all_phase_arrays(self, *args, component=None): new_links = [] for i, data in enumerate(dc): + data_is_folded = '_LCVIZ_EPHEMERIS' in data.meta.keys() + if data_is_folded: + continue + times = data.get_component('World 0').data phases = _times_to_phases(times) if component not in self.phase_cids: @@ -511,7 +517,8 @@ def get_data(self, dataset, component=None): phlc.remove_column("time") # TODO: phased lc shouldn't have the same time format/scale, but this is needed # in order for binning to work (until there's a fix to lightkurve) - phlc.add_column(Time(phases, format=lc.time.format, scale=lc.time.scale), name="time", index=0) + phlc.add_column(Time(phases, format=lc.time.format, scale=lc.time.scale), + name="time", index=0) phlc.add_column(lc.time.copy(), name="time_original", index=len(lc._required_columns)) # Add extra column and meta data specific to FoldedLightCurve diff --git a/lcviz/template_mixin.py b/lcviz/template_mixin.py index 47bddb7c..ffc56474 100644 --- a/lcviz/template_mixin.py +++ b/lcviz/template_mixin.py @@ -2,7 +2,6 @@ from traitlets import List, Unicode from ipyvuetify import VuetifyTemplate from glue.core import HubListener -from glue.core.data import Data import jdaviz from jdaviz.core.events import SnackbarMessage diff --git a/lcviz/utils.py b/lcviz/utils.py index 3f87bba9..acdc76d7 100644 --- a/lcviz/utils.py +++ b/lcviz/utils.py @@ -69,10 +69,8 @@ class LightCurveHandler: def to_data(self, obj, reference_time=None): is_folded = isinstance(obj, FoldedLightCurve) - time = obj.time_original if is_folded else obj.time - time_coord = TimeCoordinates( - time, reference_time=reference_time - ) + time = obj.time_original if is_folded and hasattr(obj, 'time_original') else obj.time + time_coord = TimeCoordinates(time) data = Data(coords=time_coord) if hasattr(obj, 'label'): @@ -154,6 +152,9 @@ def to_object(self, data_or_subset): component_ids.remove(skip_comp) for component_id in component_ids: + if component_id.label in names: + # avoid duplicate column + continue component = data.get_component(component_id) values = component.data[glue_mask] From d6c7af43f9c7e0ade497fe007ddd147ff5e2f196 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Tue, 25 Jul 2023 14:30:50 -0400 Subject: [PATCH 06/24] remove "map_to_times" implementation * can always reconsider re-implementing down the road --- lcviz/plugins/binning/binning.py | 56 ++++++------------------------- lcviz/plugins/binning/binning.vue | 10 ------ 2 files changed, 11 insertions(+), 55 deletions(-) diff --git a/lcviz/plugins/binning/binning.py b/lcviz/plugins/binning/binning.py index 022c23c6..802a5a2a 100644 --- a/lcviz/plugins/binning/binning.py +++ b/lcviz/plugins/binning/binning.py @@ -1,5 +1,4 @@ from astropy.time import Time -import numpy as np from traitlets import Bool, observe from jdaviz.core.custom_traitlets import IntHandleEmpty @@ -33,7 +32,6 @@ class Binning(PluginTemplateMixin, DatasetSelectMixin, EphemerisSelectMixin, Add * :meth:`input_lc` Data used as input to binning, based on ``dataset`` and ``ephemeris``. * ``n_bins`` - * ``map_to_times`` * ``add_results`` (:class:`~jdaviz.core.template_mixin.AddResults`) * :meth:`bin` """ @@ -42,7 +40,6 @@ class Binning(PluginTemplateMixin, DatasetSelectMixin, EphemerisSelectMixin, Add show_live_preview = Bool(True).tag(sync=True) n_bins = IntHandleEmpty(100).tag(sync=True) - map_to_times = Bool(False).tag(sync=True) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -62,8 +59,7 @@ def not_from_binning_plugin(data): @property def user_api(self): expose = ['dataset', 'ephemeris', 'input_lc', - 'n_bins', 'map_to_times', - 'add_results', 'bin'] + 'n_bins', 'add_results', 'bin'] return PluginUserApi(self, expose=expose) @property @@ -127,7 +123,7 @@ def viewer_filter(viewer): @observe('show_live_preview', 'plugin_opened', 'dataset_selected', 'ephemeris_selected', - 'n_bins', 'map_to_times') + 'n_bins') def _live_update(self, event={}): if not self.show_live_preview or not self.plugin_opened: self._clear_marks() @@ -135,7 +131,7 @@ def _live_update(self, event={}): lc = self.bin(add_data=False) - if self.ephemeris_selected == 'No ephemeris' or self.map_to_times: + if self.ephemeris_selected == 'No ephemeris': ref_time = lc.meta.get('reference_time', 0) ref_time = getattr(ref_time, 'value', ref_time) times = lc.time - ref_time @@ -150,17 +146,9 @@ def _live_update(self, event={}): # TODO: fix this to be general and not rely on ugly id do_phase = viewer_id != 'lcviz-0' else: - if self.map_to_times: - # then the flux-vs-time viewer gets the data from binning, and all - # flux-vs-phase viewers are converted based on their current ephemeris - - visible = True - # TODO: fix this to be general and not rely on ugly id - do_phase = viewer_id != 'lcviz-0' - else: - # TODO: try to fix flashing as traitlets update - visible = viewer_id.split(':')[-1] == self.ephemeris_selected - do_phase = False + # TODO: try to fix flashing as traitlets update + visible = viewer_id.split(':')[-1] == self.ephemeris_selected + do_phase = False if visible: if do_phase: @@ -189,33 +177,11 @@ def bin(self, add_data=True): # lc.time.value are actually phases, so convert to times starting at time t0 times = self.ephemeris_plugin.phases_to_times(lc.time.value, self.ephemeris_selected) - if self.map_to_times: - # the original starts at time t0 - binned_lc = lc.copy() - t0 = self.ephemeris_dict.get('t0', 0.0) - period = self.ephemeris_dict.get('period', 1.0) - lc.time = Time(times, - format=input_lc.time.format, - scale=input_lc.time.scale) - - # extend forward and backwards in cycles for the full range of input_lc - min_time, max_time = input_lc.time_original.min().value, input_lc.time_original.max().value - for start_time in np.arange(min_time, max_time, period): - this_times = start_time + (times - t0) - if start_time == min_time: - lc.time = Time(this_times, - format=input_lc.time.format, - scale=input_lc.time.scale) - else: - binned_lc.time = this_times - lc = lc.append(binned_lc) - - else: - # then just set the time_original column, leaving the time column as phases - time_col = Time(times, - format=input_lc.time_original.format, - scale=input_lc.time_original.scale) - lc.add_column(time_col, name="time_original", index=len(lc._required_columns)) + # set the time_original column, leaving the time column as phases + time_col = Time(times, + format=input_lc.time_original.format, + scale=input_lc.time_original.scale) + lc.add_column(time_col, name="time_original", index=len(lc._required_columns)) if add_data: # add data to the collection/viewer diff --git a/lcviz/plugins/binning/binning.vue b/lcviz/plugins/binning/binning.vue index ffef5b11..442abf83 100644 --- a/lcviz/plugins/binning/binning.vue +++ b/lcviz/plugins/binning/binning.vue @@ -54,16 +54,6 @@ - - - - - Date: Tue, 25 Jul 2023 14:41:44 -0400 Subject: [PATCH 07/24] barebones plugin docs entries --- docs/plugins.rst | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/plugins.rst b/docs/plugins.rst index e7bd5d65..21f8d72d 100644 --- a/docs/plugins.rst +++ b/docs/plugins.rst @@ -194,11 +194,17 @@ This plugin exposes the periodogram (in period or frequency space) for an input .. _ephemeris: Ephemeris -============ +========== The ephemeris plugin allows for setting, finding, and refining the ephemeris or ephemerides used for phase-folding. +.. _binning: + +Binning +======= + +This plugin supports binning a light curve in time or phase-space. .. admonition:: User API Example :class: dropdown From fef4827152c8e75be2f34493a6eac8737b1c88a9 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Tue, 25 Jul 2023 15:30:39 -0400 Subject: [PATCH 08/24] fix failing test --- lcviz/tests/test_plugin_binning.py | 2 ++ lcviz/utils.py | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/lcviz/tests/test_plugin_binning.py b/lcviz/tests/test_plugin_binning.py index a38df26b..6cde501f 100644 --- a/lcviz/tests/test_plugin_binning.py +++ b/lcviz/tests/test_plugin_binning.py @@ -19,6 +19,8 @@ def test_plugin_binning(helper, light_curve_like_kepler_quarter): assert len(_get_marks_from_viewer(tv)) == 1 assert len(_get_marks_from_viewer(pv)) == 0 + b.bin(add_data=True) + b.ephemeris = 'default' assert len(_get_marks_from_viewer(tv)) == 0 assert len(_get_marks_from_viewer(pv)) == 1 diff --git a/lcviz/utils.py b/lcviz/utils.py index acdc76d7..9b6fdd00 100644 --- a/lcviz/utils.py +++ b/lcviz/utils.py @@ -100,7 +100,10 @@ def to_data(self, obj, reference_time=None): data[cid] = component_data if hasattr(component_data, 'unit'): - data.get_component(cid).units = str(component_data.unit) + try: + data.get_component(cid).units = str(component_data.unit) + except KeyError: + continue data.meta.update({'uncertainty_type': 'std'}) From 28467d622fc33b430e7c68e0b2d183b4a1c6c51f Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Tue, 25 Jul 2023 16:13:24 -0400 Subject: [PATCH 09/24] data component filter for folded data * does not make sense to use phase-binned data (or any other pre-phase-folded data) as input to flattening or frequency analysis --- lcviz/plugins/flatten/flatten.py | 4 ++++ lcviz/plugins/frequency_analysis/frequency_analysis.py | 5 +++++ lcviz/utils.py | 7 ++++++- 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lcviz/plugins/flatten/flatten.py b/lcviz/plugins/flatten/flatten.py index d907e542..b96de0dc 100644 --- a/lcviz/plugins/flatten/flatten.py +++ b/lcviz/plugins/flatten/flatten.py @@ -10,6 +10,7 @@ from jdaviz.core.user_api import PluginUserApi from lcviz.marks import LivePreviewTrend, LivePreviewFlattened +from lcviz.utils import data_not_folded from lcviz.viewers import TimeScatterView, PhaseScatterView from lcviz.parsers import _data_with_reftime @@ -54,6 +55,9 @@ class Flatten(PluginTemplateMixin, DatasetSelectMixin, AddResultsMixin): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + # do not support flattening data in phase-space + self.dataset.add_filter(data_not_folded) + # marks do not exist for the new viewer, so force another update to compute and draw # those marks self.hub.subscribe(self, ViewerAddedMessage, handler=lambda _: self._live_update()) diff --git a/lcviz/plugins/frequency_analysis/frequency_analysis.py b/lcviz/plugins/frequency_analysis/frequency_analysis.py index 2b5d5e17..3a241266 100644 --- a/lcviz/plugins/frequency_analysis/frequency_analysis.py +++ b/lcviz/plugins/frequency_analysis/frequency_analysis.py @@ -9,6 +9,8 @@ DatasetSelectMixin, SelectPluginComponent, PlotMixin) from jdaviz.core.user_api import PluginUserApi +from lcviz.utils import data_not_folded + __all__ = ['FrequencyAnalysis'] @@ -54,6 +56,9 @@ def __init__(self, *args, **kwargs): self._ignore_auto_update = False + # do not support data only in phase-space + self.dataset.add_filter(data_not_folded) + self.method = SelectPluginComponent(self, items='method_items', selected='method_selected', diff --git a/lcviz/utils.py b/lcviz/utils.py index 9b6fdd00..09fee5fb 100644 --- a/lcviz/utils.py +++ b/lcviz/utils.py @@ -15,7 +15,7 @@ LightCurve, KeplerLightCurve, TessLightCurve, FoldedLightCurve ) -__all__ = ['TimeCoordinates', 'LightCurveHandler'] +__all__ = ['TimeCoordinates', 'LightCurveHandler', 'data_not_folded'] class TimeCoordinates(Coordinates): @@ -205,3 +205,8 @@ class KeplerLightCurveHandler(LightCurveHandler): class TessLightCurveHandler(LightCurveHandler): # Works the same as LightCurve pass + + +# plugin component filters +def data_not_folded(data): + return data.meta.get('_LCVIZ_EPHEMERIS', None) is None From 4d29db2df0494a51f65480c6253ead4bb0d968b3 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Thu, 27 Jul 2023 15:51:10 -0400 Subject: [PATCH 10/24] fix failing test on CI --- lcviz/plugins/binning/binning.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lcviz/plugins/binning/binning.py b/lcviz/plugins/binning/binning.py index 802a5a2a..7a4d3db0 100644 --- a/lcviz/plugins/binning/binning.py +++ b/lcviz/plugins/binning/binning.py @@ -130,15 +130,15 @@ def _live_update(self, event={}): return lc = self.bin(add_data=False) + # TODO: remove the need for this (inconsistent quantity vs value setting in lc object) + lc_time = getattr(lc.time, 'value', lc.time) if self.ephemeris_selected == 'No ephemeris': ref_time = lc.meta.get('reference_time', 0) ref_time = getattr(ref_time, 'value', ref_time) - times = lc.time - ref_time + times = lc_time - ref_time else: - times = lc.time - # TODO: remove the need for this (inconsistent quantity vs value setting in lc object) - times = getattr(times, 'value', times) + times = lc_time for viewer_id, mark in self.marks.items(): if self.ephemeris_selected == 'No ephemeris': From 5afc057952d4a1a7293750024c511823eff75d21 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Thu, 27 Jul 2023 16:12:55 -0400 Subject: [PATCH 11/24] minor fixes for data-entries when changing ephemeris name --- lcviz/plugins/coords_info/coords_info.py | 11 +++++++++++ lcviz/plugins/ephemeris/ephemeris.py | 10 ++++++++++ 2 files changed, 21 insertions(+) diff --git a/lcviz/plugins/coords_info/coords_info.py b/lcviz/plugins/coords_info/coords_info.py index b18c268e..9ab33b4b 100644 --- a/lcviz/plugins/coords_info/coords_info.py +++ b/lcviz/plugins/coords_info/coords_info.py @@ -5,6 +5,7 @@ from jdaviz.core.registries import tool_registry from lcviz.viewers import TimeScatterView, PhaseScatterView +from lcviz.events import ViewerRenamedMessage __all__ = ['CoordsInfo'] @@ -15,6 +16,16 @@ class CoordsInfo(CoordsInfo): _supported_viewer_classes = (TimeScatterView, PhaseScatterView) _viewer_classes_with_marker = (TimeScatterView, PhaseScatterView) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # TODO: move to jdaviz if/once viewer renaming supported + self.hub.subscribe(self, ViewerRenamedMessage, + handler=self._viewer_renamed) + + def _viewer_renamed(self, msg): + self._marks[msg.new_viewer_ref] = self._marks.pop(msg.old_viewer_ref) + def update_display(self, viewer, x, y): self._dict = {} diff --git a/lcviz/plugins/ephemeris/ephemeris.py b/lcviz/plugins/ephemeris/ephemeris.py index 5160b0f9..cb8429a8 100644 --- a/lcviz/plugins/ephemeris/ephemeris.py +++ b/lcviz/plugins/ephemeris/ephemeris.py @@ -322,6 +322,16 @@ def _on_component_rename(self, old_lbl, new_lbl): self._ephemerides[new_lbl] = self._ephemerides.pop(old_lbl, {}) if self._phase_viewer_id(old_lbl) in self.app.get_viewer_ids(): self.app._rename_viewer(self._phase_viewer_id(old_lbl), self._phase_viewer_id(new_lbl)) + + # update metadata entries so that they can be used for filtering applicable entries in + # data menus + for dc_item in self.app.data_collection: + if dc_item.meta.get('_LCVIZ_EPHEMERIS', {}).get('ephemeris', None) == old_lbl: + dc_item.meta['_LCVIZ_EPHEMERIS']['ephemeris'] = new_lbl + for data_item in self.app.state.data_items: + if data_item.get('meta', {}).get('_LCVIZ_EPHEMERIS', {}).get('ephemeris', None) == old_lbl: # noqa + data_item['meta']['_LCVIZ_EPHEMERIS']['ephemeris'] = new_lbl + self._check_if_phase_viewer_exists() self.hub.broadcast(EphemerisComponentChangedMessage(old_lbl=old_lbl, new_lbl=new_lbl, sender=self)) From 64e3e4319c599069375dd98bc1206bf929b56755 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Mon, 31 Jul 2023 14:17:04 -0400 Subject: [PATCH 12/24] larger default marker size for data from binning plugin --- lcviz/viewers.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lcviz/viewers.py b/lcviz/viewers.py index 2e878067..89aab87b 100644 --- a/lcviz/viewers.py +++ b/lcviz/viewers.py @@ -91,6 +91,11 @@ def data(self, cls=None): return data + def _apply_layer_defaults(self, layer_state): + if getattr(layer_state.layer, 'meta', {}).get('Plugin', None) == 'Binning': + # increased size of binned results, by default + layer_state.size = 5 + def set_plot_axes(self): # set which components should be plotted dc = self.jdaviz_app.data_collection From 11394d018209a7f024f8c5abad252fb6f9f43a44 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Mon, 31 Jul 2023 14:47:15 -0400 Subject: [PATCH 13/24] update live-preview behavior for jdaviz 3.6 --- lcviz/plugins/binning/binning.py | 9 +++++---- lcviz/plugins/binning/binning.vue | 3 +++ lcviz/tests/test_plugin_binning.py | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/lcviz/plugins/binning/binning.py b/lcviz/plugins/binning/binning.py index 7a4d3db0..02bb7c6d 100644 --- a/lcviz/plugins/binning/binning.py +++ b/lcviz/plugins/binning/binning.py @@ -36,6 +36,7 @@ class Binning(PluginTemplateMixin, DatasetSelectMixin, EphemerisSelectMixin, Add * :meth:`bin` """ template_file = __file__, "binning.vue" + uses_active_status = Bool(True).tag(sync=True) show_live_preview = Bool(True).tag(sync=True) @@ -85,7 +86,7 @@ def marks(self): marks[id] = mark break else: - mark = LivePreviewBinning(viewer, visible=self.plugin_opened) + mark = LivePreviewBinning(viewer, visible=self.is_active) viewer.figure.marks = viewer.figure.marks + [mark] marks[id] = mark return marks @@ -121,11 +122,11 @@ def viewer_filter(viewer): self.add_results.viewer.filters = [viewer_filter] - @observe('show_live_preview', 'plugin_opened', + @observe('show_live_preview', 'is_active', 'dataset_selected', 'ephemeris_selected', 'n_bins') def _live_update(self, event={}): - if not self.show_live_preview or not self.plugin_opened: + if not self.show_live_preview or not self.is_active: self._clear_marks() return @@ -161,7 +162,7 @@ def _live_update(self, event={}): mark.visible = visible def _on_ephemeris_update(self, msg): - if not self.show_live_preview or not self.plugin_opened: + if not self.show_live_preview or not self.is_active: return if msg.ephemeris_label != self.ephemeris_selected: diff --git a/lcviz/plugins/binning/binning.vue b/lcviz/plugins/binning/binning.vue index 442abf83..cf5c89b4 100644 --- a/lcviz/plugins/binning/binning.vue +++ b/lcviz/plugins/binning/binning.vue @@ -2,6 +2,9 @@ diff --git a/lcviz/tests/test_plugin_binning.py b/lcviz/tests/test_plugin_binning.py index 6cde501f..998dee60 100644 --- a/lcviz/tests/test_plugin_binning.py +++ b/lcviz/tests/test_plugin_binning.py @@ -10,7 +10,7 @@ def test_plugin_binning(helper, light_curve_like_kepler_quarter): tv = helper.app.get_viewer(helper._default_time_viewer_reference_name) b = helper.plugins['Binning'] - b.open_in_tray() + b._obj.plugin_opened = True ephem = helper.plugins['Ephemeris'] ephem.period = 1.2345 pv = ephem.create_phase_viewer() From 7edc2922da1e54c6601d5ceaa90321247777f292 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Mon, 21 Aug 2023 16:32:20 -0400 Subject: [PATCH 14/24] fix rebase for reference_time --- lcviz/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lcviz/utils.py b/lcviz/utils.py index 09fee5fb..94615839 100644 --- a/lcviz/utils.py +++ b/lcviz/utils.py @@ -70,7 +70,7 @@ class LightCurveHandler: def to_data(self, obj, reference_time=None): is_folded = isinstance(obj, FoldedLightCurve) time = obj.time_original if is_folded and hasattr(obj, 'time_original') else obj.time - time_coord = TimeCoordinates(time) + time_coord = TimeCoordinates(time, reference_time=reference_time) data = Data(coords=time_coord) if hasattr(obj, 'label'): From 356f4f975ac5052c85cb738f99c2d819ff75e556 Mon Sep 17 00:00:00 2001 From: "Brett M. Morris" Date: Wed, 6 Sep 2023 08:59:00 -0400 Subject: [PATCH 15/24] Prototype fix for CID uniqueness (#1) * fix for CID uniqueness * fix missing phase_comp_lbl reference * fix for binning test --------- Co-authored-by: Kyle Conroy --- lcviz/helper.py | 17 ++++++ lcviz/plugins/binning/binning.py | 24 +++++++- lcviz/plugins/ephemeris/ephemeris.py | 91 ++++++++++++++-------------- lcviz/viewers.py | 2 +- 4 files changed, 85 insertions(+), 49 deletions(-) diff --git a/lcviz/helper.py b/lcviz/helper.py index 2f123ed7..92a629bf 100644 --- a/lcviz/helper.py +++ b/lcviz/helper.py @@ -4,6 +4,7 @@ from lightkurve import LightCurve +from glue.core.component_id import ComponentID from glue.core.link_helpers import LinkSame from jdaviz.core.helpers import ConfigHelper from lcviz.events import ViewerRenamedMessage @@ -123,6 +124,8 @@ class LCviz(ConfigHelper): 'plot': 'lcviz-time-viewer', 'reference': 'flux-vs-time'}]}]}]} + _component_ids = {} + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._default_time_viewer_reference_name = _default_time_viewer_reference_name @@ -191,3 +194,17 @@ def get_data(self, data_label=None, cls=LightCurve, subset=None): Data is returned as type cls with subsets applied. """ return super()._get_data(data_label=data_label, mask_subset=subset, cls=cls) + + def _phase_comp_lbl(self, component): + return f'phase:{component}' + + def _set_data_component(self, data, component_label, values): + if component_label not in self._component_ids: + self._component_ids[component_label] = ComponentID(component_label) + + if self._component_ids[component_label] in data.components: + data.update_components({self._component_ids[component_label]: values}) + else: + data.add_component(values, self._component_ids[component_label]) + + data.add_component(values, self._component_ids[component_label]) diff --git a/lcviz/plugins/binning/binning.py b/lcviz/plugins/binning/binning.py index 02bb7c6d..482e5d8c 100644 --- a/lcviz/plugins/binning/binning.py +++ b/lcviz/plugins/binning/binning.py @@ -1,5 +1,6 @@ from astropy.time import Time from traitlets import Bool, observe +from glue.config import data_translator from jdaviz.core.custom_traitlets import IntHandleEmpty from jdaviz.core.events import (ViewerAddedMessage, ViewerRemovedMessage) @@ -184,10 +185,31 @@ def bin(self, add_data=True): scale=input_lc.time_original.scale) lc.add_column(time_col, name="time_original", index=len(lc._required_columns)) + lc.meta.update({'_LCVIZ_BINNED': True}) + + # convert to glue Data manually, so we may edit the `phase` component: + handler, _ = data_translator.get_handler_for(lc) + data = handler.to_data(lc) + phase_comp_lbl = self.app._jdaviz_helper._phase_comp_lbl(self.ephemeris_selected) + + # here we use the `value` attribute of `lc.time`, which has units of *phase*: + self.app._jdaviz_helper._set_data_component(data, phase_comp_lbl, lc.time.value) + + else: + data = None + if add_data: # add data to the collection/viewer # NOTE: lc will have _LCVIZ_EPHEMERIS set if phase-folded - self.add_results.add_results_from_plugin(lc) + self._set_results_viewer() + self.add_results.add_results_from_plugin(data or lc) + + if self.ephemeris_selected != 'No ephemeris': + # prevent phase axis from becoming a time axis: + viewer_id = self.ephemeris_plugin._obj.phase_viewer_id + pv = self.app.get_viewer(viewer_id) + phase_comp_lbl = self.app._jdaviz_helper._phase_comp_lbl(self.ephemeris_selected) + pv.state.x_att = self.app._jdaviz_helper._component_ids[phase_comp_lbl] return lc diff --git a/lcviz/plugins/ephemeris/ephemeris.py b/lcviz/plugins/ephemeris/ephemeris.py index cb8429a8..ebe6225e 100644 --- a/lcviz/plugins/ephemeris/ephemeris.py +++ b/lcviz/plugins/ephemeris/ephemeris.py @@ -61,7 +61,6 @@ class Ephemeris(PluginTemplateMixin, DatasetSelectMixin): template_file = __file__, "ephemeris.vue" # EPHEMERIS - phase_cids = {} component_mode = Unicode().tag(sync=True) component_edit_value = Unicode().tag(sync=True) component_items = List().tag(sync=True) @@ -128,7 +127,11 @@ def user_api(self): return PluginUserApi(self, expose=expose) def _phase_comp_lbl(self, component): - return f'phase:{component}' + if self.app._jdaviz_helper is None: + # duplicate logic from helper in case this is ever called before the helper + # is fully intialized + return f'phase:{component}' + return self.app._jdaviz_helper._phase_comp_lbl(component) @property def phase_comp_lbl(self): @@ -185,19 +188,19 @@ def _callable(times): return _callable - def times_to_phases(self, times, component=None): - if component is None: - component = self.component.selected + def times_to_phases(self, times, ephem_component=None): + if ephem_component is None: + ephem_component = self.component.selected - return self._times_to_phases_callable(component)(times) + return self._times_to_phases_callable(ephem_component)(times) - def phases_to_times(self, phases, component=None): - if component is None: - component = self.component.selected + def phases_to_times(self, phases, ephem_component=None): + if ephem_component is None: + ephem_component = self.component.selected # this is not used internally, so we don't need the traitlet # and callable optimizations - ephem = self.ephemerides.get(component, {}) + ephem = self.ephemerides.get(ephem_component, {}) t0 = ephem.get('t0', _default_t0) period = ephem.get('period', _default_period) dpdt = ephem.get('dpdt', _default_dpdt) @@ -207,18 +210,22 @@ def phases_to_times(self, phases, component=None): else: return t0 + (phases)*period - def _update_all_phase_arrays(self, *args, component=None): - if component is None: - for component in self.component.choices: - self._update_all_phase_arrays(component=component) + def _update_all_phase_arrays(self, *args, ephem_component=None): + # `ephem_component` is the name given to the + # *ephemeris* component in the orbiting system, e.g. "default", + # rather than the glue Data Component ID: + + if ephem_component is None: + for ephem_component in self.component.choices: + self._update_all_phase_arrays(ephem_component=ephem_component) return dc = self.app.data_collection - phase_comp_lbl = self._phase_comp_lbl(component) + phase_comp_lbl = self._phase_comp_lbl(ephem_component) # we'll create the callable function for this component once so it can be re-used - _times_to_phases = self._times_to_phases_callable(component) + _times_to_phases = self._times_to_phases_callable(ephem_component) new_links = [] for i, data in enumerate(dc): @@ -228,21 +235,11 @@ def _update_all_phase_arrays(self, *args, component=None): times = data.get_component('World 0').data phases = _times_to_phases(times) - if component not in self.phase_cids: - self.phase_cids[component] = ComponentID(phase_comp_lbl) - - if self.phase_cids[component] in data.components: - data.update_components({self.phase_cids[component]: phases}) - else: - data.add_component(phases, self.phase_cids[component]) - # this loop catches phase components generated automatically by - # when add_results is triggered in other plugins: - for comp in data.components: - if phase_comp_lbl == comp.label: - data.remove_component(phase_comp_lbl) + self.app._jdaviz_helper._set_data_component( + data, phase_comp_lbl, phases + ) - data.add_component(phases, self.phase_cids[component]) if i != 0: ref_data = dc[0] new_link = LinkSame( @@ -260,7 +257,7 @@ def _update_all_phase_arrays(self, *args, component=None): # update any plugin markers # TODO: eventually might need to loop over multiple matching viewers - phase_viewer_id = self._phase_viewer_id(component) + phase_viewer_id = self._phase_viewer_id(ephem_component) if phase_viewer_id in self.app.get_viewer_ids(): phase_viewer = self.app.get_viewer(phase_viewer_id) for mark in phase_viewer.custom_marks: @@ -298,7 +295,7 @@ def create_phase_viewer(self): pv = self.app.get_viewer(phase_viewer_id) if create_phase_viewer: pv.state.x_min, pv.state.x_max = (self.wrap_at-1, self.wrap_at) - pv.state.x_att = self.phase_cids[self.component_selected] + pv.state.x_att = self.app._jdaviz_helper._component_ids[self.phase_comp_lbl] return pv def vue_create_phase_viewer(self, *args): @@ -387,7 +384,7 @@ def _change_component(self, *args): # otherwise, this is a new component and there is no need. self._ephem_traitlet_changed() - def update_ephemeris(self, component=None, t0=None, period=None, dpdt=None, wrap_at=None): + def update_ephemeris(self, ephem_component=None, t0=None, period=None, dpdt=None, wrap_at=None): """ Update the ephemeris for a given component. @@ -408,22 +405,22 @@ def update_ephemeris(self, component=None, t0=None, period=None, dpdt=None, wrap ------- dictionary of ephemeris corresponding to ``component`` """ - if component is None: - component = self.component_selected + if ephem_component is None: + ephem_component = self.component_selected - if component not in self.component.choices: # pragma: no cover + if ephem_component not in self.component.choices: # pragma: no cover raise ValueError(f"component must be one of {self.component.choices}") - existing_ephem = self._ephemerides.get(component, {}) + existing_ephem = self._ephemerides.get(ephem_component, {}) for name, value in {'t0': t0, 'period': period, 'dpdt': dpdt, 'wrap_at': wrap_at}.items(): if value is not None: existing_ephem[name] = value - if component == self.component_selected: + if ephem_component == self.component_selected: setattr(self, name, value) - self._ephemerides[component] = existing_ephem - self._update_all_phase_arrays(component=component) - self.hub.broadcast(EphemerisChangedMessage(ephemeris_label=component, + self._ephemerides[ephem_component] = existing_ephem + self._update_all_phase_arrays(ephem_component=ephem_component) + self.hub.broadcast(EphemerisChangedMessage(ephemeris_label=ephem_component, sender=self)) return existing_ephem @@ -448,7 +445,7 @@ def round_to_1(x): self.update_ephemeris(**{event.get('name'): event.get('new')}) # will call _update_all_phase_arrays else: - self._update_all_phase_arrays(component=self.component_selected) + self._update_all_phase_arrays(ephem_component=self.component_selected) # update zoom-limits if wrap_at was changed if event.get('name') == 'wrap_at': @@ -505,18 +502,18 @@ def _update_periodogram(self, *args): def vue_adopt_period_at_max_power(self, *args): self.period = self.period_at_max_power - def get_data(self, dataset, component=None): + def get_data(self, dataset, ephem_component=None): # TODO: support subset_to_apply and then include a wrapper at the helper-level? # (would need to catch when cls does not result in a lightkurve object or write # behaviors for other cases as well) - if component is None: - component = self.component.selected + if ephem_component is None: + ephem_component = self.component.selected lc = self.app._jdaviz_helper.get_data(dataset) data = next((x for x in self.app.data_collection if x.label == dataset)) comps = {str(comp): comp for comp in data.components} - xcomp = f'phase:{component}' + xcomp = f'phase:{ephem_component}' phases = data.get_component(comps.get(xcomp)).data # the following code is adopted directly from lightkurve @@ -532,8 +529,8 @@ def get_data(self, dataset, component=None): phlc.add_column(lc.time.copy(), name="time_original", index=len(lc._required_columns)) # Add extra column and meta data specific to FoldedLightCurve - ephemeris = self.ephemerides.get(component) - phlc.meta["_LCVIZ_EPHEMERIS"] = {'ephemeris': component, **ephemeris} + ephemeris = self.ephemerides.get(ephem_component) + phlc.meta["_LCVIZ_EPHEMERIS"] = {'ephemeris': ephem_component, **ephemeris} phlc.meta["PERIOD"] = ephemeris.get('period') phlc.meta["EPOCH_TIME"] = ephemeris.get('t0') phlc.sort("time") diff --git a/lcviz/viewers.py b/lcviz/viewers.py index 89aab87b..e057939e 100644 --- a/lcviz/viewers.py +++ b/lcviz/viewers.py @@ -227,4 +227,4 @@ def times_to_phases(self, times): if ephem is None: raise ValueError("must have ephemeris plugin loaded to convert") - return ephem.times_to_phases(times, component=self.ephemeris_component) + return ephem.times_to_phases(times, ephem_component=self.ephemeris_component) From 9ecbdb7c1ceb0d4d2d35a843f7875ea03921b530 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Wed, 6 Sep 2023 09:20:50 -0400 Subject: [PATCH 16/24] update binning plugin docs to point to lightkurve --- docs/plugins.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/plugins.rst b/docs/plugins.rst index 21f8d72d..a42b5de8 100644 --- a/docs/plugins.rst +++ b/docs/plugins.rst @@ -199,6 +199,7 @@ Ephemeris The ephemeris plugin allows for setting, finding, and refining the ephemeris or ephemerides used for phase-folding. + .. _binning: Binning @@ -206,6 +207,7 @@ Binning This plugin supports binning a light curve in time or phase-space. + .. admonition:: User API Example :class: dropdown @@ -224,6 +226,13 @@ This plugin supports binning a light curve in time or phase-space. ephem.rename_component('default', 'my component name') +.. seealso:: + + This plugin uses the following ``lightkurve`` implementations: + + * :meth:`lightkurve.LightCurve.bin` + + .. _export-plot: Export Plot From 7a2015238e46e7b2100e28c244a1e83fcfc830d7 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Fri, 8 Sep 2023 09:51:13 -0400 Subject: [PATCH 17/24] fix reference time of binned data --- lcviz/plugins/binning/binning.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lcviz/plugins/binning/binning.py b/lcviz/plugins/binning/binning.py index 482e5d8c..84ddf47b 100644 --- a/lcviz/plugins/binning/binning.py +++ b/lcviz/plugins/binning/binning.py @@ -12,6 +12,7 @@ from lcviz.events import EphemerisChangedMessage from lcviz.helper import _default_time_viewer_reference_name from lcviz.marks import LivePreviewBinning +from lcviz.parsers import _data_with_reftime from lcviz.template_mixin import EphemerisSelectMixin @@ -196,7 +197,8 @@ def bin(self, add_data=True): self.app._jdaviz_helper._set_data_component(data, phase_comp_lbl, lc.time.value) else: - data = None + # need to send through parser-logic to assign the correct reference time + data = _data_with_reftime(self.app, lc) if add_data: # add data to the collection/viewer From 6db133206f24c3d6b39aae218e9eb16cc7c35771 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Fri, 8 Sep 2023 09:55:59 -0400 Subject: [PATCH 18/24] remove unused import --- lcviz/plugins/ephemeris/ephemeris.py | 1 - 1 file changed, 1 deletion(-) diff --git a/lcviz/plugins/ephemeris/ephemeris.py b/lcviz/plugins/ephemeris/ephemeris.py index ebe6225e..8929366c 100644 --- a/lcviz/plugins/ephemeris/ephemeris.py +++ b/lcviz/plugins/ephemeris/ephemeris.py @@ -2,7 +2,6 @@ from astropy.time import Time from traitlets import Bool, Float, List, Unicode, observe -from glue.core.component_id import ComponentID from glue.core.link_helpers import LinkSame from glue.core.message import DataCollectionAddMessage from jdaviz.core.custom_traitlets import FloatHandleEmpty From ae04c79f9a48d1d7ec478fb115a0c54b9b5857f9 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Fri, 8 Sep 2023 10:04:19 -0400 Subject: [PATCH 19/24] fix broken test --- lcviz/tests/test_plugin_ephemeris.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lcviz/tests/test_plugin_ephemeris.py b/lcviz/tests/test_plugin_ephemeris.py index 13c982ef..5af3a7c5 100644 --- a/lcviz/tests/test_plugin_ephemeris.py +++ b/lcviz/tests/test_plugin_ephemeris.py @@ -52,7 +52,7 @@ def test_plugin_ephemeris(helper, light_curve_like_kepler_quarter): assert ephem.period == 3.14 assert ephem.ephemeris['period'] == 3.14 # modify the ephemeris of the NON-selected ephemeris component - ephem.update_ephemeris(component='default', period=2) + ephem.update_ephemeris(ephem_component='default', period=2) assert ephem.period == 3.14 assert ephem.ephemerides['default']['period'] == 2 From 289908d968c884acdf1ef8fb266418bd9fbc32ff Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Fri, 8 Sep 2023 10:12:31 -0400 Subject: [PATCH 20/24] fix preview disappearing after phase-binning --- lcviz/plugins/binning/binning.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lcviz/plugins/binning/binning.py b/lcviz/plugins/binning/binning.py index 84ddf47b..5758d12b 100644 --- a/lcviz/plugins/binning/binning.py +++ b/lcviz/plugins/binning/binning.py @@ -212,6 +212,8 @@ def bin(self, add_data=True): pv = self.app.get_viewer(viewer_id) phase_comp_lbl = self.app._jdaviz_helper._phase_comp_lbl(self.ephemeris_selected) pv.state.x_att = self.app._jdaviz_helper._component_ids[phase_comp_lbl] + # by resetting x_att, the preview marks may have dissappeared + self._live_update() return lc From 525e5fc27e56954384b12f9539c46fa0c49c00ab Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Fri, 8 Sep 2023 11:04:23 -0400 Subject: [PATCH 21/24] enable binning plugin API docs --- docs/plugins.rst | 2 ++ docs/reference/api_plugins.rst | 3 +++ 2 files changed, 5 insertions(+) diff --git a/docs/plugins.rst b/docs/plugins.rst index a42b5de8..a9c2f316 100644 --- a/docs/plugins.rst +++ b/docs/plugins.rst @@ -211,6 +211,8 @@ This plugin supports binning a light curve in time or phase-space. .. admonition:: User API Example :class: dropdown + See the :class:`~lcviz.plugins.binning.binning.Binning` user API documentation for more details. + .. code-block:: python from lcviz import LCviz diff --git a/docs/reference/api_plugins.rst b/docs/reference/api_plugins.rst index 2eec68fc..a366d19a 100644 --- a/docs/reference/api_plugins.rst +++ b/docs/reference/api_plugins.rst @@ -3,6 +3,9 @@ Plugins API =========== +.. automodapi:: lcviz.plugins.binning.binning + :no-inheritance-diagram: + .. automodapi:: lcviz.plugins.ephemeris.ephemeris :no-inheritance-diagram: From 9759589c9bd4d19f7f1bd8d462ac29fde500b3ec Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Fri, 8 Sep 2023 11:11:41 -0400 Subject: [PATCH 22/24] add links from other plugin docs to their respective API --- docs/plugins.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/plugins.rst b/docs/plugins.rst index a9c2f316..2aeed53b 100644 --- a/docs/plugins.rst +++ b/docs/plugins.rst @@ -13,6 +13,8 @@ This plugin allows viewing of any metadata associated with the selected data. .. admonition:: User API Example :class: dropdown + See the :class:`~lcviz.plugins.metadata_viewer.metadata_viewer.MetadataViewer` user API documentation for more details. + .. code-block:: python from lcviz import LCviz @@ -44,6 +46,8 @@ This plugin gives access to per-viewer and per-layer plotting options. .. admonition:: User API Example :class: dropdown + See the :class:`~lcviz.plugins.plot_options.plot_options.PlotOptions` user API documentation for more details. + .. code-block:: python from lcviz import LCviz @@ -97,6 +101,8 @@ visible when the plugin is opened. .. admonition:: User API Example :class: dropdown + See the :class:`~lcviz.plugins.markers.markers.Markers` user API documentation for more details. + .. code-block:: python from lcviz import LCviz @@ -132,6 +138,8 @@ can be disabled through the plugin settings. .. admonition:: User API Example :class: dropdown + See the :class:`~lcviz.plugins.flatten.flatten.Flatten` user API documentation for more details. + .. code-block:: python from lcviz import LCviz @@ -166,6 +174,8 @@ This plugin exposes the periodogram (in period or frequency space) for an input .. admonition:: User API Example :class: dropdown + See the :class:`~lcviz.plugins.frequency_analysis.frequency_analysis.FrequencyAnalysis` user API documentation for more details. + .. code-block:: python from lcviz import LCviz @@ -246,6 +256,8 @@ This plugin allows exporting the plot in a given viewer to various image formats .. admonition:: User API Example :class: dropdown + See the :class:`~lcviz.plugins.export_plot.export_plot.ExportViewer` user API documentation for more details. + .. code-block:: python from lcviz import LCviz From 2c9bac7e4a0f77bf57812800b1c71958703c0097 Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Fri, 8 Sep 2023 11:22:18 -0400 Subject: [PATCH 23/24] add/fix user-API snippet for binning --- docs/plugins.rst | 28 ++++++++++++++++++++++++---- lcviz/tests/test_plugin_binning.py | 12 ++++++++++++ 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/docs/plugins.rst b/docs/plugins.rst index 2aeed53b..0558f69f 100644 --- a/docs/plugins.rst +++ b/docs/plugins.rst @@ -210,6 +210,26 @@ The ephemeris plugin allows for setting, finding, and refining the ephemeris or for phase-folding. +.. admonition:: User API Example + :class: dropdown + + See the :class:`~lcviz.plugins.ephemeris.ephemeris.Ephemeris` user API documentation for more details. + + .. code-block:: python + + from lcviz import LCviz + lc = search_lightcurve("HAT-P-11", mission="Kepler", + cadence="long", quarter=10).download().flatten() + lcviz = LCviz() + lcviz.load_data(lc) + lcviz.show() + + ephem = lcviz.plugins['Ephemeris'] + ephem.period = 4.88780258 + ephem.t0 = 2.43 + ephem.rename_component('default', 'my component name') + + .. _binning: Binning @@ -232,10 +252,10 @@ This plugin supports binning a light curve in time or phase-space. lcviz.load_data(lc) lcviz.show() - ephem = lcviz.plugins['Ephemeris'] - ephem.period = 4.88780258 - ephem.t0 = 2.43 - ephem.rename_component('default', 'my component name') + binning = lcviz.plugins['Binning'] + binning.n_bins = 150 + binned_lc = binning.bin(add_data=True) + print(binned_lc) .. seealso:: diff --git a/lcviz/tests/test_plugin_binning.py b/lcviz/tests/test_plugin_binning.py index 998dee60..2b0087fd 100644 --- a/lcviz/tests/test_plugin_binning.py +++ b/lcviz/tests/test_plugin_binning.py @@ -5,6 +5,18 @@ def _get_marks_from_viewer(viewer, cls=(LivePreviewBinning)): return [m for m in viewer.figure.marks if isinstance(m, cls) and m.visible] +def test_docs_snippets(helper, light_curve_like_kepler_quarter): + lcviz, lc = helper, light_curve_like_kepler_quarter + + lcviz.load_data(lc) + # lcviz.show() + + binning = lcviz.plugins['Binning'] + binning.n_bins = 150 + binned_lc = binning.bin(add_data=True) + print(binned_lc) + + def test_plugin_binning(helper, light_curve_like_kepler_quarter): helper.load_data(light_curve_like_kepler_quarter) tv = helper.app.get_viewer(helper._default_time_viewer_reference_name) From 3eac4bff77b973a9c1ac9af058765f254ecc0bdd Mon Sep 17 00:00:00 2001 From: Kyle Conroy Date: Tue, 12 Sep 2023 13:01:27 -0400 Subject: [PATCH 24/24] improve test coverage --- lcviz/events.py | 2 +- lcviz/template_mixin.py | 2 +- lcviz/tests/test_plugin_binning.py | 25 +++++++++++++++++-------- lcviz/tests/test_plugin_ephemeris.py | 4 ++-- lcviz/utils.py | 2 +- 5 files changed, 22 insertions(+), 13 deletions(-) diff --git a/lcviz/events.py b/lcviz/events.py index 708699d9..54f521ef 100644 --- a/lcviz/events.py +++ b/lcviz/events.py @@ -27,7 +27,7 @@ def __init__(self, old_lbl, new_lbl, *args, **kwargs): self.type = 'add' elif old_lbl is not None and new_lbl is None: self.type = 'remove' - else: + else: # pragma: no cover raise ValueError("must provide at least one of old_lbl or new_lbl") diff --git a/lcviz/template_mixin.py b/lcviz/template_mixin.py index ffc56474..88efe605 100644 --- a/lcviz/template_mixin.py +++ b/lcviz/template_mixin.py @@ -187,7 +187,7 @@ def selected_obj(self): return self.ephemeris_plugin.ephemerides.get(self.selected, None) def get_data_for_dataset(self, dataset, ycomp='flux'): - if not isinstance(dataset, DatasetSelect): + if not isinstance(dataset, DatasetSelect): # pragma: no cover raise ValueError("dataset must be DatasetSelect object") if self.selected in self._manual_options: return dataset.selected_obj diff --git a/lcviz/tests/test_plugin_binning.py b/lcviz/tests/test_plugin_binning.py index 2b0087fd..034d58d3 100644 --- a/lcviz/tests/test_plugin_binning.py +++ b/lcviz/tests/test_plugin_binning.py @@ -27,14 +27,23 @@ def test_plugin_binning(helper, light_curve_like_kepler_quarter): ephem.period = 1.2345 pv = ephem.create_phase_viewer() - assert b.ephemeris == 'No ephemeris' - assert len(_get_marks_from_viewer(tv)) == 1 - assert len(_get_marks_from_viewer(pv)) == 0 + with b.as_active(): + assert b.ephemeris == 'No ephemeris' + assert len(_get_marks_from_viewer(tv)) == 1 + assert len(_get_marks_from_viewer(pv)) == 0 + assert b._obj.ephemeris_dict == {} - b.bin(add_data=True) + # update ephemeris will force re-phasing + ephem.period = 1.111 - b.ephemeris = 'default' - assert len(_get_marks_from_viewer(tv)) == 0 - assert len(_get_marks_from_viewer(pv)) == 1 + b.bin(add_data=True) - b.bin(add_data=True) + b.ephemeris = 'default' + assert len(_get_marks_from_viewer(tv)) == 0 + assert len(_get_marks_from_viewer(pv)) == 1 + assert len(b._obj.ephemeris_dict.keys()) > 0 + + # update ephemeris will force re-binning + ephem.period = 1.222 + + b.bin(add_data=True) diff --git a/lcviz/tests/test_plugin_ephemeris.py b/lcviz/tests/test_plugin_ephemeris.py index 5af3a7c5..9953f2af 100644 --- a/lcviz/tests/test_plugin_ephemeris.py +++ b/lcviz/tests/test_plugin_ephemeris.py @@ -68,5 +68,5 @@ def test_plugin_ephemeris(helper, light_curve_like_kepler_quarter): ephem._obj.vue_adopt_period_at_max_power() assert ephem.period != 2 - # test coverage for non-zero dpdt - ephem.dpdt = 0.00001 + # test that non-zero dpdt does not crash + ephem.dpdt = 0.005 diff --git a/lcviz/utils.py b/lcviz/utils.py index 94615839..572423da 100644 --- a/lcviz/utils.py +++ b/lcviz/utils.py @@ -102,7 +102,7 @@ def to_data(self, obj, reference_time=None): if hasattr(component_data, 'unit'): try: data.get_component(cid).units = str(component_data.unit) - except KeyError: + except KeyError: # pragma: no cover continue data.meta.update({'uncertainty_type': 'std'})