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
149 changes: 142 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 @@ -87,13 +95,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 +138,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 +150,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', 'adopt_from_catalog', 'adopt_from_catalog_in_new_viewer'
]
return PluginUserApi(self, expose=expose)

def _phase_comp_lbl(self, component=None):
Expand Down Expand Up @@ -578,3 +607,109 @@
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 648 in lcviz/plugins/ephemeris/ephemeris.py

View check run for this annotation

Codecov / codecov/patch

lcviz/plugins/ephemeris/ephemeris.py#L648

Added line #L648 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 673 in lcviz/plugins/ephemeris/ephemeris.py

View check run for this annotation

Codecov / codecov/patch

lcviz/plugins/ephemeris/ephemeris.py#L673

Added line #L673 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 681 in lcviz/plugins/ephemeris/ephemeris.py

View check run for this annotation

Codecov / codecov/patch

lcviz/plugins/ephemeris/ephemeris.py#L681

Added line #L681 was not covered by tests

def adopt_from_catalog(self, *args):
if len(self._get_phase_viewers()):
# if a phase viewer is available, adopt the ephemeris in the phase viewer:
if not np.any(np.isnan([self.period_from_catalog, self.t0_from_catalog])):
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:
# otherwise, adopt the ephemeris in a new phase viewer:
self.adopt_from_catalog_in_new_viewer()

def adopt_from_catalog_in_new_viewer(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 703 in lcviz/plugins/ephemeris/ephemeris.py

View check run for this annotation

Codecov / codecov/patch

lcviz/plugins/ephemeris/ephemeris.py#L703

Added line #L703 was not covered by tests
SnackbarMessage(
f"Ephemeris component {new_component_label} already exists, skipping.",
sender=self, color="warning"
)
)
else:
self.add_component(new_component_label)
self.create_phase_viewer()
self.adopt_from_catalog()
kecnry marked this conversation as resolved.
Show resolved Hide resolved

def vue_adopt_from_catalog_in_new_viewer(self, *args):
self.adopt_from_catalog_in_new_viewer()

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

View check run for this annotation

Codecov / codecov/patch

lcviz/plugins/ephemeris/ephemeris.py#L715

Added line #L715 was not covered by tests
119 changes: 114 additions & 5 deletions lcviz/plugins/ephemeris/ephemeris.vue
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@
label="Period derivative"
v-model.number="dpdt"
:step="dpdt_step"
type="number"
hint="The first time-derivative of the period of the ephemeris."
persistent-hint
:rules="[() => dpdt!=='' || 'This field is required']"
Expand All @@ -81,7 +80,6 @@
label="Wrapping phase"
v-model.number="wrap_at"
:step="0.1"
type="number"
:hint="'Phased data will encompass the range '+wrap_at_range+'.'"
persistent-hint
:rules="[() => wrap_at!=='' || 'This field is required']"
Expand Down Expand Up @@ -110,8 +108,6 @@
></v-select>
</v-row>



<div style="display: grid"> <!-- overlay container -->
<div style="grid-area: 1/1">

Expand All @@ -120,7 +116,7 @@
</v-row>
<v-row v-else>
<j-tooltip :tooltipcontent="'adopt period into '+component_selected+' ephemeris.'">
<v-btn text color='primary '@click='adopt_period_at_max_power' style="padding: 0px">
<v-btn text color='primary' @click='adopt_period_at_max_power' style="padding: 0px">
period: {{period_at_max_power}}
</v-btn>
</j-tooltip>
Expand All @@ -144,7 +140,120 @@
</div>
</div>

<j-plugin-section-header>Query NASA Exoplanet Archive</j-plugin-section-header>
<v-row>
<span class="v-messages v-messages__message text--secondary">
Query the
<a href="https://exoplanetarchive.ipac.caltech.edu/docs/pscp_about.html" target="_blank">
Planetary Systems Composite Data</a> table from
<a href="https://exoplanetarchive.ipac.caltech.edu/" target="_blank">
NASA Exoplanet Archive</a>. Queries first by name, then falls back on
coordinates if the object name is not recognized.
</span>
</v-row>

<v-row>
<v-text-field
ref="query_name"
type="string"
label="Object name"
v-model.number="query_name"
hint="Object name."
persistent-hint
></v-text-field>
</v-row>
<v-row>
<v-text-field
ref="query_ra"
type="number"
label="RA (degrees)"
v-model.number="query_ra"
:step="ra_dec_step"
hint="Object right ascension."
persistent-hint
></v-text-field>
</v-row>
<v-row>
<v-text-field
ref="query_dec"
type="number"
label="Dec (degrees)"
v-model.number="query_dec"
:step="ra_dec_step"
hint="Object declination."
persistent-hint
></v-text-field>
</v-row>
<v-row>
<v-text-field
ref="query_radius"
type="number"
label="Radius (arcseconds)"
v-model.number="query_radius"
:step="1"
hint="Radius around the query coordinate."
persistent-hint
></v-text-field>
</v-row>

<v-row justify="end">
<j-tooltip tooltipcontent="Query for this object.">
<plugin-action-button
:spinner="query_spinner"
:results_isolated_to_plugin="false"
@click="query_for_ephemeris">
Query
</plugin-action-button>
</j-tooltip>
</v-row>
<div v-if="query_result_items.length > 0">
<v-row>
<v-select
:menu-props="{ left: true }"
attach
:items="query_result_items"
:item-value="item => item.label"
v-model="query_result_selected"
label="Ephemerides available"
:hint="'Ephemeris parameters from ' + query_result_items.length + ' available query result(s)'"
persistent-hint
dense
>

<template v-slot:selection="{ item }">
<span>
{{ item.label }}
</span>
</template>
<template v-slot:item="{ item }">
<span style="margin-top: 8px; margin-bottom: 0px">
{{ item.label }}
<v-row style="line-height: 1.0; margin: 0px; opacity: 0.85; font-size: 8pt">
Period: {{ item.period }} d, Epoch: {{ item.epoch }} d
</v-row>
</span>
</template>

</v-select>
</v-row>

<v-row v-if="query_result_selected !== ''">
<span class="v-messages v-messages__message text--secondary">
Period: {{period_from_catalog}} d, Epoch: {{t0_from_catalog}} d
</span>
<j-tooltip :tooltipcontent="'Adopt period and epoch into '+component_selected+' ephemeris.'">
<v-row justify="end">
<v-col>
<plugin-action-button
@click="adopt_from_catalog_in_new_viewer"
:disabled="component_items.map(item => item.label).includes(query_result_selected.replace(/\s/g, ''))">
Create new component
</plugin-action-button>
</v-col>
</v-row>
</j-tooltip>
</v-row>
</div>
</div>

</j-tray-plugin>
Expand Down
3 changes: 2 additions & 1 deletion lcviz/tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,8 @@ def test_apply_yrangerois(helper, light_curve_like_kepler_quarter):
def test_data_label(helper, light_curve_like_kepler_quarter):
# add data without specifying data label:
helper.load_data(light_curve_like_kepler_quarter)
assert helper.app.data_collection[-1].label == 'Light curve [Q10]'
object_name = helper.app.data_collection[-1].meta['OBJECT']
assert helper.app.data_collection[-1].label == f'{object_name} [Q10]'

# specify label, check that quarter isn't appended:
data_label = 'Cool target'
Expand Down
Loading