-
Notifications
You must be signed in to change notification settings - Fork 158
/
core.py
925 lines (796 loc) · 37.6 KB
/
core.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
"""Defines the Seismology class."""
import logging
import warnings
import numpy as np
from matplotlib import pyplot as plt
from scipy.signal import find_peaks
from astropy import units as u
from astropy.units import cds
from .. import MPLSTYLE
from . import utils, stellar_estimators
from ..periodogram import SNRPeriodogram
from ..utils import LightkurveWarning, validate_method
from .utils import SeismologyQuantity
# Import the optional Bokeh dependency required by ``interact_echelle```,
# or print a friendly error otherwise.
try:
import bokeh # Import bokeh first so we get an ImportError we can catch
from bokeh.io import show, output_notebook
from bokeh.plotting import figure
from bokeh.models import LogColorMapper, Slider, RangeSlider, Button
from bokeh.layouts import layout, Spacer
except:
# Nice error will be raised when ``interact_echelle``` is called.
pass
log = logging.getLogger(__name__)
__all__ = ["Seismology"]
class Seismology(object):
"""Enables astroseismic quantities to be estimated from periodograms.
This class provides easy access to methods to estimate numax, deltanu, radius,
mass, and logg, and stores them on its tray for easy diagnostic plotting.
Examples
--------
Download the TESS light curve for HIP 116158:
>>> import lightkurve as lk
>>> lc = lk.search_lightcurve("HIP 116158", sector=2).download() # doctest: +SKIP
>>> lc = lc.normalize().remove_nans().remove_outliers() # doctest: +SKIP
Create a Lomb-Scargle periodogram:
>>> pg = lc.to_periodogram(normalization='psd', minimum_frequency=100, maximum_frequency=800) # doctest: +SKIP
Create a Seismology object and use it to estimate parameters:
>>> seismology = pg.flatten().to_seismology() # doctest: +SKIP
>>> seismology.estimate_numax() # doctest: +SKIP
numax: 415.00 uHz (method: ACF2D)
>>> seismology.estimate_deltanu() # doctest: +SKIP
deltanu: 28.78 uHz (method: ACF2D)
>>> seismology.estimate_radius(teff=5080) # doctest: +SKIP
radius: 2.78 solRad (method: Uncorrected Scaling Relations)
Parameters
----------
periodogram : `~lightkurve.periodogram.Periodogram` object
Periodogram to be analyzed. Must be background-corrected,
e.g. using `periodogram.flatten()`.
"""
periodogram = None
"""The periodogram from which seismological parameters are being extracted."""
def __init__(self, periodogram):
if not isinstance(periodogram, SNRPeriodogram):
warnings.warn(
"Seismology received a periodogram which does not appear "
"to have been background-corrected. Please consider calling "
"`periodogram.flatten()` prior to extracting seismological parameters.",
LightkurveWarning,
)
self.periodogram = periodogram
def __repr__(self):
attrs = np.asarray(["numax", "deltanu", "mass", "radius", "logg"])
tray = np.asarray([hasattr(self, attr) for attr in attrs])
if tray.sum() == 0:
tray_str = " - no values have been computed so far."
else:
tray_str = " - computed values:\n * " + "\n * ".join(
[getattr(self, attr).__repr__() for attr in attrs[tray]]
)
return "Seismology(ID: {}){}".format(self.periodogram.label, tray_str)
@staticmethod
def from_lightcurve(lc, **kwargs):
"""Returns a `Seismology` object given a `LightCurve`."""
log.info(
"Building a Seismology object directly from a light curve "
"uses default periodogram parameters. For further tuneability, "
"create a periodogram object first, using `to_periodogram`."
)
return Seismology(
periodogram=lc.normalize()
.remove_nans()
.fill_gaps()
.to_periodogram(**kwargs)
.flatten()
)
def _validate_numax(self, numax):
"""Raises exception if `numax` is None and `self.numax` is not set."""
if numax is None:
try:
return self.numax
except AttributeError:
raise AttributeError(
"You need to call `Seismology.estimate_numax()` first."
)
return numax
def _validate_deltanu(self, deltanu):
"""Raises exception if `deltanu` is None and `self.deltanu` is not set."""
if deltanu is None:
try:
return self.deltanu
except AttributeError:
raise AttributeError(
"You need to call `Seismology.estimate_deltanu()` first."
)
return deltanu
def _clean_echelle(
self,
deltanu=None,
numax=None,
minimum_frequency=None,
maximum_frequency=None,
smooth_filter_width=0.1,
scale="linear",
):
"""Takes input seismology object and creates the necessary arrays for an echelle
diagram. Validates all the inputs.
Parameters
----------
deltanu : float
Value for the large frequency separation of the seismic mode
frequencies in the periodogram. Assumed to have the same units as
the frequencies, unless given an Astropy unit.
Is assumed to be in the same units as frequency if not given a unit.
numax : float
Value for the frequency of maximum oscillation. If a numax is
passed, a suitable range one FWHM of the mode envelope either side
of the will be shown. This is overwritten by custom frequency ranges.
Is assumed to be in the same units as frequency if not given a unit.
minimum_frequency : float
The minimum frequency at which to display the echelle
Is assumed to be in the same units as frequency if not given a unit.
maximum_frequency : float
The maximum frequency at which to display the echelle.
Is assumed to be in the same units as frequency if not given a unit.
smooth_filter_width : float
If given a value, will smooth periodogram used to plot the echelle
diagram using the periodogram.smooth(method='boxkernel') method with
a filter width of `smooth_filter_width`. This helps visualise the
echelle diagram. Is assumed to be in the same units as the
periodogram frequency.
ax : `~matplotlib.axes.Axes`
A matplotlib axes object to plot into. If no axes is provided,
a new one will be created.
scale: str
Set z axis to be "linear" or "log". Default is linear.
Returns
-------
ep : np.ndarray
Echelle diagram power
x_f : np.ndarray
frequencies for X axis
y_f : np.ndarray
frequencies for Y axis
"""
if (minimum_frequency is None) & (maximum_frequency is None):
numax = self._validate_numax(numax)
deltanu = self._validate_deltanu(deltanu)
if (not hasattr(numax, "unit")) & (numax is not None):
numax = numax * self.periodogram.frequency.unit
if (not hasattr(deltanu, "unit")) & (deltanu is not None):
deltanu = deltanu * self.periodogram.frequency.unit
if smooth_filter_width:
pgsmooth = self.periodogram.smooth(filter_width=smooth_filter_width)
freq = pgsmooth.frequency # Makes code below more readable below
power = pgsmooth.power # Makes code below more readable below
else:
freq = self.periodogram.frequency # Makes code below more readable
power = self.periodogram.power # Makes code below more readable
fmin = freq[0]
fmax = freq[-1]
# Check for any superfluous input
if (numax is not None) & (
any([a is not None for a in [minimum_frequency, maximum_frequency]])
):
warnings.warn(
"You have passed both a numax and a frequency limit. "
"The frequency limit will override the numax input.",
LightkurveWarning,
)
# Ensure input numax is in the correct units (if there is one)
if numax is not None:
numax = u.Quantity(numax, freq.unit).value
if numax > freq[-1].value:
raise ValueError(
"You can't pass in a numax outside the"
"frequency range of the periodogram."
)
fwhm = utils.get_fwhm(self.periodogram, numax)
fmin = numax - 2 * fwhm
if fmin < freq[0].value:
fmin = freq[0].value
fmax = numax + 2 * fwhm
if fmax > freq[-1].value:
fmax = freq[-1].value
# Set limits and set them in the right units
if minimum_frequency is not None:
fmin = u.Quantity(minimum_frequency, freq.unit).value
if fmin > freq[-1].value:
raise ValueError(
"You can't pass in a limit outside the "
"frequency range of the periodogram."
)
if maximum_frequency is not None:
fmax = u.Quantity(maximum_frequency, freq.unit).value
if fmax > freq[-1].value:
raise ValueError(
"You can't pass in a limit outside the "
"frequency range of the periodogram."
)
# Make sure fmin and fmax are Quantities or code below will break
fmin = u.Quantity(fmin, freq.unit)
fmax = u.Quantity(fmax, freq.unit)
# Add on 1x deltanu so we don't miss off any important range due to rounding
if fmax < freq[-1] - 1.5 * deltanu:
fmax += deltanu
fs = np.median(np.diff(freq))
x0 = int(freq[0] / fs)
ff = freq[int(fmin / fs) - x0 : int(fmax / fs) - x0] # Selected frequency range
pp = power[int(fmin / fs) - x0 : int(fmax / fs) - x0] # Power range
# Reshape the power into n_rows of n_columns
# When modulus ~ zero, deltanu divides into frequency without remainder
mod_zeros = find_peaks(-1.0 * (ff % deltanu))[0]
# The bottom left corner of the plot is the lowest frequency that
# divides into deltanu with almost zero remainder
start = mod_zeros[0]
# The top left corner of the plot is the highest frequency that
# divides into deltanu with almost zero remainder. This index is the
# approximate end, because we fix an integer number of rows and columns
approx_end = mod_zeros[-1]
# The number of rows is the number of times you can partition your
# frequency range into chunks of size deltanu, start and ending at
# frequencies that divide nearly evenly into deltanu
n_rows = len(mod_zeros) - 1
# The number of columns is the total number of frequency points divided
# by the number of rows, floor divided to the nearest integer value
n_columns = int((approx_end - start) / n_rows)
# The exact end point is therefore the ncolumns*nrows away from the start
end = start + n_columns * n_rows
ep = np.reshape(pp[start:end], (n_rows, n_columns))
if scale == "log":
ep = np.log10(ep)
# Reshape the freq into n_rowss of n_columnss & create arays
ef = np.reshape(ff[start:end], (n_rows, n_columns))
x_f = (ef[0, :] - ef[0, 0]) % deltanu
y_f = ef[:, 0]
return ep, x_f, y_f
def plot_echelle(
self,
deltanu=None,
numax=None,
minimum_frequency=None,
maximum_frequency=None,
smooth_filter_width=0.1,
scale="linear",
ax=None,
cmap="Blues",
):
"""Plots an echelle diagram of the periodogram by stacking the
periodogram in slices of deltanu.
Modes of equal radial degree should appear approximately vertically aligned.
If no structure is present, you are likely dealing with a faulty deltanu
value or a low signal to noise case.
This method is adapted from work by Daniel Hey & Guy Davies.
Parameters
----------
deltanu : float
Value for the large frequency separation of the seismic mode
frequencies in the periodogram. Assumed to have the same units as
the frequencies, unless given an Astropy unit.
Is assumed to be in the same units as frequency if not given a unit.
numax : float
Value for the frequency of maximum oscillation. If a numax is
passed, a suitable range one FWHM of the mode envelope either side
of the will be shown. This is overwritten by custom frequency ranges.
Is assumed to be in the same units as frequency if not given a unit.
minimum_frequency : float
The minimum frequency at which to display the echelle
Is assumed to be in the same units as frequency if not given a unit.
maximum_frequency : float
The maximum frequency at which to display the echelle.
Is assumed to be in the same units as frequency if not given a unit.
smooth_filter_width : float
If given a value, will smooth periodogram used to plot the echelle
diagram using the periodogram.smooth(method='boxkernel') method with
a filter width of `smooth_filter_width`. This helps visualise the
echelle diagram. Is assumed to be in the same units as the
periodogram frequency.
scale: str
Set z axis to be "linear" or "log". Default is linear.
cmap : str
The name of the matplotlib colourmap to use in the echelle diagram.
Returns
-------
ax : `~matplotlib.axes.Axes`
The matplotlib axes object.
"""
if (minimum_frequency is None) & (maximum_frequency is None):
numax = self._validate_numax(numax)
deltanu = self._validate_deltanu(deltanu)
if (not hasattr(numax, "unit")) & (numax is not None):
numax = numax * self.periodogram.frequency.unit
if (not hasattr(deltanu, "unit")) & (deltanu is not None):
deltanu = deltanu * self.periodogram.frequency.unit
ep, x_f, y_f = self._clean_echelle(
numax=numax,
deltanu=deltanu,
minimum_frequency=minimum_frequency,
maximum_frequency=maximum_frequency,
smooth_filter_width=smooth_filter_width,
)
# Plot the echelle diagram
with plt.style.context(MPLSTYLE):
if ax is None:
_, ax = plt.subplots()
extent = (x_f[0].value, x_f[-1].value, y_f[0].value, y_f[-1].value)
figsize = plt.rcParams["figure.figsize"]
a = figsize[1] / figsize[0]
b = (extent[3] - extent[2]) / (extent[1] - extent[0])
vmin = np.nanpercentile(ep.value, 1)
vmax = np.nanpercentile(ep.value, 99)
im = ax.imshow(
ep.value,
cmap=cmap,
aspect=a / b,
origin="lower",
extent=extent,
vmin=vmin,
vmax=vmax,
)
cbar = plt.colorbar(im, ax=ax, extend="both", pad=0.01)
if isinstance(self.periodogram, SNRPeriodogram):
ylabel = "Signal to Noise Ratio (SNR)"
elif self.periodogram.power.unit == cds.ppm:
ylabel = "Amplitude [{}]".format(
self.periodogram.power.unit.to_string("latex")
)
else:
ylabel = "Power Spectral Density [{}]".format(
self.periodogram.power.unit.to_string("latex")
)
if scale == "log":
ylabel = "log10(" + ylabel + ")"
cbar.set_label(ylabel)
ax.set_xlabel(r"Frequency mod. {:.2f}".format(deltanu))
ax.set_ylabel(
r"Frequency [{}]".format(
self.periodogram.frequency.unit.to_string("latex")
)
)
ax.set_title("Echelle diagram for {}".format(self.periodogram.label))
return ax
def _make_echelle_elements(
self,
deltanu,
cmap="viridis",
minimum_frequency=None,
maximum_frequency=None,
smooth_filter_width=0.1,
scale="linear",
width=490,
height=340,
title="Echelle",
):
"""Helper function to make the elements of the echelle diagram for bokeh plotting."""
if not hasattr(deltanu, "unit"):
deltanu = deltanu * self.periodogram.frequency.unit
if smooth_filter_width:
pgsmooth = self.periodogram.smooth(filter_width=smooth_filter_width)
freq = pgsmooth.frequency # Makes code below more readable below
else:
freq = self.periodogram.frequency # Makes code below more readable
ep, x_f, y_f = self._clean_echelle(
deltanu=deltanu,
minimum_frequency=minimum_frequency,
maximum_frequency=maximum_frequency,
smooth_filter_width=smooth_filter_width,
scale=scale,
)
fig = figure(
width=width,
height=height,
x_range=(0, 1),
y_range=(y_f[0].value, y_f[-1].value),
title=title,
tools="pan,box_zoom,reset",
toolbar_location="above",
border_fill_color="white",
)
fig.yaxis.axis_label = r"Frequency [{}]".format(freq.unit.to_string())
fig.xaxis.axis_label = r"Frequency / {:.3f} Mod. 1".format(deltanu)
lo, hi = np.nanpercentile(ep.value, [0.1, 99.9])
vlo, vhi = 0.3 * lo, 1.7 * hi
vstep = (lo - hi) / 500
color_mapper = LogColorMapper(palette="RdYlGn10", low=lo, high=hi)
fig.image(
image=[ep.value],
x=0,
y=y_f[0].value,
dw=1,
dh=y_f[-1].value,
color_mapper=color_mapper,
name="img",
)
stretch_slider = RangeSlider(
start=vlo,
end=vhi,
step=vstep,
title="",
value=(lo, hi),
orientation="vertical",
width=10,
height=230,
direction="rtl",
show_value=False,
sizing_mode="fixed",
name="stretch",
)
def stretch_change_callback(attr, old, new):
"""TPF stretch slider callback."""
fig.select("img")[0].glyph.color_mapper.high = new[1]
fig.select("img")[0].glyph.color_mapper.low = new[0]
stretch_slider.on_change("value", stretch_change_callback)
return fig, stretch_slider
def interact_echelle(self, notebook_url="localhost:8888", **kwargs):
"""Display an interactive Jupyter notebook widget showing an Echelle diagram.
This feature only works inside an active Jupyter Notebook, and
requires an optional dependency, ``bokeh`` (v1.0 or later).
This dependency can be installed using e.g. `conda install bokeh`.
Parameters
----------
notebook_url : str
Location of the Jupyter notebook page (default: "localhost:8888")
When showing Bokeh applications, the Bokeh server must be
explicitly configured to allow connections originating from
different URLs. This parameter defaults to the standard notebook
host and port. If you are running on a different location, you
will need to supply this value for the application to display
properly. If no protocol is supplied in the URL, e.g. if it is
of the form "localhost:8888", then "http" will be used.
"""
try:
import bokeh
if bokeh.__version__[0] == "0":
warnings.warn(
"interact() requires Bokeh version 1.0 or later", LightkurveWarning
)
except ImportError:
log.error(
"The interact() tool requires the `bokeh` Python package; "
"you can install bokeh using e.g. `conda install bokeh`."
)
return None
maximum_frequency = kwargs.pop(
"maximum_frequency", self.periodogram.frequency.max().value
)
minimum_frequency = kwargs.pop(
"minimum_frequency", self.periodogram.frequency.min().value
)
if not hasattr(self, "deltanu"):
dnu = SeismologyQuantity(
quantity=self.periodogram.frequency.max() / 30,
name="deltanu",
method="echelle",
)
else:
dnu = self.deltanu
def create_interact_ui(doc):
fig_tpf, stretch_slider = self._make_echelle_elements(
dnu,
maximum_frequency=maximum_frequency,
minimum_frequency=minimum_frequency,
**kwargs
)
maxdnu = self.periodogram.frequency.max().value / 5
# Interactive slider widgets
dnu_slider = Slider(
start=0.01,
end=maxdnu,
value=dnu.value,
step=0.01,
title="Delta Nu",
width=290,
)
r_button = Button(label=">", button_type="default", width=30)
l_button = Button(label="<", button_type="default", width=30)
rr_button = Button(label=">>", button_type="default", width=30)
ll_button = Button(label="<<", button_type="default", width=30)
def update(attr, old, new):
"""Callback to take action when dnu slider changes"""
dnu = SeismologyQuantity(
quantity=dnu_slider.value * u.microhertz,
name="deltanu",
method="echelle",
)
ep, _, _ = self._clean_echelle(
deltanu=dnu,
minimum_frequency=minimum_frequency,
maximum_frequency=maximum_frequency,
**kwargs
)
fig_tpf.select("img")[0].data_source.data["image"] = [ep.value]
fig_tpf.xaxis.axis_label = r"Frequency / {:.3f} Mod. 1".format(dnu)
def go_right_by_one_small():
"""Step forward in time by a single cadence"""
existing_value = dnu_slider.value
if existing_value < maxdnu:
dnu_slider.value = existing_value + 0.002
def go_left_by_one_small():
"""Step back in time by a single cadence"""
existing_value = dnu_slider.value
if existing_value > 0:
dnu_slider.value = existing_value - 0.002
def go_right_by_one():
"""Step forward in time by a single cadence"""
existing_value = dnu_slider.value
if existing_value < maxdnu:
dnu_slider.value = existing_value + 0.01
def go_left_by_one():
"""Step back in time by a single cadence"""
existing_value = dnu_slider.value
if existing_value > 0:
dnu_slider.value = existing_value - 0.01
dnu_slider.on_change("value", update)
r_button.on_click(go_right_by_one_small)
l_button.on_click(go_left_by_one_small)
rr_button.on_click(go_right_by_one)
ll_button.on_click(go_left_by_one)
widgets_and_figures = layout(
[fig_tpf, [Spacer(height=20), stretch_slider]],
[
ll_button,
Spacer(width=30),
l_button,
Spacer(width=25),
dnu_slider,
Spacer(width=30),
r_button,
Spacer(width=23),
rr_button,
],
)
doc.add_root(widgets_and_figures)
output_notebook(verbose=False, hide_banner=True)
return show(create_interact_ui, notebook_url=notebook_url)
def estimate_numax(self, method="acf2d", **kwargs):
"""Returns the frequency of the peak of the seismic oscillation modes envelope.
At present, the only method supported is based on using a
2D autocorrelation function (ACF2D). This method is implemented by the
`~lightkurve.seismology.estimate_numax_acf2d` function which accepts
the parameters `numaxs`, `window_width`, and `spacing`.
For details and literature references, please read the detailed
docstring of this function by typing ``lightkurve.seismology.estimate_numax_acf2d?``
in a Python terminal or notebook.
Parameters
----------
method : str
Method to use. Only ``"acf2d"`` is supported at this time.
Returns
-------
numax : `~lightkurve.seismology.SeismologyQuantity`
Numax of the periodogram, including details on the units and method.
"""
method = validate_method(method, supported_methods=["acf2d"])
if method == "acf2d":
from .numax_estimators import estimate_numax_acf2d
result = estimate_numax_acf2d(self.periodogram, **kwargs)
self.numax = result
return result
def diagnose_numax(self, numax=None):
"""Create diagnostic plots showing how numax was estimated."""
numax = self._validate_numax(numax)
return numax.diagnostics_plot_method(numax, self.periodogram)
def estimate_deltanu(self, method="acf2d", numax=None):
"""Returns the average value of the large frequency spacing, DeltaNu,
of the seismic oscillations of the target.
At present, the only method supported is based on using an
autocorrelation function (ACF2D). This method is implemented by the
`~lightkurve.seismology.estimate_deltanu_acf2d` function which requires
the parameter `numax`. For details and literature references, please
read the detailed docstring of this function by typing
``lightkurve.seismology.estimate_deltanu_acf2d?`` in a Python terminal or notebook.
Parameters
----------
method : str
Method to use. Only ``"acf2d"`` is supported at this time.
Returns
-------
deltanu : `~lightkurve.seismology.SeismologyQuantity`
DeltaNu of the periodogram, including details on the units and method.
"""
method = validate_method(method, supported_methods=["acf2d"])
numax = self._validate_numax(numax)
if method == "acf2d":
from .deltanu_estimators import estimate_deltanu_acf2d
result = estimate_deltanu_acf2d(self.periodogram, numax=numax)
self.deltanu = result
return result
def diagnose_deltanu(self, deltanu=None):
"""Create diagnostic plots showing how numax was estimated."""
deltanu = self._validate_deltanu(deltanu)
return deltanu.diagnostics_plot_method(deltanu, self.periodogram)
def estimate_radius(self, teff=None, numax=None, deltanu=None):
"""Returns a stellar radius estimate based on the scaling relations.
The two global observable seismic parameters, numax and deltanu, along with
temperature, scale with fundamental stellar properties (Brown et al. 1991;
Kjeldsen & Bedding 1995). These scaling relations can be rearranged to
calculate a stellar radius as
R = Rsol * (numax/numax_sol)(deltanu/deltanusol)^-2(Teff/Teffsol)^0.5
where R is the radius and Teff is the effective temperature, and the suffix
'sol' indicates a solar value. In this method we use the solar values for
numax and deltanu as given in Huber et al. (2011) and for Teff as given in
Prsa et al. (2016).
This code structure borrows from work done in Bellinger et al. (2019), which
also functions as an accessible explanation of seismic scaling relations.
If no value of effective temperature is given, this function will check the
meta data of the `Periodogram` object used to create the `Seismology` object.
These data will often contain an effective tempearture from the Kepler Input
Catalogue (KIC, https://ui.adsabs.harvard.edu/abs/2011AJ....142..112B/abstract),
or from the EPIC or TIC for K2 and TESS respectively. The temperature values in these
catalogues are estimated using photometry, and so have large associated uncertainties
(roughly 200 K, see KIC). For more better results, spectroscopic measurements of
temperature are often more precise.
NOTE: These scaling relations are scaled to the Sun, and therefore do not
always produce an entirely accurate result for more evolved stars.
Parameters
----------
numax : float
The frequency of maximum power of the seismic mode envelope. If not
given an astropy unit, assumed to be in units of microhertz.
deltanu : float
The frequency spacing between two consecutive overtones of equal radial
degree. If not given an astropy unit, assumed to be in units of
microhertz.
teff : float
The effective temperature of the star. In units of Kelvin.
numax_err : float
Error on numax. Assumed to be same units as numax
deltanu_err : float
Error on deltanu. Assumed to be same units as deltanu
teff_err : float
Error on Teff. Assumed to be same units as Teff.
Returns
-------
radius : `~lightkurve.seismology.SeismologyQuantity`
Stellar radius estimate.
"""
numax = self._validate_numax(numax)
deltanu = self._validate_deltanu(deltanu)
if teff is None:
teff = self.periodogram.meta.get("TEFF")
if teff is None:
raise ValueError(
"You must provide an effective temperature argument (`teff`) to `estimate_radius`,"
"because the Periodogram object does not contain it in its meta data (i.e. `pg.meta['TEFF']` is missing"
)
else:
log.info(
"Using value for effective temperature from the Kepler Input Catalogue."
"These temperatue values may sometimes differ significantly from modern estimates."
)
pass
else:
pass
result = stellar_estimators.estimate_radius(numax, deltanu, teff)
self.radius = result
return result
def estimate_mass(self, teff=None, numax=None, deltanu=None):
"""Calculates mass using the asteroseismic scaling relations.
The two global observable seismic parameters, numax and deltanu, along with
temperature, scale with fundamental stellar properties (Brown et al. 1991;
Kjeldsen & Bedding 1995). These scaling relations can be rearranged to
calculate a stellar mass as
M = Msol * (numax/numax_sol)^3(deltanu/deltanusol)^-4(Teff/Teffsol)^1.5
where M is the mass and Teff is the effective temperature, and the suffix
'sol' indicates a solar value. In this method we use the solar values for
numax and deltanu as given in Huber et al. (2011) and for Teff as given in
Prsa et al. (2016).
This code structure borrows from work done in Bellinger et al. (2019), which
also functions as an accessible explanation of seismic scaling relations.
If no value of effective temperature is given, this function will check the
meta data of the `Periodogram` object used to create the `Seismology` object.
These data will often contain an effective tempearture from the Kepler Input
Catalogue (KIC, https://ui.adsabs.harvard.edu/abs/2011AJ....142..112B/abstract),
or from the EPIC or TIC for K2 and TESS respectively. The temperature values in these
catalogues are estimated using photometry, and so have large associated uncertainties
(roughly 200 K, see KIC). For more better results, spectroscopic measurements of
temperature are often more precise.
NOTE: These scaling relations are scaled to the Sun, and therefore do not
always produce an entirely accurate result for more evolved stars.
Parameters
----------
numax : float
The frequency of maximum power of the seismic mode envelope. If not
given an astropy unit, assumed to be in units of microhertz.
deltanu : float
The frequency spacing between two consecutive overtones of equal radial
degree. If not given an astropy unit, assumed to be in units of
microhertz.
teff : float
The effective temperature of the star. In units of Kelvin.
numax_err : float
Error on numax. Assumed to be same units as numax
deltanu_err : float
Error on deltanu. Assumed to be same units as deltanu
teff_err : float
Error on Teff. Assumed to be same units as Teff.
Returns
-------
mass : `~lightkurve.seismology.SeismologyQuantity`
Stellar mass estimate.
"""
numax = self._validate_numax(numax)
deltanu = self._validate_deltanu(deltanu)
if teff is None:
teff = self.periodogram.meta.get("TEFF")
if teff is None:
raise ValueError(
"You must provide an effective temperature argument (`teff`) to `estimate_radius`,"
"because the Periodogram object does not contain it in its meta data (i.e. `pg.meta['TEFF']` is missing"
)
else:
log.info(
"Using value for effective temperature from the Kepler Input Catalogue."
"These temperatue values may sometimes differ significantly from modern estimates."
)
pass
else:
pass
result = stellar_estimators.estimate_mass(numax, deltanu, teff)
self.mass = result
return result
def estimate_logg(self, teff=None, numax=None):
"""Calculates the log of the surface gravity using the asteroseismic scaling
relations.
The two global observable seismic parameters, numax and deltanu, along with
temperature, scale with fundamental stellar properties (Brown et al. 1991;
Kjeldsen & Bedding 1995). These scaling relations can be rearranged to
calculate a stellar surface gravity as
g = gsol * (numax/numax_sol)(Teff/Teffsol)^0.5
where g is the surface gravity and Teff is the effective temperature,
and the suffix 'sol' indicates a solar value. In this method we use the
solar values for numax as given in Huber et al. (2011) and for Teff as given
in Prsa et al. (2016). The solar surface gravity is calcluated from the
astropy constants for solar mass and radius and does not have an error.
The solar surface gravity is returned as log10(g) with units in dex, as is
common in the astrophysics literature.
This code structure borrows from work done in Bellinger et al. (2019), which
also functions as an accessible explanation of seismic scaling relations.
If no value of effective temperature is given, this function will check the
meta data of the `Periodogram` object used to create the `Seismology` object.
These data will often contain an effective tempearture from the Kepler Input
Catalogue (KIC, https://ui.adsabs.harvard.edu/abs/2011AJ....142..112B/abstract),
or from the EPIC or TIC for K2 and TESS respectively. The temperature values in these
catalogues are estimated using photometry, and so have large associated uncertainties
(roughly 200 K, see KIC). For more better results, spectroscopic measurements of
temperature are often more precise.
NOTE: These scaling relations are scaled to the Sun, and therefore do not
always produce an entirely accurate result for more evolved stars.
Parameters
----------
numax : float
The frequency of maximum power of the seismic mode envelope. If not
given an astropy unit, assumed to be in units of microhertz.
teff : float
The effective temperature of the star. In units of Kelvin.
numax_err : float
Error on numax. Assumed to be same units as numax
teff_err : float
Error on teff. Assumed to be same units as teff.
Returns
-------
logg : `~lightkurve.seismology.SeismologyQuantity`
Stellar surface gravity estimate.
"""
numax = self._validate_numax(numax)
if teff is None:
teff = self.periodogram.meta.get("TEFF")
if teff is None:
raise ValueError(
"You must provide an effective temperature argument (`teff`) to `estimate_radius`,"
"because the Periodogram object does not contain it in its meta data (i.e. `pg.meta['TEFF']` is missing"
)
else:
log.info(
"Using value for effective temperature from the Kepler Input Catalogue."
"These temperatue values may sometimes differ significantly from modern estimates."
)
pass
else:
pass
result = stellar_estimators.estimate_logg(numax, teff)
self.logg = result
return result