import numpy as np
from astropy.coordinates import SkyCoord
from astropy.time import Time
import astropy.units as u
from astroquery.ipac.nexsci.nasa_exoplanet_archive import NasaExoplanetArchive
from traitlets import Bool, Float, List, Unicode, observe
from glue.core.link_helpers import LinkSame
from glue.core.message import DataCollectionAddMessage
from jdaviz.core.custom_traitlets import FloatHandleEmpty
from jdaviz.core.events import (NewViewerMessage, ViewerAddedMessage, ViewerRemovedMessage)
from jdaviz.core.registries import tray_registry
from jdaviz.core.template_mixin import (PluginTemplateMixin, DatasetSelectMixin,
SelectPluginComponent, EditableSelectPluginComponent,
with_spinner)
from jdaviz.core.user_api import PluginUserApi
from jdaviz.core.events import SnackbarMessage
from lightkurve import periodogram, FoldedLightCurve, LightCurve
from lcviz.events import EphemerisComponentChangedMessage, EphemerisChangedMessage
from lcviz.viewers import TimeScatterView, PhaseScatterView
from lcviz.utils import is_lc, is_not_tpf, phase_comp_lbl
__all__ = ['Ephemeris']
_default_t0 = 0.0
_default_period = 1.0
_default_dpdt = 0.0
_default_wrap_at = 1.0
_default_query_radius = 2 # [arcsec]
[docs]
@tray_registry('ephemeris', label="Ephemeris", category='data:analysis')
class Ephemeris(PluginTemplateMixin, DatasetSelectMixin):
"""
See the :ref:`Ephemeris Plugin Documentation <ephemeris>` for more details.
Only the following attributes and methods are available through the
:ref:`public plugin API <plugin-apis>`:
* :meth:`~jdaviz.core.template_mixin.PluginTemplateMixin.show`
* :meth:`~jdaviz.core.template_mixin.PluginTemplateMixin.open_in_tray`
* :meth:`~jdaviz.core.template_mixin.PluginTemplateMixin.close_in_tray`
* ``component`` (:class:`~jdaviz.core.template_mixin.EditableSelectPluginComponent`):
Label of the component corresponding to the active ephemeris.
* :attr:`t0` : float
Zeropoint of the ephemeris.
* :attr:`period` : float
Period of the ephemeris, defined at ``t0``.
* :attr:`dpdt` : float
First derivative of the period of the ephemeris.
* :attr:`wrap_at` : float
Phase at which to wrap (phased data will encompass the range 1-wrap_at to wrap_at).
* :meth:`ephemeris`
* :meth:`ephemerides`
* :meth:`update_ephemeris`
* :meth:`create_phase_viewer`
* :meth:`add_component`
* :meth:`rename_component`
* :meth:`times_to_phases`
* :meth:`phases_to_times`
* :meth:`get_data`
* ``dataset`` (:class:`~jdaviz.core.template_mixin.DatasetSelect`):
Dataset to use for determining the period.
* ``method`` (:class:`~jdaviz.core.template_mixin.SelectPluginComponent`):
Method/algorithm to determine the period.
* :meth:`query_for_ephemeris`
Query the `NASA Exoplanet Archive <https://exoplanetarchive.ipac.caltech.edu/>`_'s
`Planetary System Composite Parameters
<https://exoplanetarchive.ipac.caltech.edu/docs/pscp_about.html>`_
table for the planet-hosting star identified
by the observation's header key "OBJECT", or if that fails,
by the observation's header keys for RA and Dec.
* ``query_result`` (:class:`~jdaviz.core.template_mixin.SelectPluginComponent`):
The name of a planet from a NASA Exoplanet Archive query, used for
adopting literature values for the orbital period and mid-transit time.
* :meth:`create_ephemeris_from_query`
Create an ephemeris component with the period and epoch from
the planet selected from the NASA Exoplanet Archive query in ``query_result``.
"""
template_file = __file__, "ephemeris.vue"
reference_time = None
# EPHEMERIS
component_mode = Unicode().tag(sync=True)
component_edit_value = Unicode().tag(sync=True)
component_items = List().tag(sync=True)
component_selected = Unicode().tag(sync=True)
phase_viewer_exists = Bool(False).tag(sync=True)
t0 = FloatHandleEmpty(_default_t0).tag(sync=True)
t0_step = Float(0.1).tag(sync=True)
period = FloatHandleEmpty(_default_period).tag(sync=True)
period_step = Float(0.1).tag(sync=True)
dpdt = FloatHandleEmpty(_default_dpdt).tag(sync=True)
dpdt_step = Float(0.1).tag(sync=True)
wrap_at = FloatHandleEmpty(_default_wrap_at).tag(sync=True)
# PERIOD FINDING
method_items = List().tag(sync=True)
method_selected = Unicode().tag(sync=True)
method_spinner = Bool().tag(sync=True)
method_err = Unicode().tag(sync=True)
period_at_max_power = Float().tag(sync=True)
# QUERIES
query_name = Unicode().tag(sync=True)
query_ra = FloatHandleEmpty().tag(sync=True)
query_dec = FloatHandleEmpty().tag(sync=True)
query_radius = FloatHandleEmpty(_default_query_radius).tag(sync=True)
query_result_items = List().tag(sync=True)
query_result_selected = Unicode().tag(sync=True)
ra_dec_step = Float(0.01).tag(sync=True)
period_from_catalog = FloatHandleEmpty().tag(sync=True)
t0_from_catalog = FloatHandleEmpty().tag(sync=True)
query_spinner = Bool().tag(sync=True)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._plugin_description = 'Ephemerides for phase-folding.'
self._default_initialized = False
self._ignore_ephem_change = False
self._ephemerides = {}
self._prev_wrap_at = _default_wrap_at
self._nasa_exoplanet_archive = None
self.dataset.get_data_cls = LightCurve
self.dataset.add_filter(is_lc)
self.component = EditableSelectPluginComponent(self,
name='ephemeris',
mode='component_mode',
edit_value='component_edit_value',
items='component_items',
selected='component_selected',
manual_options=['default'],
on_add=self._on_component_add,
on_rename_after_selection=self._on_component_rename, # noqa
on_remove_after_selection=self._on_component_remove, # noqa
validate_choice=self._validate_component)
# force the original entry in ephemerides with defaults
self._change_component()
self.method = SelectPluginComponent(self,
items='method_items',
selected='method_selected',
manual_options=['Lomb-Scargle', 'Box Least Squares'])
self.query_result = SelectPluginComponent(self,
items='query_result_items',
selected='query_result_selected')
# TODO: could optimize by only updating for the new data entry only
# (would require some refactoring and probably wouldn't have significant gains)
self.hub.subscribe(self, DataCollectionAddMessage, handler=self._update_all_phase_arrays)
self.hub.subscribe(self, ViewerAddedMessage, handler=self._check_if_phase_viewer_exists)
self.hub.subscribe(self, ViewerRemovedMessage, handler=self._check_if_phase_viewer_exists)
self._set_relevant()
@observe('dataset_items')
def _set_relevant(self, *args):
if not len(self.dataset_items):
self.irrelevant_msg = 'No valid datasets loaded'
else:
self.irrelevant_msg = ''
@property
def user_api(self):
expose = [
'component', 'period', 'dpdt', 't0', 'wrap_at',
'ephemeris', 'ephemerides',
'update_ephemeris', 'create_phase_viewer',
'add_component', 'remove_component', 'rename_component',
'times_to_phases', 'phases_to_times', 'get_data',
'dataset', 'method', 'period_at_max_power',
'adopt_period_at_max_power',
'query_name', 'query_ra', 'query_dec', 'query_radius',
'query_for_ephemeris', 'query_result', 'create_ephemeris_from_query'
]
return PluginUserApi(self, expose=expose)
def _phase_comp_lbl(self, component=None):
if component is None:
component = self.component_selected
return phase_comp_lbl(component)
@property
def phase_comp_lbl(self):
return self._phase_comp_lbl()
def _generate_phase_viewer_id(self, component=None):
if component is None:
component = self.component_selected
return self.app._jdaviz_helper._get_clone_viewer_reference(f'flux-vs-phase:{component}')
def _get_phase_viewers(self, lbl=None):
if lbl is None:
lbl = self.component_selected
return [viewer for vid, viewer in self.app._viewer_store.items()
if isinstance(viewer, PhaseScatterView)
and viewer._ephemeris_component == lbl]
@property
def default_phase_viewer(self):
if not self.phase_viewer_exists:
return None
# we'll just treat the "default" as the first viewer connected to this
# ephemeris component
return self._get_phase_viewers()[0]
@property
def ephemerides(self):
return self._ephemerides
@property
def ephemeris(self):
return self.ephemerides.get(self.component_selected, {})
def _times_to_phases_callable(self, component):
if component == self.component_selected:
# retrieving from traitlets is cheaper than dictionaries
t0 = self.t0
period = self.period
dpdt = self.dpdt
wrap_at = self.wrap_at
else:
ephem = self.ephemerides.get(component, {})
t0 = ephem.get('t0', _default_t0)
period = ephem.get('period', _default_period)
dpdt = ephem.get('dpdt', _default_dpdt)
wrap_at = ephem.get('wrap_at', _default_wrap_at)
def _callable(times):
if hasattr(times, '__len__') and 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:
return np.mod((times-t0)/period + (1-wrap_at), 1.0) - (1-wrap_at)
return _callable
[docs]
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(ephem_component)(times)
[docs]
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(ephem_component, {})
t0 = ephem.get('t0', _default_t0)
period = ephem.get('period', _default_period)
dpdt = ephem.get('dpdt', _default_dpdt)
if dpdt != 0:
return t0 + period/dpdt*(np.exp(dpdt*(phases))-1.0)
else:
return t0 + (phases)*period
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(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(ephem_component)
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)
self.app._jdaviz_helper._set_data_component(
data, _phase_comp_lbl, phases
)
if i != 0:
ref_data = dc[0]
new_link = LinkSame(
cid1=ref_data.world_component_ids[0],
cid2=data.world_component_ids[0],
data1=ref_data,
data2=data,
labels1=ref_data.label,
labels2=data.label
)
new_links.append(new_link)
dc.add_link(new_links)
# update any plugin markers
for viewer in self._get_phase_viewers(ephem_component):
for mark in viewer.custom_marks:
if hasattr(mark, 'update_phase_folding'):
mark.update_phase_folding()
return _phase_comp_lbl
def _set_viewer_to_ephem_component(self, viewer, ephem_component=None):
viewer._ephemeris_component = ephem_component
# set x_att
phase_comp = self.app._jdaviz_helper._component_ids[self._phase_comp_lbl(ephem_component)]
viewer.state.x_att = phase_comp
# set viewer limits
wrap_at = self.ephemerides.get(ephem_component, {}).get('wrap_at', self.wrap_at)
viewer.state.x_min, viewer.state.x_max = (wrap_at-1, wrap_at)
[docs]
def create_phase_viewer(self, ephem_component=None):
"""
Create a new phase viewer corresponding to ``component`` and populate the phase arrays
with the current ephemeris, if necessary.
Parameters
----------
ephem_component : str, optional
label of the component. If not provided or ``None``, will default to plugin value.
"""
if ephem_component is None:
ephem_component = self.component_selected
_phase_comp_lbl = self._phase_comp_lbl(ephem_component)
dc = self.app.data_collection
# check to see if this component already has a phase array. We'll just check the first
# item in the data-collection since the rest of the logic in this plugin /should/ populate
# the arrays across all entries.
if _phase_comp_lbl not in [comp.label for comp in dc[0].components]:
self.update_ephemeris() # calls _update_all_phase_arrays
phase_viewer_id = self._generate_phase_viewer_id(ephem_component)
# TODO: stack horizontally by default?
self.app._on_new_viewer(NewViewerMessage(PhaseScatterView, data=None, sender=self.app),
vid=phase_viewer_id, name=phase_viewer_id,
open_data_menu_if_empty=False)
# access new viewer, set bookkeeping for ephemeris component
pv = self.app.get_viewer(phase_viewer_id)
pv._ephemeris_component = ephem_component
# since we couldn't set ephemeris_component right away, _check_if_phase_viewer_exists
# might be out-of-date
self._check_if_phase_viewer_exists()
# set default data visibility
tvs = self.get_matching_viewers(TimeScatterView)
if len(tvs):
tvdm = tvs[0].data_menu
visible_layers = tvdm.data_labels_visible
else:
visible_layers = [dci.label for dci in dc if is_lc(dci) and is_not_tpf(dci)]
loaded_layers = pv.data_menu.data_labels_loaded
for data in dc:
if data.ndim > 1:
# skip image/cube entries
continue
visible = data.label in visible_layers
if data.label not in loaded_layers and data.label in pv.data_menu._obj.dataset.choices:
pv.data_menu.add_data(data.label)
pv.data_menu.set_layer_visibility(data.label, visible)
self._set_viewer_to_ephem_component(pv, ephem_component=ephem_component)
return pv.user_api
[docs]
def vue_create_phase_viewer(self, *args):
if not self.phase_viewer_exists:
self.create_phase_viewer()
[docs]
def vue_period_halve(self, *args):
self.period /= 2
[docs]
def vue_period_double(self, *args):
self.period *= 2
def _check_if_phase_viewer_exists(self, *args):
self.phase_viewer_exists = len(self._get_phase_viewers()) > 0
def _validate_component(self, lbl):
if '[' in lbl or ']' in lbl:
return 'cannot contain square brackets'
if ':' in lbl:
return 'cannot contain colon'
return ''
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, {})
for viewer in self._get_phase_viewers(old_lbl):
self.app._update_viewer_reference_name(
viewer._ref_or_id,
viewer._ref_or_id.replace(old_lbl, new_lbl),
update_id=True
)
viewer._ephemeris_component = 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))
def _on_component_remove(self, lbl):
_ = self._ephemerides.pop(lbl, {})
# remove the corresponding viewer(s), if any exist
for viewer in self._get_phase_viewers(lbl):
self.app.vue_destroy_viewer_item(viewer._ref_or_id)
self.hub.broadcast(EphemerisComponentChangedMessage(old_lbl=lbl, new_lbl=None,
sender=self))
[docs]
def rename_component(self, old_lbl, new_lbl):
# NOTE: the component will call _on_component_rename after updating
self.component.rename_choice(old_lbl, new_lbl)
[docs]
def add_component(self, lbl, set_as_selected=True):
self.component.add_choice(lbl, set_as_selected=set_as_selected)
[docs]
def remove_component(self, lbl):
# NOTE: the component will call _on_component_remove after updating
self.component.remove_choice(lbl)
@observe('component_selected')
def _change_component(self, *args):
if not hasattr(self, 'component'):
# plugin/traitlet startup
return
if self.component_selected == '':
# no component selected (this can happen when removing all components)
return
self._check_if_phase_viewer_exists()
ephem = self._ephemerides.get(self.component_selected, {})
# we'll temporarily disable updating the phasing so that we can set all
# traitlets simultaneously and THEN revising phase arrays if necessary
self._ignore_ephem_change = True
self.t0 = ephem.get('t0', self.t0)
self.period = ephem.get('period', self.period)
self.dpdt = ephem.get('dpdt', self.dpdt)
self.wrap_at = ephem.get('wrap_at', self.wrap_at)
# if this is a new component, update those default values back to the dictionary
self.update_ephemeris(t0=self.t0, period=self.period, dpdt=self.dpdt, wrap_at=self.wrap_at)
self._ignore_ephem_change = False
if ephem:
# if there were any changes applied by accessing the dictionary,
# then we need to update phasing, etc (since we set _ignore_ephem_change)
# otherwise, this is a new component and there is no need.
self._ephem_traitlet_changed()
[docs]
def update_ephemeris(self, ephem_component=None, t0=None, period=None, dpdt=None, wrap_at=None):
"""
Update the ephemeris for a given component.
Parameters
----------
ephem_component : str, optional
label of the component. If not provided or ``None``, will default to plugin value.
t0 : float, optional
value of t0 to replace
period : float, optional
value of period to replace
dpdt : float, optional
value of dpdt to replace
wrap_at : float, optional
value of wrap_at to replace
Returns
-------
dictionary of ephemeris corresponding to ``component``
"""
if ephem_component is None:
ephem_component = self.component_selected
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(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 ephem_component == self.component_selected:
setattr(self, name, value)
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
@observe('period', 'dpdt', 't0', 'wrap_at')
def _ephem_traitlet_changed(self, event={}):
if self._ignore_ephem_change:
return
for value in (self.period, self.dpdt, self.t0, self.wrap_at):
if not isinstance(value, (int, float)):
return
if self.period <= 0:
return
def round_to_1(x):
return round(x, -int(np.floor(np.log10(abs(x)))))
# if phase-viewer doesn't yet exist in the app, create it now
if not self.phase_viewer_exists:
self.create_phase_viewer()
# update value in the dictionary (to support multi-ephems)
if event:
self.update_ephemeris(**{event.get('name'): event.get('new')})
# will call _update_all_phase_arrays
else:
self._update_all_phase_arrays(ephem_component=self.component_selected)
# update zoom-limits if wrap_at was changed
if event.get('name') == 'wrap_at':
old = event.get('old') if event.get('old') != '' else self._prev_wrap_at
if event.get('new') != '':
delta_phase = event.get('new') - old
for pv in self._get_phase_viewers():
pvs = pv.state
pvs.x_min, pvs.x_max = pvs.x_min + delta_phase, pvs.x_max + delta_phase
# we need to cache the old value since it could become a string
# if the widget is cleared
self._prev_wrap_at = event.get('new')
# update step-sizes
self.period_step = round_to_1(self.period/5000)
self.dpdt_step = max(round_to_1(abs(self.period * self.dpdt)/1000) if self.dpdt != 0 else 0,
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')):
return
if self.reference_time is None and self.dataset.selected_obj is not None:
self.reference_time = self.dataset.selected_obj.meta.get('REFTIME', 0.0)
# TODO: support multiselect on self.dataset and combine light curves (or would that be a
# dedicated plugin of its own)?
self.method_spinner = True
self.method_err = ''
if self.method == 'Box Least Squares':
try:
per = periodogram.BoxLeastSquaresPeriodogram.from_lightcurve(self.dataset.selected_obj) # noqa
except Exception as err:
self.method_spinner = False
self.method_err = str(err)
return
elif self.method == 'Lomb-Scargle':
try:
per = periodogram.LombScarglePeriodogram.from_lightcurve(self.dataset.selected_obj)
except Exception as err:
self.method_spinner = False
self.method_err = str(err)
return
else: # pragma: no cover
self.method_spinner = False
raise NotImplementedError(f"periodogram not implemented for {self.method}")
# TODO: will need to return in display units once supported
self.period_at_max_power = per.period_at_max_power.value
self.method_spinner = False
[docs]
def adopt_period_at_max_power(self):
self.period = self.period_at_max_power
[docs]
def vue_adopt_period_at_max_power(self, *args):
self.adopt_period_at_max_power()
[docs]
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 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:{ephem_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)
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(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")
return phlc
@property
def nasa_exoplanet_archive(self):
if self._nasa_exoplanet_archive is None:
self._nasa_exoplanet_archive = NasaExoplanetArchive()
return self._nasa_exoplanet_archive
@observe('dataset_selected')
def _query_params_from_metadata(self, *args):
if not hasattr(self, 'dataset'):
return
if self.dataset.selected_obj is None:
return
self.query_name = self.dataset.selected_obj.meta.get('OBJECT', '')
self.query_ra = self.dataset.selected_obj.meta.get('RA')
self.query_dec = self.dataset.selected_obj.meta.get('DEC')
[docs]
def query_for_ephemeris(self):
query_result = None
if self.query_name:
# first query by object name:
query_result = self.nasa_exoplanet_archive.query_object(
object_name=self.query_name,
table='pscomppars'
)
if (
(query_result is None or len(query_result) == 0) and
(None not in (self.query_ra, self.query_dec))
):
# next query by coordinates:
coord = SkyCoord(ra=self.query_ra, dec=self.query_dec, unit=u.deg)
query_result = self.nasa_exoplanet_archive.query_region(
table='pscomppars',
coordinates=coord,
radius=self.query_radius * u.arcsec,
)
if query_result is None or len(query_result) == 0:
# no metadata found for RA, Dec, or object name
return None
else:
query_result.sort('pl_name')
self.astroquery_result = query_result
self.astroquery_result.add_index('pl_name')
self.query_result_items = [
{
'label': name, # required key for SelectPluginComponent
'period': period,
'epoch': epoch if not np.isnan(epoch) else 0
}
for name, period, epoch in zip(
list(self.astroquery_result['pl_name']),
np.array(self.astroquery_result['pl_orbper'].to_value(u.day)),
np.array(self.astroquery_result['pl_tranmid'].to_value(u.day))
)
]
@observe('query_result_selected')
def _select_query_result(self, *args):
selected_query_result = self.astroquery_result.loc[self.query_result_selected]
self.period_from_catalog = selected_query_result['pl_orbper'].base.to_value(u.day)
if np.isnan(selected_query_result['pl_tranmid'].base.to_value(u.day)):
self.t0_from_catalog = 0
else:
self.t0_from_catalog = (
selected_query_result['pl_tranmid'].base.to_value(u.day) - self.reference_time
) % self.period_from_catalog
[docs]
@with_spinner('query_spinner')
def vue_query_for_ephemeris(self, *args):
self.query_for_ephemeris()
[docs]
def create_ephemeris_from_query(self, *args):
new_component_label = self.query_result_selected.replace(' ', '')
if new_component_label in self.component.choices:
# warn the user that an ephemeris component already exists with this label,
# a second won't be added:
self.hub.broadcast(
SnackbarMessage(
f"Ephemeris component {new_component_label} already exists, "
f"this ephemeris component will not be added.",
sender=self, color="warning"
)
)
elif not np.any(np.isnan([self.period_from_catalog, self.t0_from_catalog])):
self.add_component(new_component_label)
self.create_phase_viewer()
self.period = self.period_from_catalog
self.t0 = self.t0_from_catalog
# reset the phase axis wrap to feature the primary transit:
self.wrap_at = 0.5
viewer = self._get_phase_viewers()[0]
viewer.reset_limits()
else:
self.hub.broadcast(
SnackbarMessage(
f"Catalog period ({self.period_from_catalog}) or "
f"epoch ({self.t0_from_catalog}) is NaN, this ephemeris "
f"component will not be added.",
sender=self, color="warning"
)
)
[docs]
def vue_create_ephemeris_from_query(self, *args):
self.create_ephemeris_from_query()