arvi 0.2.8__py3-none-any.whl → 0.2.10__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of arvi might be problematic. Click here for more details.
- arvi/dace_wrapper.py +7 -5
- arvi/instrument_specific.py +23 -9
- arvi/kepmodel_wrapper.py +296 -0
- arvi/nasaexo_wrapper.py +7 -3
- arvi/plots.py +1 -3
- arvi/reports.py +108 -1
- arvi/stats.py +30 -5
- arvi/timeseries.py +312 -119
- arvi/utils.py +86 -8
- {arvi-0.2.8.dist-info → arvi-0.2.10.dist-info}/METADATA +1 -1
- {arvi-0.2.8.dist-info → arvi-0.2.10.dist-info}/RECORD +14 -13
- {arvi-0.2.8.dist-info → arvi-0.2.10.dist-info}/WHEEL +0 -0
- {arvi-0.2.8.dist-info → arvi-0.2.10.dist-info}/licenses/LICENSE +0 -0
- {arvi-0.2.8.dist-info → arvi-0.2.10.dist-info}/top_level.txt +0 -0
arvi/dace_wrapper.py
CHANGED
|
@@ -10,7 +10,7 @@ from .setup_logger import setup_logger
|
|
|
10
10
|
from .utils import create_directory, all_logging_disabled, stdout_disabled, timer, tqdm
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
def load_spectroscopy(user=None):
|
|
13
|
+
def load_spectroscopy(user=None, verbose=True):
|
|
14
14
|
logger = setup_logger()
|
|
15
15
|
with all_logging_disabled():
|
|
16
16
|
from dace_query.spectroscopy import SpectroscopyClass, Spectroscopy as default_Spectroscopy
|
|
@@ -19,7 +19,8 @@ def load_spectroscopy(user=None):
|
|
|
19
19
|
from .config import config
|
|
20
20
|
# requesting as public
|
|
21
21
|
if config.request_as_public:
|
|
22
|
-
|
|
22
|
+
if verbose:
|
|
23
|
+
logger.warning('requesting DACE data as public')
|
|
23
24
|
with all_logging_disabled():
|
|
24
25
|
dace = DaceClass(dace_rc_config_path='none')
|
|
25
26
|
return SpectroscopyClass(dace_instance=dace)
|
|
@@ -168,7 +169,7 @@ def get_observations_from_instrument(star, instrument, user=None, main_id=None,
|
|
|
168
169
|
dict:
|
|
169
170
|
dictionary with data from DACE
|
|
170
171
|
"""
|
|
171
|
-
Spectroscopy = load_spectroscopy(user)
|
|
172
|
+
Spectroscopy = load_spectroscopy(user, verbose)
|
|
172
173
|
found_dace_id = False
|
|
173
174
|
with timer('dace_id query'):
|
|
174
175
|
try:
|
|
@@ -192,12 +193,13 @@ def get_observations_from_instrument(star, instrument, user=None, main_id=None,
|
|
|
192
193
|
except TypeError:
|
|
193
194
|
msg = f'no {instrument} observations for {star}'
|
|
194
195
|
raise ValueError(msg) from None
|
|
196
|
+
|
|
195
197
|
if (isinstance(instrument, str)):
|
|
196
198
|
filters = {
|
|
197
199
|
"ins_name": {"contains": [instrument]},
|
|
198
200
|
"obj_id_daceid": {"contains": [dace_id]}
|
|
199
201
|
}
|
|
200
|
-
elif (isinstance(instrument, list)):
|
|
202
|
+
elif (isinstance(instrument, (list, tuple, np.ndarray))):
|
|
201
203
|
filters = {
|
|
202
204
|
"ins_name": {"contains": instrument},
|
|
203
205
|
"obj_id_daceid": {"contains": [dace_id]}
|
|
@@ -283,7 +285,7 @@ def get_observations_from_instrument(star, instrument, user=None, main_id=None,
|
|
|
283
285
|
def get_observations(star, instrument=None, user=None, main_id=None, verbose=True):
|
|
284
286
|
logger = setup_logger()
|
|
285
287
|
if instrument is None:
|
|
286
|
-
Spectroscopy = load_spectroscopy(user)
|
|
288
|
+
Spectroscopy = load_spectroscopy(user, verbose)
|
|
287
289
|
|
|
288
290
|
try:
|
|
289
291
|
with stdout_disabled(), all_logging_disabled():
|
arvi/instrument_specific.py
CHANGED
|
@@ -123,7 +123,10 @@ def HARPS_commissioning(self, mask=True, plot=True):
|
|
|
123
123
|
if check(self, 'HARPS') is None:
|
|
124
124
|
return
|
|
125
125
|
|
|
126
|
-
affected =
|
|
126
|
+
affected = np.logical_and(
|
|
127
|
+
self.instrument_array == 'HARPS03',
|
|
128
|
+
self.time < HARPS_start
|
|
129
|
+
)
|
|
127
130
|
total_affected = affected.sum()
|
|
128
131
|
|
|
129
132
|
if self.verbose:
|
|
@@ -133,7 +136,7 @@ def HARPS_commissioning(self, mask=True, plot=True):
|
|
|
133
136
|
|
|
134
137
|
if mask:
|
|
135
138
|
self.mask[affected] = False
|
|
136
|
-
self._propagate_mask_changes()
|
|
139
|
+
self._propagate_mask_changes(_remove_instrument=False)
|
|
137
140
|
|
|
138
141
|
if plot:
|
|
139
142
|
self.plot(show_masked=True)
|
|
@@ -155,7 +158,14 @@ def HARPS_fiber_commissioning(self, mask=True, plot=True):
|
|
|
155
158
|
if check(self, 'HARPS') is None:
|
|
156
159
|
return
|
|
157
160
|
|
|
158
|
-
affected =
|
|
161
|
+
affected = np.logical_and(
|
|
162
|
+
self.time >= HARPS_technical_intervention_range[0],
|
|
163
|
+
self.time <= HARPS_technical_intervention_range[1]
|
|
164
|
+
)
|
|
165
|
+
affected = np.logical_and(
|
|
166
|
+
affected,
|
|
167
|
+
np.char.find(self.instrument_array, 'HARPS') == 0
|
|
168
|
+
)
|
|
159
169
|
total_affected = affected.sum()
|
|
160
170
|
|
|
161
171
|
if self.verbose:
|
|
@@ -165,7 +175,7 @@ def HARPS_fiber_commissioning(self, mask=True, plot=True):
|
|
|
165
175
|
|
|
166
176
|
if mask:
|
|
167
177
|
self.mask[affected] = False
|
|
168
|
-
self._propagate_mask_changes()
|
|
178
|
+
self._propagate_mask_changes(_remove_instrument=False)
|
|
169
179
|
|
|
170
180
|
if plot:
|
|
171
181
|
self.plot(show_masked=True)
|
|
@@ -187,7 +197,10 @@ def ESPRESSO_commissioning(self, mask=True, plot=True):
|
|
|
187
197
|
if check(self, 'ESPRESSO') is None:
|
|
188
198
|
return
|
|
189
199
|
|
|
190
|
-
affected =
|
|
200
|
+
affected = np.logical_and(
|
|
201
|
+
self.instrument_array == 'ESPRESSO18',
|
|
202
|
+
self.time < ESPRESSO_start
|
|
203
|
+
)
|
|
191
204
|
total_affected = affected.sum()
|
|
192
205
|
|
|
193
206
|
if self.verbose:
|
|
@@ -197,7 +210,7 @@ def ESPRESSO_commissioning(self, mask=True, plot=True):
|
|
|
197
210
|
|
|
198
211
|
if mask:
|
|
199
212
|
self.mask[affected] = False
|
|
200
|
-
self._propagate_mask_changes()
|
|
213
|
+
self._propagate_mask_changes(_remove_instrument=False)
|
|
201
214
|
|
|
202
215
|
if plot and total_affected > 0:
|
|
203
216
|
self.plot(show_masked=True)
|
|
@@ -246,7 +259,7 @@ def ADC_issues(self, mask=True, plot=True, check_headers=False):
|
|
|
246
259
|
|
|
247
260
|
if mask:
|
|
248
261
|
self.mask[intersect] = False
|
|
249
|
-
self._propagate_mask_changes()
|
|
262
|
+
self._propagate_mask_changes(_remove_instrument=False)
|
|
250
263
|
|
|
251
264
|
if plot:
|
|
252
265
|
self.plot(show_masked=True)
|
|
@@ -282,7 +295,7 @@ def blue_cryostat_issues(self, mask=True, plot=True):
|
|
|
282
295
|
|
|
283
296
|
if mask:
|
|
284
297
|
self.mask[intersect] = False
|
|
285
|
-
self._propagate_mask_changes()
|
|
298
|
+
self._propagate_mask_changes(_remove_instrument=False)
|
|
286
299
|
|
|
287
300
|
if plot:
|
|
288
301
|
self.plot(show_masked=True)
|
|
@@ -330,7 +343,7 @@ def qc_scired_issues(self, plot=False, **kwargs):
|
|
|
330
343
|
return
|
|
331
344
|
|
|
332
345
|
self.mask[affected] = False
|
|
333
|
-
self._propagate_mask_changes()
|
|
346
|
+
self._propagate_mask_changes(_remove_instrument=False)
|
|
334
347
|
|
|
335
348
|
if plot:
|
|
336
349
|
self.plot(show_masked=True)
|
|
@@ -364,6 +377,7 @@ class ISSUES:
|
|
|
364
377
|
logger.error('are the data binned? cannot proceed to mask these points...')
|
|
365
378
|
|
|
366
379
|
results = list(filter(lambda x: x is not None, results))
|
|
380
|
+
self._propagate_mask_changes()
|
|
367
381
|
|
|
368
382
|
try:
|
|
369
383
|
return np.logical_or.reduce(results)
|
arvi/kepmodel_wrapper.py
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import io
|
|
2
|
+
from contextlib import redirect_stdout, contextmanager
|
|
3
|
+
from string import ascii_lowercase
|
|
4
|
+
from matplotlib import pyplot as plt
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from kepmodel.rv import RvModel
|
|
8
|
+
from spleaf.cov import merge_series
|
|
9
|
+
from spleaf.term import Error, InstrumentJitter
|
|
10
|
+
|
|
11
|
+
from .setup_logger import setup_logger
|
|
12
|
+
from .utils import timer, adjust_lightness
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class model:
|
|
16
|
+
logger = setup_logger()
|
|
17
|
+
# periodogram settings
|
|
18
|
+
Pmin, Pmax, nfreq = 1.5, 10_000, 100_000
|
|
19
|
+
|
|
20
|
+
@contextmanager
|
|
21
|
+
def ew(self):
|
|
22
|
+
for name in self.model.keplerian:
|
|
23
|
+
self.model.set_keplerian_param(name, param=['P', 'la0', 'K', 'e', 'w'])
|
|
24
|
+
try:
|
|
25
|
+
yield
|
|
26
|
+
finally:
|
|
27
|
+
for name in self.model.keplerian:
|
|
28
|
+
self.model.set_keplerian_param(name, param=['P', 'la0', 'K', 'esinw', 'ecosw'])
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def nu0(self):
|
|
32
|
+
return 2 * np.pi / self.Pmax
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def dnu(self):
|
|
36
|
+
return (2 * np.pi / self.Pmin - self.nu0) / (self.nfreq - 1)
|
|
37
|
+
|
|
38
|
+
def __init__(self, s):
|
|
39
|
+
self.s = s
|
|
40
|
+
self.instruments = s.instruments
|
|
41
|
+
self.Pmax = 2 * np.ptp(s.time)
|
|
42
|
+
ts = self.ts = self.s._mtime_sorter
|
|
43
|
+
|
|
44
|
+
# t, y_ ye, series_index = merge_series(
|
|
45
|
+
# [_s.mtime for _s in s],
|
|
46
|
+
# [_s.mvrad for _s in s],
|
|
47
|
+
# [_s.msvrad for _s in s],
|
|
48
|
+
# )
|
|
49
|
+
|
|
50
|
+
inst_jit = self._get_jitters()
|
|
51
|
+
|
|
52
|
+
self.model = RvModel(self.s.mtime[ts], self.s.mvrad[ts],
|
|
53
|
+
err=Error(self.s.msvrad[ts]), **inst_jit)
|
|
54
|
+
self.np = 0
|
|
55
|
+
self._add_means()
|
|
56
|
+
|
|
57
|
+
def _add_means(self):
|
|
58
|
+
for inst in self.s.instruments:
|
|
59
|
+
# if inst not in self.s.instrument_array[self.s.mask]:
|
|
60
|
+
# continue
|
|
61
|
+
mask = self.s.instrument_array[self.s.mask][self.ts] == inst
|
|
62
|
+
self.model.add_lin(
|
|
63
|
+
derivative=1.0 * mask,
|
|
64
|
+
name=f"offset_{inst}",
|
|
65
|
+
value=getattr(self.s, inst).mvrad.mean(),
|
|
66
|
+
)
|
|
67
|
+
self.model.fit_lin()
|
|
68
|
+
|
|
69
|
+
def _get_jitters(self):
|
|
70
|
+
inst_jit = {}
|
|
71
|
+
for inst in self.s.instruments:
|
|
72
|
+
inst_jit[f'jit_{inst}'] = InstrumentJitter(
|
|
73
|
+
indices=self.s.instrument_array[self.s.mask] == inst,
|
|
74
|
+
sig=self.s.svrad[self.s.mask].min()
|
|
75
|
+
)
|
|
76
|
+
return inst_jit
|
|
77
|
+
|
|
78
|
+
def _set_jitters(self, value=0.0):
|
|
79
|
+
for par in self.model.cov.param:
|
|
80
|
+
if 'jit' in par:
|
|
81
|
+
self.model.set_param(value, f'cov.{par}')
|
|
82
|
+
# self.model.fit()
|
|
83
|
+
|
|
84
|
+
def _equal_coralie_offsets(self):
|
|
85
|
+
if self.s._check_instrument('CORALIE') is None:
|
|
86
|
+
return
|
|
87
|
+
mask = np.char.find(self.s.instrument_array, 'CORALIE') == 0
|
|
88
|
+
mean = self.s.vrad[mask].mean()
|
|
89
|
+
for inst in self.instruments:
|
|
90
|
+
if 'CORALIE' in inst:
|
|
91
|
+
self.model.set_param(mean, f'lin.offset_{inst}')
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def __repr__(self):
|
|
95
|
+
with self.ew():
|
|
96
|
+
with io.StringIO() as buf, redirect_stdout(buf):
|
|
97
|
+
self.model.show_param()
|
|
98
|
+
output = buf.getvalue()
|
|
99
|
+
return output
|
|
100
|
+
|
|
101
|
+
def to_table(self, **kwargs):
|
|
102
|
+
from .utils import pretty_print_table
|
|
103
|
+
lines = repr(self)
|
|
104
|
+
lines = lines.replace(' [deg]', '_[deg]')
|
|
105
|
+
lines = lines.encode().replace(b'\xc2\xb1', b'').decode()
|
|
106
|
+
lines = lines.split('\n')
|
|
107
|
+
lines = [line.split() for line in lines]
|
|
108
|
+
lines = [[col.replace('_[deg]', ' [deg]') for col in line] for line in lines]
|
|
109
|
+
pretty_print_table(lines[:-2], **kwargs)
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def fit_param(self):
|
|
113
|
+
return self.model.fit_param
|
|
114
|
+
|
|
115
|
+
def fit(self):
|
|
116
|
+
# fit offsets
|
|
117
|
+
self.model.fit_param = [f'lin.offset_{inst}' for inst in self.instruments]
|
|
118
|
+
# fit jitters
|
|
119
|
+
self.model.fit_param += [f'cov.{par}' for par in self.model.cov.param]
|
|
120
|
+
# fit keplerian(s)
|
|
121
|
+
self.model.fit_param += [
|
|
122
|
+
f'kep.{k}.{p}'
|
|
123
|
+
for k, v in self.model.keplerian.items()
|
|
124
|
+
for p in v._param
|
|
125
|
+
]
|
|
126
|
+
# if self.np == 0:
|
|
127
|
+
# self._set_jitters(0.1 * np.std(self.model.y - self.model.model()))
|
|
128
|
+
try:
|
|
129
|
+
self.model.fit()
|
|
130
|
+
except Exception as e:
|
|
131
|
+
print(e)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def plot(self, **kwargs):
|
|
135
|
+
fig, ax = self.s.plot(**kwargs)
|
|
136
|
+
tt = self.s._tt()
|
|
137
|
+
time_offset = 50000 if 'remove_50000' in kwargs else 0
|
|
138
|
+
|
|
139
|
+
for i, inst in enumerate(self.s):
|
|
140
|
+
inst_name = inst.instruments[0].replace('-', '_')
|
|
141
|
+
val = self.model.get_param(f'lin.offset_{inst_name}')
|
|
142
|
+
x = np.array([inst.mtime.min(), inst.mtime.max()]) - time_offset
|
|
143
|
+
y = [val, val]
|
|
144
|
+
ax.plot(x, y, ls='--', color=f'C{i}')
|
|
145
|
+
mask = (tt > inst.mtime.min()) & (tt < inst.mtime.max())
|
|
146
|
+
color = adjust_lightness(f'C{i}', 1.2)
|
|
147
|
+
ax.plot(tt[mask] - time_offset,
|
|
148
|
+
val + self.model.keplerian_model(tt)[mask],
|
|
149
|
+
color=color)
|
|
150
|
+
|
|
151
|
+
return fig, ax
|
|
152
|
+
|
|
153
|
+
def plot_phasefolding(self, planets=None, ax=None):
|
|
154
|
+
t = self.model.t
|
|
155
|
+
res = self.model.residuals()
|
|
156
|
+
sig = np.sqrt(self.model.cov.A)
|
|
157
|
+
|
|
158
|
+
Msmooth = np.linspace(0, 360, 1000)
|
|
159
|
+
|
|
160
|
+
if planets is None:
|
|
161
|
+
planets = list(self.model.keplerian.keys())
|
|
162
|
+
|
|
163
|
+
if ax is None:
|
|
164
|
+
fig, axs = plt.subplots(
|
|
165
|
+
1, len(planets), sharex=True, sharey=True, constrained_layout=True,
|
|
166
|
+
squeeze=False
|
|
167
|
+
)
|
|
168
|
+
else:
|
|
169
|
+
axs = np.atleast_1d(ax)
|
|
170
|
+
fig = axs[0].figure
|
|
171
|
+
|
|
172
|
+
for p, ax in zip(planets, axs.flat):
|
|
173
|
+
self.model.set_keplerian_param(p, param=['P', 'la0', 'K', 'e', 'w'])
|
|
174
|
+
kep = self.model.keplerian[p]
|
|
175
|
+
P = self.model.get_param(f'kep.{p}.P')
|
|
176
|
+
M0 = (180 / np.pi * (self.model.get_param(f'kep.{p}.la0') - self.model.get_param(f'kep.{p}.w')))
|
|
177
|
+
M = (M0 + 360 / P * t) % 360
|
|
178
|
+
reskep = res + kep.rv(t)
|
|
179
|
+
tsmooth = (Msmooth - M0) * P / 360
|
|
180
|
+
mod = kep.rv(tsmooth)
|
|
181
|
+
|
|
182
|
+
ax.plot([0, 360], [0, 0], '--', c='gray', lw=1)
|
|
183
|
+
ax.plot(Msmooth, mod, 'k-', lw=3, rasterized=True)
|
|
184
|
+
for inst in self.instruments:
|
|
185
|
+
sel = self.s.instrument_array[self.s.mask] == inst
|
|
186
|
+
ax.errorbar(M[sel], reskep[sel], sig[sel], fmt='.',
|
|
187
|
+
rasterized=True, alpha=0.7)
|
|
188
|
+
|
|
189
|
+
ax.set(ylabel='RV [m/s]', xlabel='Mean anomaly [deg]',
|
|
190
|
+
xticks=np.arange(0, 361, 90))
|
|
191
|
+
ax.minorticks_on()
|
|
192
|
+
self.model.set_keplerian_param(p, param=['P', 'la0', 'K', 'esinw', 'ecosw'])
|
|
193
|
+
|
|
194
|
+
# handles, labels = ax.get_legend_handles_labels()
|
|
195
|
+
# fig.legend(handles, labels, loc='center left', bbox_to_anchor=(0.9, 0.5))
|
|
196
|
+
return fig, axs
|
|
197
|
+
|
|
198
|
+
def add_planet_from_period(self, period):
|
|
199
|
+
self.model.add_keplerian_from_period(period, fit=True)
|
|
200
|
+
self.model.fit()
|
|
201
|
+
self.np += 1
|
|
202
|
+
|
|
203
|
+
def _plot_periodogram(self, P=None, power=None, kmax=None, faplvl=None,
|
|
204
|
+
**kwargs):
|
|
205
|
+
if P is None and power is None:
|
|
206
|
+
with timer('periodogram'):
|
|
207
|
+
nu, power = self.model.periodogram(self.nu0, self.dnu, self.nfreq)
|
|
208
|
+
P = 2 * np.pi / nu
|
|
209
|
+
|
|
210
|
+
if 'ax' in kwargs:
|
|
211
|
+
ax = kwargs.pop('ax')
|
|
212
|
+
fig = ax.figure
|
|
213
|
+
else:
|
|
214
|
+
fig, ax = plt.subplots(1, 1, constrained_layout=True)
|
|
215
|
+
ax.semilogx(P, power, 'k', lw=1, rasterized=True)
|
|
216
|
+
ax.set_ylim(0, 1.2 * power.max())
|
|
217
|
+
ax.set(xlabel='Period [days]', ylabel='Normalized power')
|
|
218
|
+
|
|
219
|
+
if kmax is None:
|
|
220
|
+
kmax = np.argmax(power)
|
|
221
|
+
ax.plot(P[kmax], power[kmax], 'or', ms=4)
|
|
222
|
+
ax.text(P[kmax], power[kmax] * 1.1, f'{P[kmax]:.3f} d',
|
|
223
|
+
ha='right', va='center', color='r')
|
|
224
|
+
|
|
225
|
+
if faplvl is None:
|
|
226
|
+
faplvl = self.model.fap(power[kmax], nu.max())
|
|
227
|
+
ax.text(0.99, 0.95, f'FAP = {faplvl:.2g}', transform=ax.transAxes,
|
|
228
|
+
ha='right', va='top')
|
|
229
|
+
|
|
230
|
+
return fig, ax
|
|
231
|
+
|
|
232
|
+
def add_keplerian_from_periodogram(self, fap_max=0.001, plot=False,
|
|
233
|
+
fit_first=True):
|
|
234
|
+
if fit_first and self.np == 0:
|
|
235
|
+
self.fit()
|
|
236
|
+
|
|
237
|
+
self._equal_coralie_offsets()
|
|
238
|
+
|
|
239
|
+
with timer('periodogram'):
|
|
240
|
+
nu, power = self.model.periodogram(self.nu0, self.dnu, self.nfreq)
|
|
241
|
+
|
|
242
|
+
P = 2 * np.pi / nu
|
|
243
|
+
# Compute FAP
|
|
244
|
+
kmax = np.argmax(power)
|
|
245
|
+
faplvl = self.model.fap(power[kmax], nu.max())
|
|
246
|
+
self.logger.info('highest periodogram peak:')
|
|
247
|
+
self.logger.info(f'P={P[kmax]:.4f} d, power={power[kmax]:.3f}, FAP={faplvl:.2e}')
|
|
248
|
+
if plot:
|
|
249
|
+
self._plot_periodogram(P, power, kmax, faplvl)
|
|
250
|
+
|
|
251
|
+
if faplvl > fap_max:
|
|
252
|
+
print('non-significant peak')
|
|
253
|
+
self.fit()
|
|
254
|
+
return False
|
|
255
|
+
|
|
256
|
+
# add new planet
|
|
257
|
+
letter = ascii_lowercase[1:][self.np]
|
|
258
|
+
self.model.add_keplerian_from_period(P[kmax], name=letter,
|
|
259
|
+
guess_kwargs={'emax': 0.8})
|
|
260
|
+
# self.model.set_keplerian_param(letter, param=['P', 'la0', 'K', 'e', 'w'])
|
|
261
|
+
self.model.set_keplerian_param(letter, param=['P', 'la0', 'K', 'esinw', 'ecosw'])
|
|
262
|
+
self.np += 1
|
|
263
|
+
self.fit()
|
|
264
|
+
|
|
265
|
+
if plot:
|
|
266
|
+
self.plot()
|
|
267
|
+
|
|
268
|
+
return True
|
|
269
|
+
|
|
270
|
+
@property
|
|
271
|
+
def offsets(self):
|
|
272
|
+
names = [f'lin.offset_{inst}' for inst in self.instruments]
|
|
273
|
+
return {
|
|
274
|
+
name.replace('lin.', ''): self.model.get_param(name)
|
|
275
|
+
for name in names
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
@property
|
|
279
|
+
def jitters(self):
|
|
280
|
+
names = [f'cov.{par}' for par in self.model.cov.param]
|
|
281
|
+
return {
|
|
282
|
+
name.replace('cov.', '').replace('.sig', ''): self.model.get_param(name)
|
|
283
|
+
for name in names
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
@property
|
|
287
|
+
def keplerians(self):
|
|
288
|
+
keps = {name: {} for name in self.model.keplerian.keys()}
|
|
289
|
+
for name in keps:
|
|
290
|
+
params = self.model.keplerian[name]._param
|
|
291
|
+
pars = [f'kep.{name}.{p}' for p in params]
|
|
292
|
+
keps[name] = {
|
|
293
|
+
par.replace(f'kep.{name}.', ''): self.model.get_param(par)
|
|
294
|
+
for par in pars
|
|
295
|
+
}
|
|
296
|
+
return keps
|
arvi/nasaexo_wrapper.py
CHANGED
|
@@ -6,7 +6,7 @@ from io import StringIO
|
|
|
6
6
|
import numpy as np
|
|
7
7
|
from astropy.timeseries import LombScargle
|
|
8
8
|
|
|
9
|
-
from .setup_logger import
|
|
9
|
+
from .setup_logger import setup_logger
|
|
10
10
|
from kepmodel.rv import RvModel
|
|
11
11
|
from spleaf.term import Error
|
|
12
12
|
|
|
@@ -32,6 +32,7 @@ def run_query(query):
|
|
|
32
32
|
|
|
33
33
|
class Planets:
|
|
34
34
|
def __init__(self, system):
|
|
35
|
+
logger = setup_logger()
|
|
35
36
|
self.s = system
|
|
36
37
|
self.verbose = system.verbose
|
|
37
38
|
|
|
@@ -163,6 +164,7 @@ class Planets:
|
|
|
163
164
|
self.model.show_param()
|
|
164
165
|
|
|
165
166
|
def fit_all(self, adjust_data=False):
|
|
167
|
+
logger = setup_logger()
|
|
166
168
|
self.model.fit()
|
|
167
169
|
|
|
168
170
|
newP = np.array([self.model.get_param(f'kep.{i}.P') for i in range(self.np)])
|
|
@@ -187,5 +189,7 @@ class Planets:
|
|
|
187
189
|
self.s._build_arrays()
|
|
188
190
|
|
|
189
191
|
def __repr__(self):
|
|
190
|
-
|
|
191
|
-
|
|
192
|
+
P = list(map(float, self.P))
|
|
193
|
+
K = list(map(float, self.K))
|
|
194
|
+
e = list(map(float, self.e))
|
|
195
|
+
return f'{self.star}({self.np} planets, {P=}, {K=}, {e=})'
|
arvi/plots.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from functools import partialmethod, wraps
|
|
2
2
|
from itertools import cycle
|
|
3
3
|
|
|
4
|
+
import matplotlib.pyplot as plt
|
|
4
5
|
import numpy as np
|
|
5
6
|
|
|
6
7
|
from astropy.timeseries import LombScargle
|
|
@@ -9,9 +10,6 @@ from .setup_logger import setup_logger
|
|
|
9
10
|
from .config import config
|
|
10
11
|
from .stats import wmean
|
|
11
12
|
|
|
12
|
-
from .utils import lazy_import
|
|
13
|
-
plt = lazy_import('matplotlib.pyplot')
|
|
14
|
-
|
|
15
13
|
|
|
16
14
|
def plot_settings(func):
|
|
17
15
|
@wraps(func)
|
arvi/reports.py
CHANGED
|
@@ -40,6 +40,7 @@ class REPORTS:
|
|
|
40
40
|
rows.append([self.star] + [''] * len(self.instruments) + [''])
|
|
41
41
|
rows.append([''] + self.instruments + ['full'])
|
|
42
42
|
rows.append(['N'] + list(self.NN.values()) + [self.N])
|
|
43
|
+
rows.append(['T span'] + [np.ptp(s.mtime).round(1) for s in self] + [np.ptp(self.mtime).round(1)])
|
|
43
44
|
rows.append(['RV span'] + [np.ptp(s.mvrad).round(3) for s in self] + [np.ptp(self.mvrad).round(3)])
|
|
44
45
|
rows.append(['RV std'] + [s.mvrad.std().round(3) for s in self] + [self.mvrad.std().round(3)])
|
|
45
46
|
rows.append(['eRV mean'] + [s.msvrad.mean().round(3) for s in self] + [self.msvrad.mean().round(3)])
|
|
@@ -201,4 +202,110 @@ class REPORTS:
|
|
|
201
202
|
pdf.savefig(fig)
|
|
202
203
|
# os.system(f'evince {save} &')
|
|
203
204
|
|
|
204
|
-
return fig
|
|
205
|
+
return fig
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def kepmodel_report(self, fit_keplerians=3, save=None, nasaexo_title=False):
|
|
209
|
+
import matplotlib.pyplot as plt
|
|
210
|
+
import matplotlib.gridspec as gridspec
|
|
211
|
+
from matplotlib.backends.backend_pdf import PdfPages
|
|
212
|
+
logger = setup_logger()
|
|
213
|
+
|
|
214
|
+
def set_align_for_column(table, col, align="left"):
|
|
215
|
+
cells = [key for key in table._cells if key[1] == col]
|
|
216
|
+
for cell in cells:
|
|
217
|
+
table._cells[cell]._loc = align
|
|
218
|
+
table._cells[cell]._text.set_horizontalalignment(align)
|
|
219
|
+
|
|
220
|
+
from .kepmodel_wrapper import model
|
|
221
|
+
m = model(self)
|
|
222
|
+
|
|
223
|
+
while fit_keplerians > 0:
|
|
224
|
+
if m.add_keplerian_from_periodogram():
|
|
225
|
+
fit_keplerians -= 1
|
|
226
|
+
else:
|
|
227
|
+
break
|
|
228
|
+
|
|
229
|
+
m.fit()
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
# size = A4
|
|
233
|
+
size = 8.27, 11.69
|
|
234
|
+
fig = plt.figure(figsize=size, constrained_layout=True)
|
|
235
|
+
gs = gridspec.GridSpec(5, 3, figure=fig, height_ratios=[2, 1, 1, 1, 1])
|
|
236
|
+
|
|
237
|
+
# first row, all columns
|
|
238
|
+
ax1 = plt.subplot(gs[0, :])
|
|
239
|
+
|
|
240
|
+
if nasaexo_title:
|
|
241
|
+
title = str(self.planets).replace('(', '\n').replace(')', '')
|
|
242
|
+
star, planets = title.split('\n')
|
|
243
|
+
planets = planets.replace('planets,', 'known planets\n')
|
|
244
|
+
ax1.set_title(star, loc='left', fontsize=14)
|
|
245
|
+
ax1.set_title(planets, loc='right', fontsize=10)
|
|
246
|
+
else:
|
|
247
|
+
title = f'{self.star}'
|
|
248
|
+
ax1.set_title(title, loc='left', fontsize=14)
|
|
249
|
+
# ax1.set_title(r"\href{http://www.google.com}{link}", color='blue',
|
|
250
|
+
# loc='center')
|
|
251
|
+
|
|
252
|
+
m.plot(ax=ax1, N_in_label=True, tooltips=False, remove_50000=True)
|
|
253
|
+
|
|
254
|
+
ax1.legend().remove()
|
|
255
|
+
legend_ax = plt.subplot(gs[1, -1])
|
|
256
|
+
legend_ax.axis('off')
|
|
257
|
+
leg = plt.legend(*ax1.get_legend_handles_labels(),
|
|
258
|
+
prop={'family': 'monospace'})
|
|
259
|
+
legend_ax.add_artist(leg)
|
|
260
|
+
|
|
261
|
+
ax2 = plt.subplot(gs[1, :-1])
|
|
262
|
+
m._plot_periodogram(ax=ax2)
|
|
263
|
+
|
|
264
|
+
ax3 = plt.subplot(gs[2, 0])
|
|
265
|
+
ax3.axis('off')
|
|
266
|
+
items = list(m.offsets.items())
|
|
267
|
+
items = [[item[0].replace('offset_', 'offet '), item[1].round(3)] for item in items]
|
|
268
|
+
table = ax3.table(items, loc='center', edges='open')
|
|
269
|
+
table.auto_set_font_size(False)
|
|
270
|
+
table.set_fontsize(9)
|
|
271
|
+
set_align_for_column(table, 1, align="left")
|
|
272
|
+
|
|
273
|
+
ax4 = plt.subplot(gs[2, 1])
|
|
274
|
+
ax4.axis('off')
|
|
275
|
+
items = list(m.jitters.items())
|
|
276
|
+
items = [[item[0].replace('jit_', 'jitter '), item[1].round(3)] for item in items]
|
|
277
|
+
table = ax4.table(items, loc='center', edges='open')
|
|
278
|
+
table.auto_set_font_size(False)
|
|
279
|
+
table.set_fontsize(9)
|
|
280
|
+
set_align_for_column(table, 1, align="left")
|
|
281
|
+
|
|
282
|
+
ax5 = plt.subplot(gs[2, 2])
|
|
283
|
+
ax5.axis('off')
|
|
284
|
+
items = [
|
|
285
|
+
['N', m.model.n],
|
|
286
|
+
[r'N$_{\rm free}$', len(m.model.fit_param)],
|
|
287
|
+
[r'$\chi^2$', round(m.model.chi2(), 2)],
|
|
288
|
+
[r'$\chi^2_r$', round(m.model.chi2() / (m.model.n - len(m.model.fit_param)), 2)],
|
|
289
|
+
[r'$\log L$', round(m.model.loglike(), 2)],
|
|
290
|
+
]
|
|
291
|
+
table = ax5.table(items, loc='center', edges='open')
|
|
292
|
+
table.auto_set_font_size(False)
|
|
293
|
+
table.set_fontsize(9)
|
|
294
|
+
set_align_for_column(table, 1, align="left")
|
|
295
|
+
|
|
296
|
+
for i, name in enumerate(m.keplerians):
|
|
297
|
+
ax = plt.subplot(gs[3, i])
|
|
298
|
+
m.plot_phasefolding(planets=name, ax=ax)
|
|
299
|
+
|
|
300
|
+
ax = plt.subplot(gs[4, i])
|
|
301
|
+
ax.axis('off')
|
|
302
|
+
with m.ew():
|
|
303
|
+
items = list(m.keplerians[name].items())
|
|
304
|
+
items = [[item[0], item[1].round(3)] for item in items]
|
|
305
|
+
table = ax.table(items, loc='center', edges='open')
|
|
306
|
+
table.auto_set_font_size(False)
|
|
307
|
+
table.set_fontsize(9)
|
|
308
|
+
set_align_for_column(table, 1, align="left")
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
return fig, m
|
arvi/stats.py
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
from functools import partial
|
|
1
2
|
import numpy as np
|
|
3
|
+
from scipy.stats import norm
|
|
2
4
|
|
|
3
5
|
def wmean(a, e):
|
|
4
6
|
"""Weighted mean of array `a`, with uncertainties given by `e`.
|
|
@@ -50,16 +52,39 @@ def wrms(a, e, ignore_nans=False):
|
|
|
50
52
|
w = 1 / e**2
|
|
51
53
|
return np.sqrt(np.sum(w * (a - np.average(a, weights=w))**2) / sum(w))
|
|
52
54
|
|
|
55
|
+
# from https://stackoverflow.com/questions/20601872/numpy-or-scipy-to-calculate-weighted-median
|
|
56
|
+
def weighted_quantiles_interpolate(values, weights, quantiles):
|
|
57
|
+
i = np.argsort(values)
|
|
58
|
+
c = np.cumsum(weights[i])
|
|
59
|
+
q = np.searchsorted(c, quantiles * c[-1])
|
|
60
|
+
# Ensure right-end isn't out of bounds. Thanks @Jeromino!
|
|
61
|
+
q_plus1 = np.clip(q + 1, a_min=None, a_max=values.shape[0] - 1)
|
|
62
|
+
return np.where(
|
|
63
|
+
c[q] / c[-1] == quantiles,
|
|
64
|
+
0.5 * (values[i[q]] + values[i[q_plus1]]),
|
|
65
|
+
values[i[q]],
|
|
66
|
+
)
|
|
53
67
|
|
|
54
|
-
|
|
68
|
+
weighted_median = partial(weighted_quantiles_interpolate, quantiles=0.5)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def sigmaclip_median(a, low=4.0, high=4.0, k=1/norm.ppf(3/4)):
|
|
55
73
|
"""
|
|
56
74
|
Same as scipy.stats.sigmaclip but using the median and median absolute
|
|
57
75
|
deviation instead of the mean and standard deviation.
|
|
58
76
|
|
|
59
77
|
Args:
|
|
60
|
-
a (array):
|
|
61
|
-
|
|
62
|
-
|
|
78
|
+
a (array):
|
|
79
|
+
Array containing data
|
|
80
|
+
low (float):
|
|
81
|
+
Number of MAD to use for the lower clipping limit
|
|
82
|
+
high (float):
|
|
83
|
+
Number of MAD to use for the upper clipping limit
|
|
84
|
+
k (float):
|
|
85
|
+
Scale factor for the MAD to be an estimator of the standard
|
|
86
|
+
deviation. Depends on the (assumed) distribution of the data.
|
|
87
|
+
Default value is for the normal distribution (=1/norm.ppf(3/4)).
|
|
63
88
|
Returns:
|
|
64
89
|
SigmaclipResult: Object with the following attributes:
|
|
65
90
|
- `clipped`: Masked array of data
|
|
@@ -71,7 +96,7 @@ def sigmaclip_median(a, low=4.0, high=4.0):
|
|
|
71
96
|
c = np.asarray(a).ravel()
|
|
72
97
|
delta = 1
|
|
73
98
|
while delta:
|
|
74
|
-
c_mad = median_abs_deviation(c)
|
|
99
|
+
c_mad = median_abs_deviation(c) * k
|
|
75
100
|
c_median = np.median(c)
|
|
76
101
|
size = c.size
|
|
77
102
|
critlower = c_median - c_mad * low
|