Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add methods for querying NASA Exoplanet Archive for ephemeris #127

Merged
merged 9 commits into from
Jul 29, 2024
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

* Prevent duplicate sub-intervals (quarter/sector/campaign) in data labels. [#120]

* Add feature to query the NASA Exoplanet Archive for exoplanet ephemerides. [#127]

0.4.3 (unreleased)
------------------

Expand Down
11 changes: 9 additions & 2 deletions lcviz/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,15 @@ def light_curve_like_kepler_quarter(seed=42):
)
lc['flux_alt'] = flux + 1
lc['flux_alt_err'] = flux_err
lc.meta['MISSION'] = 'KEPLER'
lc.meta['QUARTER'] = 10
lc.meta.update(
{
'MISSION': 'KEPLER',
'QUARTER': 10,
'OBJECT': 'HAT-P-11',
'RA': 297.7101763,
'DEC': 48.0818635,
}
)

return lc

Expand Down
164 changes: 157 additions & 7 deletions lcviz/plugins/ephemeris/ephemeris.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
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
Expand All @@ -8,8 +12,10 @@
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)
SelectPluginComponent, EditableSelectPluginComponent,
with_spinner)
from jdaviz.core.user_api import PluginUserApi
from jdaviz.core.events import SnackbarMessage

from lightkurve import periodogram, FoldedLightCurve

Expand All @@ -24,6 +30,8 @@
_default_dpdt = 0.0
_default_wrap_at = 1.0

_default_query_radius = 2 # [arcsec]


@tray_registry('ephemeris', label="Ephemeris")
class Ephemeris(PluginTemplateMixin, DatasetSelectMixin):
Expand Down Expand Up @@ -59,6 +67,19 @@
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"

Expand Down Expand Up @@ -87,13 +108,26 @@

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._default_initialized = False
self._ignore_ephem_change = False
self._ephemerides = {}
self._prev_wrap_at = _default_wrap_at
self._nasa_exoplanet_archive = None

self.dataset.add_filter(is_not_tpf)

Expand All @@ -117,6 +151,10 @@
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)
Expand All @@ -125,12 +163,16 @@

@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']
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_for_ephemeris',
'query_result', 'create_ephemeris_from_query'
]
return PluginUserApi(self, expose=expose)

def _phase_comp_lbl(self, component=None):
Expand Down Expand Up @@ -578,3 +620,111 @@
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):
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')

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

Check warning on line 661 in lcviz/plugins/ephemeris/ephemeris.py

View check run for this annotation

Codecov / codecov/patch

lcviz/plugins/ephemeris/ephemeris.py#L661

Added line #L661 was not covered by tests

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)
ref_time = self.app.data_collection[0].coords.reference_time.jd
if np.isnan(selected_query_result['pl_tranmid'].base.to_value(u.day)):
self.t0_from_catalog = 0

Check warning on line 686 in lcviz/plugins/ephemeris/ephemeris.py

View check run for this annotation

Codecov / codecov/patch

lcviz/plugins/ephemeris/ephemeris.py#L686

Added line #L686 was not covered by tests
else:
self.t0_from_catalog = (
selected_query_result['pl_tranmid'].base.to_value(u.day) - ref_time
) % self.period_from_catalog

@with_spinner('query_spinner')
def vue_query_for_ephemeris(self, *args):
self.query_for_ephemeris()

Check warning on line 694 in lcviz/plugins/ephemeris/ephemeris.py

View check run for this annotation

Codecov / codecov/patch

lcviz/plugins/ephemeris/ephemeris.py#L694

Added line #L694 was not covered by tests

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(

Check warning on line 701 in lcviz/plugins/ephemeris/ephemeris.py

View check run for this annotation

Codecov / codecov/patch

lcviz/plugins/ephemeris/ephemeris.py#L701

Added line #L701 was not covered by tests
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(

Check warning on line 720 in lcviz/plugins/ephemeris/ephemeris.py

View check run for this annotation

Codecov / codecov/patch

lcviz/plugins/ephemeris/ephemeris.py#L720

Added line #L720 was not covered by tests
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"
)
)

def vue_create_ephemeris_from_query(self, *args):
self.create_ephemeris_from_query()

Check warning on line 730 in lcviz/plugins/ephemeris/ephemeris.py

View check run for this annotation

Codecov / codecov/patch

lcviz/plugins/ephemeris/ephemeris.py#L730

Added line #L730 was not covered by tests
Loading