Skip to content

Commit 954f6cb

Browse files
authored
ephemeris plugin: custom phase-wrapping (#44)
* implement "wrap_at" functionality * set initial phase-viewer limits based on wrap_at * test coverage * add test-coverage for non-zero dpdt * update example notebook to use wrap_at instead of t0
1 parent 0816648 commit 954f6cb

File tree

5 files changed

+84
-15
lines changed

5 files changed

+84
-15
lines changed

lcviz/plugins/ephemeris/ephemeris.py

+43-13
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@
1818

1919
__all__ = ['Ephemeris']
2020

21-
_default_t0 = 0
22-
_default_period = 1
23-
_default_dpdt = 0
21+
_default_t0 = 0.0
22+
_default_period = 1.0
23+
_default_dpdt = 0.0
24+
_default_wrap_at = 1.0
2425

2526

2627
@tray_registry('ephemeris', label="Ephemeris")
@@ -39,6 +40,8 @@ class Ephemeris(PluginTemplateMixin, DatasetSelectMixin):
3940
Period of the ephemeris, defined at ``t0``.
4041
* :attr:`dpdt`:
4142
First derivative of the period of the ephemeris.
43+
* :attr:`wrap_at`:
44+
Phase at which to wrap (phased data will encompass the range 1-wrap_at to wrap_at).
4245
* :meth:`ephemeris`
4346
* :meth:`ephemerides`
4447
* :meth:`update_ephemeris`
@@ -68,6 +71,7 @@ class Ephemeris(PluginTemplateMixin, DatasetSelectMixin):
6871
period_step = Float(0.1).tag(sync=True)
6972
dpdt = FloatHandleEmpty(_default_dpdt).tag(sync=True)
7073
dpdt_step = Float(0.1).tag(sync=True)
74+
wrap_at = FloatHandleEmpty(_default_wrap_at).tag(sync=True)
7175

7276
# PERIOD FINDING
7377
method_items = List().tag(sync=True)
@@ -83,6 +87,7 @@ def __init__(self, *args, **kwargs):
8387

8488
self._ignore_ephem_change = False
8589
self._ephemerides = {}
90+
self._prev_wrap_at = _default_wrap_at
8691

8792
self.component = EditableSelectPluginComponent(self,
8893
mode='component_mode',
@@ -108,7 +113,7 @@ def __init__(self, *args, **kwargs):
108113

109114
@property
110115
def user_api(self):
111-
expose = ['component', 'period', 'dpdt', 't0',
116+
expose = ['component', 'period', 'dpdt', 't0', 'wrap_at',
112117
'ephemeris', 'ephemerides',
113118
'update_ephemeris', 'create_phase_viewer',
114119
'add_component', 'remove_component', 'rename_component',
@@ -136,6 +141,12 @@ def phase_viewer_ids(self):
136141
def phase_viewer_id(self):
137142
return self._phase_viewer_id(self.component_selected)
138143

144+
@property
145+
def phase_viewer(self):
146+
if not self.phase_viewer_exists:
147+
return None
148+
return self.app.get_viewer(self.phase_viewer_id)
149+
139150
@property
140151
def ephemerides(self):
141152
return self._ephemerides
@@ -150,17 +161,19 @@ def _times_to_phases_callable(self, component):
150161
t0 = self.t0
151162
period = self.period
152163
dpdt = self.dpdt
164+
wrap_at = self.wrap_at
153165
else:
154166
ephem = self.ephemerides.get(component, {})
155167
t0 = ephem.get('t0', _default_t0)
156168
period = ephem.get('period', _default_period)
157169
dpdt = ephem.get('dpdt', _default_dpdt)
170+
wrap_at = ephem.get('wrap_at', _default_wrap_at)
158171

159172
def _callable(times):
160173
if dpdt != 0:
161-
return np.mod(1./dpdt * np.log(1 + dpdt/period*(times-t0)), 1.0) # noqa
174+
return np.mod(1./dpdt * np.log(1 + dpdt/period*(times-t0)) + (1-wrap_at), 1.0) - (1-wrap_at) # noqa
162175
else:
163-
return np.mod((times-t0)/period, 1.0)
176+
return np.mod((times-t0)/period + (1-wrap_at), 1.0) - (1-wrap_at)
164177

165178
return _callable
166179

@@ -242,7 +255,8 @@ def create_phase_viewer(self):
242255
if self.phase_comp_lbl not in [comp.label for comp in dc[0].components]:
243256
self.update_ephemeris() # calls _update_all_phase_arrays
244257

245-
if not self.phase_viewer_exists:
258+
create_phase_viewer = not self.phase_viewer_exists
259+
if create_phase_viewer:
246260
# TODO: stack horizontally by default?
247261
self.app._on_new_viewer(NewViewerMessage(PhaseScatterView, data=None, sender=self.app),
248262
vid=phase_viewer_id, name=phase_viewer_id)
@@ -254,6 +268,8 @@ def create_phase_viewer(self):
254268
self.app.set_data_visibility(phase_viewer_id, data.label, visible == 'visible')
255269

256270
pv = self.app.get_viewer(phase_viewer_id)
271+
if create_phase_viewer:
272+
pv.state.x_min, pv.state.x_max = (self.wrap_at-1, self.wrap_at)
257273
pv.state.x_att = self.phase_cids[self.component_selected]
258274
return pv
259275

@@ -314,17 +330,18 @@ def _change_component(self, *args):
314330
self.t0 = ephem.get('t0', self.t0)
315331
self.period = ephem.get('period', self.period)
316332
self.dpdt = ephem.get('dpdt', self.dpdt)
333+
self.wrap_at = ephem.get('wrap_at', self.wrap_at)
317334

318335
# if this is a new component, update those default values back to the dictionary
319-
self.update_ephemeris(t0=self.t0, period=self.period, dpdt=self.dpdt)
336+
self.update_ephemeris(t0=self.t0, period=self.period, dpdt=self.dpdt, wrap_at=self.wrap_at)
320337
self._ignore_ephem_change = False
321338
if ephem:
322339
# if there were any changes applied by accessing the dictionary,
323340
# then we need to update phasing, etc (since we set _ignore_ephem_change)
324341
# otherwise, this is a new component and there is no need.
325342
self._ephem_traitlet_changed()
326343

327-
def update_ephemeris(self, component=None, t0=None, period=None, dpdt=None):
344+
def update_ephemeris(self, component=None, t0=None, period=None, dpdt=None, wrap_at=None):
328345
"""
329346
Update the ephemeris for a given component.
330347
@@ -337,7 +354,9 @@ def update_ephemeris(self, component=None, t0=None, period=None, dpdt=None):
337354
period : float, optional
338355
value of period to replace
339356
dpdt : float, optional
340-
value of period to replace
357+
value of dpdt to replace
358+
wrap_at : float, optional
359+
value of wrap_at to replace
341360
342361
Returns
343362
-------
@@ -350,7 +369,7 @@ def update_ephemeris(self, component=None, t0=None, period=None, dpdt=None):
350369
raise ValueError(f"component must be one of {self.component.choices}")
351370

352371
existing_ephem = self._ephemerides.get(component, {})
353-
for name, value in {'t0': t0, 'period': period, 'dpdt': dpdt}.items():
372+
for name, value in {'t0': t0, 'period': period, 'dpdt': dpdt, 'wrap_at': wrap_at}.items():
354373
if value is not None:
355374
existing_ephem[name] = value
356375
if component == self.component_selected:
@@ -360,11 +379,11 @@ def update_ephemeris(self, component=None, t0=None, period=None, dpdt=None):
360379
self._update_all_phase_arrays(component=component)
361380
return existing_ephem
362381

363-
@observe('period', 'dpdt', 't0')
382+
@observe('period', 'dpdt', 't0', 'wrap_at')
364383
def _ephem_traitlet_changed(self, event={}):
365384
if self._ignore_ephem_change:
366385
return
367-
for value in (self.period, self.dpdt, self.t0):
386+
for value in (self.period, self.dpdt, self.t0, self.wrap_at):
368387
if not isinstance(value, (int, float)):
369388
return
370389
if self.period <= 0:
@@ -383,6 +402,17 @@ def round_to_1(x):
383402
else:
384403
self._update_all_phase_arrays(component=self.component_selected)
385404

405+
# update zoom-limits if wrap_at was changed
406+
if event.get('name') == 'wrap_at':
407+
old = event.get('old') if event.get('old') != '' else self._prev_wrap_at
408+
if event.get('new') != '':
409+
pvs = self.phase_viewer.state
410+
delta_phase = event.get('new') - old
411+
pvs.x_min, pvs.x_max = pvs.x_min + delta_phase, pvs.x_max + delta_phase
412+
# we need to cache the old value since it could become a string
413+
# if the widget is cleared
414+
self._prev_wrap_at = event.get('new')
415+
386416
# update step-sizes
387417
self.period_step = round_to_1(self.period/5000)
388418
self.dpdt_step = max(round_to_1(abs(self.period * self.dpdt)/1000) if self.dpdt != 0 else 0,

lcviz/plugins/ephemeris/ephemeris.vue

+25
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,20 @@
7474
></v-text-field>
7575
</v-row>
7676

77+
<v-row>
78+
<v-text-field
79+
ref="wrap_at"
80+
type="number"
81+
label="Wrapping phase"
82+
v-model.number="wrap_at"
83+
:step="0.1"
84+
type="number"
85+
:hint="'Phased data will encompass the range '+wrap_at_range+'.'"
86+
persistent-hint
87+
:rules="[() => wrap_at!=='' || 'This field is required']"
88+
></v-text-field>
89+
</v-row>
90+
7791
<j-plugin-section-header>Period Finding/Refining</j-plugin-section-header>
7892

7993
<plugin-dataset-select
@@ -135,3 +149,14 @@
135149

136150
</j-tray-plugin>
137151
</template>
152+
153+
<script>
154+
module.exports = {
155+
computed: {
156+
wrap_at_range() {
157+
const lower = this.wrap_at - 1
158+
return '('+lower.toFixed(2)+', '+this.wrap_at.toFixed(2)+')'
159+
},
160+
}
161+
};
162+
</script>

lcviz/tests/test_plugin_ephemeris.py

+10
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
12
def test_docs_snippets(helper, light_curve_like_kepler_quarter):
23
lcviz, lc = helper, light_curve_like_kepler_quarter
34

@@ -29,6 +30,12 @@ def test_plugin_ephemeris(helper, light_curve_like_kepler_quarter):
2930
ephem._obj.vue_period_halve()
3031
assert ephem.period == 3.14
3132

33+
pv = ephem._obj.phase_viewer
34+
# original limits are set to 0->1 (technically 1-phase_wrap -> phase_wrap)
35+
assert (pv.state.x_min, pv.state.x_max) == (0.0, 1.0)
36+
ephem.wrap_at = 0.5
37+
assert (pv.state.x_min, pv.state.x_max) == (-0.5, 0.5)
38+
3239
ephem.add_component('custom component')
3340
assert not ephem._obj.phase_viewer_exists
3441
ephem.create_phase_viewer()
@@ -60,3 +67,6 @@ def test_plugin_ephemeris(helper, light_curve_like_kepler_quarter):
6067
assert ephem._obj.method_err == ''
6168
ephem._obj.vue_adopt_period_at_max_power()
6269
assert ephem.period != 2
70+
71+
# test coverage for non-zero dpdt
72+
ephem.dpdt = 0.00001

lcviz/viewers.py

+4
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ def _set_plot_x_axes(self, dc, component_labels, light_curve):
113113
xlabel = f'{str(x_unit.physical_type).title()} ({x_unit})'
114114

115115
self.figure.axes[0].label = xlabel
116+
self.figure.axes[0].num_ticks = 5
116117

117118
def _set_plot_y_axes(self, dc, component_labels, light_curve):
118119
self.state.y_att = dc[0].components[component_labels.index('flux')]
@@ -142,6 +143,8 @@ def _set_plot_y_axes(self, dc, component_labels, light_curve):
142143
self.figure.axes[0].tick_format = 'g'
143144
self.figure.axes[1].tick_format = 'g'
144145

146+
self.figure.axes[1].num_ticks = 5
147+
145148
def _expected_subset_layer_default(self, layer_state):
146149
super()._expected_subset_layer_default(layer_state)
147150

@@ -212,6 +215,7 @@ def _set_plot_x_axes(self, dc, component_labels, light_curve):
212215
# setting of y_att will be handled by ephemeris plugin
213216
self.state.x_att = dc[0].components[component_labels.index(f'phase:{self.ephemeris_component}')] # noqa
214217
self.figure.axes[0].label = 'phase'
218+
self.figure.axes[0].num_ticks = 5
215219

216220
def times_to_phases(self, times):
217221
ephem = self.jdaviz_helper.plugins.get('Ephemeris', None)

notebooks/LCvizExample.ipynb

+2-2
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,8 @@
119119
" (morris2017_epoch - reference_time).to_value(time_coordinates.unit) % eph.period\n",
120120
")\n",
121121
"\n",
122-
"# offset the phase axis so mid-transit occurs at phase=0.5:\n",
123-
"eph.t0 += 0.5 * eph.period"
122+
"# offset the wrapping phase so the transit (at phase 0) displays at center\n",
123+
"eph.wrap_at = 0.5"
124124
]
125125
},
126126
{

0 commit comments

Comments
 (0)