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

TPF viewer #81

Merged
merged 9 commits into from
Jan 29, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
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
32 changes: 25 additions & 7 deletions lcviz/parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ def light_curve_parser(app, file_obj, data_label=None, show_in_viewer=True, **kw

# handle flux_origin default
flux_origin = light_curve.meta.get('FLUX_ORIGIN', None) # i.e. PDCSAP or SAP
if isinstance(light_curve, lightkurve.targetpixelfile.TargetPixelFile):
new_data_label += '[TPF]'
elif flux_origin is not None:
new_data_label += f'[{flux_origin}]'

if flux_origin == 'flux' or (flux_origin is None and 'flux' in getattr(light_curve, 'columns', [])): # noqa
# then make a copy of this column so it won't be lost when changing with the flux_column
# plugin
Expand All @@ -47,14 +52,27 @@ def light_curve_parser(app, file_obj, data_label=None, show_in_viewer=True, **kw
data = _data_with_reftime(app, light_curve)
app.add_data(data, new_data_label)

if show_in_viewer:
app.add_data_to_viewer(time_viewer_reference_name, new_data_label)
if isinstance(light_curve, lightkurve.targetpixelfile.TargetPixelFile):
# ensure an image/cube/TPF viewer exists
# TODO: move this to an event listener on add_data so that we can also remove when empty?
from jdaviz.core.events import NewViewerMessage
from lcviz.viewers import CubeView
viewer_reference_name = 'image'
Copy link
Contributor

Choose a reason for hiding this comment

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

Good name. 😊

if viewer_reference_name not in app._viewer_store.keys():
app._on_new_viewer(NewViewerMessage(CubeView, data=None, sender=app),
vid='image', name='image')
if show_in_viewer:
app.add_data_to_viewer(viewer_reference_name, new_data_label)

else:
if show_in_viewer:
app.add_data_to_viewer(time_viewer_reference_name, new_data_label)

# add to any known phase viewers
ephem_plugin = app._jdaviz_helper.plugins.get('Ephemeris', None)
if ephem_plugin is not None:
for viewer_id in ephem_plugin._obj.phase_viewer_ids:
app.add_data_to_viewer(viewer_id, new_data_label)
# add to any known phase viewers
ephem_plugin = app._jdaviz_helper.plugins.get('Ephemeris', None)
if ephem_plugin is not None:
for viewer_id in ephem_plugin._obj.phase_viewer_ids:
app.add_data_to_viewer(viewer_id, new_data_label)


def _data_with_reftime(app, light_curve):
Expand Down
22 changes: 14 additions & 8 deletions lcviz/plugins/coords_info/coords_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
from jdaviz.core.events import ViewerRenamedMessage
from jdaviz.core.registries import tool_registry

from lcviz.viewers import TimeScatterView, PhaseScatterView
from lcviz.viewers import TimeScatterView, PhaseScatterView, CubeView

__all__ = ['CoordsInfo']


@tool_registry('lcviz-coords-info')
class CoordsInfo(CoordsInfo):
_supported_viewer_classes = (TimeScatterView, PhaseScatterView)
_supported_viewer_classes = (TimeScatterView, PhaseScatterView, CubeView)
_viewer_classes_with_marker = (TimeScatterView, PhaseScatterView)

def __init__(self, *args, **kwargs):
Expand All @@ -25,12 +25,7 @@ def __init__(self, *args, **kwargs):
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 = {}

if not len(viewer.state.layers):
return

def _lc_viewer_update(self, viewer, x, y):
is_phase = isinstance(viewer, PhaseScatterView)
# TODO: update with display_unit when supported in lcviz
x_unit = '' if is_phase else str(viewer.time_unit)
Expand Down Expand Up @@ -138,3 +133,14 @@ def _cursor_fallback():

self.marks[viewer._reference_id].update_xy([closest_x], [closest_y]) # noqa
self.marks[viewer._reference_id].visible = True

def update_display(self, viewer, x, y):
self._dict = {}

if not len(viewer.state.layers):
return

if isinstance(viewer, (TimeScatterView, PhaseScatterView)):
self._lc_viewer_update(viewer, x, y)
elif isinstance(viewer, CubeView):
self._image_viewer_update(viewer, x, y)
135 changes: 97 additions & 38 deletions lcviz/viewers.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,55 @@

from jdaviz.core.events import NewViewerMessage
from jdaviz.core.registries import viewer_registry
from jdaviz.configs.cubeviz.plugins.viewers import CubevizImageView
from jdaviz.configs.default.plugins.viewers import JdavizViewerMixin
from jdaviz.configs.specviz.plugins.viewers import SpecvizProfileView

from lcviz.state import ScatterViewerState

from lightkurve import LightCurve
from lightkurve import LightCurve, KeplerTargetPixelFile

__all__ = ['TimeScatterView', 'PhaseScatterView', 'CubeView']

__all__ = ['TimeScatterView', 'PhaseScatterView']

class CloneViewerMixin:
def _get_clone_viewer_reference(self):
base_name = self.reference.split("[")[0]
name = base_name
ind = 0
while name in self.jdaviz_helper.viewers.keys():
ind += 1
name = f"{base_name}[{ind}]"
return name

def clone_viewer(self):
name = self._get_clone_viewer_reference()

self.jdaviz_app._on_new_viewer(NewViewerMessage(self.__class__,
data=None,
sender=self.jdaviz_app),
vid=name, name=name)

this_viewer_item = self.jdaviz_app._get_viewer_item(self.reference)
this_state = self.state.as_dict()
for data in self.jdaviz_app.data_collection:
data_id = self.jdaviz_app._data_id_from_label(data.label)
visible = this_viewer_item['selected_data_items'].get(data_id, 'hidden')
self.jdaviz_app.set_data_visibility(name, data.label, visible == 'visible')
# TODO: don't revert color when adding same data to a new viewer
# (same happens when creating a phase-viewer from ephemeris plugin)

new_viewer = self.jdaviz_helper.viewers[name]._obj
for k, v in this_state.items():
if k in ('layers',):
continue
setattr(new_viewer.state, k, v)

return new_viewer.user_api


@viewer_registry("lcviz-time-viewer", label="flux-vs-time")
class TimeScatterView(JdavizViewerMixin, BqplotScatterView):
class TimeScatterView(JdavizViewerMixin, CloneViewerMixin, BqplotScatterView):
# categories: zoom resets, zoom, pan, subset, select tools, shortcuts
tools_nested = [
['jdaviz:homezoom', 'jdaviz:prevzoom'],
Expand All @@ -43,7 +79,6 @@ def __init__(self, *args, **kwargs):

self.display_mask = False
self.time_unit = kwargs.get('time_unit', u.d)
self._subscribe_to_layers_update()
self.initialize_toolbar()
self._subscribe_to_layers_update()
# hack to inherit a small subset of methods from SpecvizProfileView
Expand Down Expand Up @@ -210,40 +245,6 @@ def apply_roi(self, roi, use_current=False):

super().apply_roi(roi, use_current=use_current)

def _get_clone_viewer_reference(self):
base_name = self.reference.split("[")[0]
name = base_name
ind = 0
while name in self.jdaviz_helper.viewers.keys():
ind += 1
name = f"{base_name}[{ind}]"
return name

def clone_viewer(self):
name = self._get_clone_viewer_reference()

self.jdaviz_app._on_new_viewer(NewViewerMessage(self.__class__,
data=None,
sender=self.jdaviz_app),
vid=name, name=name)

this_viewer_item = self.jdaviz_app._get_viewer_item(self.reference)
this_state = self.state.as_dict()
for data in self.jdaviz_app.data_collection:
data_id = self.jdaviz_app._data_id_from_label(data.label)
visible = this_viewer_item['selected_data_items'].get(data_id, 'hidden')
self.jdaviz_app.set_data_visibility(name, data.label, visible == 'visible')
# TODO: don't revert color when adding same data to a new viewer
# (same happens when creating a phase-viewer from ephemeris plugin)

new_viewer = self.jdaviz_helper.viewers[name]._obj
for k, v in this_state.items():
if k in ('layers',):
continue
setattr(new_viewer.state, k, v)

return new_viewer.user_api


@viewer_registry("lcviz-phase-viewer", label="phase-vs-time")
class PhaseScatterView(TimeScatterView):
Expand All @@ -263,3 +264,61 @@ def times_to_phases(self, times):
raise ValueError("must have ephemeris plugin loaded to convert")

return ephem.times_to_phases(times, ephem_component=self.ephemeris_component)


@viewer_registry("lcviz-cube-viewer", label="cube")
class CubeView(CloneViewerMixin, CubevizImageView):
# categories: zoom resets, zoom, pan, subset, select tools, shortcuts
tools_nested = [
['jdaviz:homezoom', 'jdaviz:prevzoom'],
['jdaviz:boxzoom'],
['jdaviz:panzoom'],
['bqplot:rectangle'],
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there anything stopping us from allowing other ROI shapes at this point?

Copy link
Member Author

Choose a reason for hiding this comment

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

not really, but we'll have to see what we want to support for actual photometric extraction (and if we want sub-pixel support or not).

Copy link
Contributor

Choose a reason for hiding this comment

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

Agreed, but even rectangle ROIs aren't required to have 100% overlap with the pixel grid. I don't see a difference with other shapes in that sense.

Copy link
Member Author

Choose a reason for hiding this comment

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

right, we might decide to just do pixel-by-pixel masking if we don't want sub-pixel at all. Let's consider this when we write the extraction plugin - as of now the subsets don't do anything but I just wanted at least something to show up in the menu to test that they are created succesfully.

['jdaviz:sidebar_plot', 'jdaviz:sidebar_export']
]
# TODO: can we vary this default_class based on Kepler vs TESS, etc?
default_class = KeplerTargetPixelFile
Copy link
Contributor

Choose a reason for hiding this comment

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

Curious to check in on this. Does the mission specificity matter in the viewer class?

Copy link
Member Author

Choose a reason for hiding this comment

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

yeah, I wasn't sure what to do with this 🤔 . Ultimately this is just for the default and the user can override, but maybe its better to just have as None and require the user to pass a class?

Copy link
Contributor

Choose a reason for hiding this comment

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

I like that.

Copy link
Member Author

Choose a reason for hiding this comment

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

done and linked a comment back here if we ever want to revisit.


def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

self.display_mask = False
self.time_unit = kwargs.get('time_unit', u.d)
self.initialize_toolbar()
self._subscribe_to_layers_update()

# Hide axes by default
self.state.show_axes = False
Comment on lines +291 to +292
Copy link
Contributor

Choose a reason for hiding this comment

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

Just commenting that I like show_axes = False as the default. I see you toggle axes back on in the example code – handy to have the option, but takes up a lot of space.

Copy link
Member Author

Choose a reason for hiding this comment

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

ah, yes, that was mostly for debugging and confirming the mouseover information, I don't think we'd want that in any public-facing example.


# TODO: refactor upstream so lcviz can inherit cubeviewer methods/setup with jdaviz-specific
# logic:
# * _default_spectrum_viewer_reference_name
# * _default_flux_viewer_reference_name
# * _default_uncert_viewer_reference_name

def _initial_x_axis(self, *args):
# Make sure that the x_att/y_att is correct on data load
# called via a callback set upstream in CubevizImageView when reference_data is changed
ref_data = self.state.reference_data
if ref_data is not None:
self.state.x_att = ref_data.id['Pixel Axis 2 [x]']
self.state.y_att = ref_data.id['Pixel Axis 1 [y]']

def _on_layers_update(self, layers=None):
super()._on_layers_update(layers=layers)
ref_data = self.state.reference_data
if ref_data is None:
return
flux_comp = ref_data.id['flux']
for layer in self.state.layers:
if hasattr(layer, 'attribute') and layer.attribute != flux_comp:
layer.attribute = flux_comp

def data(self, cls=None):
# TODO: generalize upstream in jdaviz.
# This method is generalized from
# jdaviz/configs/cubeviz/plugins/viewers.py
return [layer_state.layer
for layer_state in self.state.layers
if hasattr(layer_state, 'layer') and
isinstance(layer_state.layer, BaseData)]
Loading