arvi 0.1.3__py3-none-any.whl → 0.1.6__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.
arvi/timeseries.py CHANGED
@@ -55,6 +55,8 @@ class RV:
55
55
  _raise_on_error: bool = field(init=True, repr=False, default=True)
56
56
 
57
57
  def __repr__(self):
58
+ if self.N == 0:
59
+ return f"RV(star='{self.star}', N=0)"
58
60
  if self.time.size == self.mtime.size:
59
61
  return f"RV(star='{self.star}', N={self.N})"
60
62
  else:
@@ -73,7 +75,7 @@ class RV:
73
75
  if self.verbose:
74
76
  logger.info(f'querying DACE for {self.__star__}...')
75
77
  try:
76
- self.dace_result = get_observations(self.__star__, self.instrument,
78
+ self.dace_result = get_observations(self.__star__, self.instrument,
77
79
  verbose=self.verbose)
78
80
  except ValueError as e:
79
81
  if self._raise_on_error:
@@ -96,20 +98,18 @@ class RV:
96
98
  arrays = get_arrays(self.dace_result, verbose=self.verbose)
97
99
  for (inst, pipe, mode), data in arrays:
98
100
  child = RV.from_dace_data(self.star, inst, pipe, mode, data, _child=True)
101
+ inst = inst.replace('-', '_')
99
102
  setattr(self, inst, child)
100
103
 
101
104
  # build joint arrays
102
105
  if not self._child:
103
106
  #! sorted?
104
- self.instruments = sorted(list(self.dace_result.keys()))
107
+ self.instruments = [
108
+ inst.replace('-', '_')
109
+ for inst in sorted(list(self.dace_result.keys()))
110
+ ]
105
111
  # self.pipelines =
106
- # "observatory" (or instrument id)
107
- self.obs = np.concatenate(
108
- [np.full(getattr(self, inst).N, i+1) for i, inst in enumerate(self.instruments)],
109
- dtype=int
110
- )
111
- # mask
112
- self.mask = np.full_like(self.obs, True, dtype=bool)
112
+
113
113
  # all other quantities
114
114
  self._build_arrays()
115
115
 
@@ -202,6 +202,18 @@ class RV:
202
202
 
203
203
  error = sigma # alias!
204
204
 
205
+ @property
206
+ def _time_sorter(self):
207
+ return np.argsort(self.time)
208
+
209
+ @property
210
+ def _mtime_sorter(self):
211
+ return np.argsort(self.mtime)
212
+
213
+ @property
214
+ def _tt(self):
215
+ return np.linspace(self.mtime.min(), self.mtime.max(), 20*self.N)
216
+
205
217
  @classmethod
206
218
  def from_dace_data(cls, star, inst, pipe, mode, data, **kwargs):
207
219
  s = cls(star, **kwargs)
@@ -211,9 +223,14 @@ class RV:
211
223
  s.time = data['rjd'][ind]
212
224
  s.vrad = data['rv'][ind]
213
225
  s.svrad = data['rv_err'][ind]
226
+
214
227
  # mask
215
228
  s.mask = np.full_like(s.time, True, dtype=bool)
216
229
  s.mask[np.isnan(s.svrad)] = False
230
+ ## be careful with bogus values
231
+ s.mask[s.svrad < 0] = False
232
+
233
+
217
234
  # all other quantities
218
235
  s._quantities = []
219
236
  for arr in data.keys():
@@ -223,11 +240,22 @@ class RV:
223
240
  setattr(s, 'ccf_mask', data[arr][ind])
224
241
  s._quantities.append('ccf_mask')
225
242
  else:
243
+ # be careful with bogus values in rhk and rhk_err
244
+ if arr in ('rhk', 'rhk_err'):
245
+ mask99999 = (data[arr] == -99999) | (data[arr] == -99)
246
+ data[arr][mask99999] = np.nan
247
+
226
248
  setattr(s, arr, data[arr][ind])
227
249
  s._quantities.append(arr)
228
- #
250
+
229
251
  s._quantities = np.array(s._quantities)
230
252
 
253
+ # mask out drs_qc = False
254
+ if not s.drs_qc.all():
255
+ n = (~s.drs_qc).sum()
256
+ logger.warning(f'masking {n} points where DRS QC failed for {inst}')
257
+ s.mask &= s.drs_qc
258
+
231
259
  s.instruments = [inst]
232
260
  s.pipelines = [pipe]
233
261
  s.modes = [mode]
@@ -258,22 +286,119 @@ class RV:
258
286
  return s
259
287
 
260
288
  @classmethod
261
- def from_snapshot(cls, file):
289
+ def from_snapshot(cls, file=None, star=None):
262
290
  import pickle
263
291
  from datetime import datetime
264
- assert file.endswith('.pkl'), 'expected a .pkl file'
265
- star, timestamp = file.replace('.pkl', '').split('_')
292
+ if star is None:
293
+ assert file.endswith('.pkl'), 'expected a .pkl file'
294
+ star, timestamp = file.replace('.pkl', '').split('_')
295
+ else:
296
+ try:
297
+ file = sorted(glob(f'{star}_*.pkl'))[-1]
298
+ except IndexError:
299
+ raise ValueError(f'cannot find any file matching {star}_*.pkl')
300
+ star, timestamp = file.replace('.pkl', '').split('_')
301
+
266
302
  dt = datetime.fromtimestamp(float(timestamp))
267
303
  logger.info(f'Reading snapshot of {star} from {dt}')
268
304
  return pickle.load(open(file, 'rb'))
269
305
 
270
- def _check_instrument(self, instrument):
306
+ @classmethod
307
+ def from_rdb(cls, files, star=None, units='ms', **kwargs):
308
+ if isinstance(files, str):
309
+ files = [files]
310
+
311
+ if star is None:
312
+ star_ = np.unique([os.path.splitext(f)[0].split('_')[0] for f in files])
313
+ if star_.size == 1:
314
+ logger.info(f'assuming star is {star_[0]}')
315
+ star = star_[0]
316
+
317
+ instruments = np.array([os.path.splitext(f)[0].split('_')[1] for f in files])
318
+ logger.info(f'assuming instruments: {instruments}')
319
+
320
+ if instruments.size == 1 and len(files) > 1:
321
+ instruments = np.repeat(instruments, len(files))
322
+
323
+ factor = 1e3 if units == 'kms' else 1.0
324
+
325
+ s = cls(star, _child=True, **kwargs)
326
+
327
+ for i, (f, instrument) in enumerate(zip(files, instruments)):
328
+ data = np.loadtxt(f, skiprows=2, usecols=range(3), unpack=True)
329
+ _s = cls(star, _child=True, **kwargs)
330
+ time = data[0]
331
+ _s.time = time
332
+ _s.vrad = data[1] * factor
333
+ _s.svrad = data[2] * factor
334
+
335
+ _quantities = []
336
+ #! hack
337
+ data = np.genfromtxt(f, names=True, dtype=None, comments='--', encoding=None)
338
+
339
+ if 'fwhm' in data.dtype.fields:
340
+ _s.fwhm = data['fwhm']
341
+ if 'sfwhm' in data.dtype.fields:
342
+ _s.fwhm_err = data['sfwhm']
343
+ else:
344
+ _s.fwhm_err = 2 * _s.svrad
345
+ else:
346
+ _s.fwhm = np.zeros_like(time)
347
+ _s.fwhm_err = np.full_like(time, np.nan)
348
+
349
+ _quantities.append('fwhm')
350
+ _quantities.append('fwhm_err')
351
+
352
+ if 'rhk' in data.dtype.fields:
353
+ _s.rhk = data['rhk']
354
+ if 'srhk' in data.dtype.fields:
355
+ _s.rhk_err = data['srhk']
356
+ else:
357
+ _s.rhk = np.zeros_like(time)
358
+ _s.rhk_err = np.full_like(time, np.nan)
359
+
360
+ _quantities.append('rhk')
361
+ _quantities.append('rhk_err')
362
+
363
+ _s.bispan = np.zeros_like(time)
364
+ _s.bispan_err = np.full_like(time, np.nan)
365
+ #! end hack
366
+
367
+ _s.mask = np.ones_like(time, dtype=bool)
368
+ _s.obs = np.full_like(time, i + 1)
369
+
370
+ _s.instruments = [instrument]
371
+ _s._quantities = np.array(_quantities)
372
+ setattr(s, instrument, _s)
373
+
374
+ s._child = False
375
+ s.instruments = list(instruments)
376
+ s._build_arrays()
377
+
378
+ if kwargs.get('do_adjust_means', False):
379
+ s.adjust_means()
380
+
381
+ return s
382
+
383
+ def _check_instrument(self, instrument, strict=False):
384
+ """
385
+ Check if there are observations from `instrument`.
386
+
387
+ Args:
388
+ instrument (str, None): Instrument name to check
389
+ strict (bool): Whether to match `instrument` exactly
390
+ Returns:
391
+ instruments (list):
392
+ List of instruments matching `instrument`, or None if there
393
+ are no matches.
394
+ """
271
395
  if instrument is None:
272
396
  return self.instruments
397
+ if not strict:
398
+ if any([instrument in inst for inst in self.instruments]):
399
+ return [inst for inst in self.instruments if instrument in inst]
273
400
  if instrument in self.instruments:
274
401
  return [instrument]
275
- if any([instrument in inst for inst in self.instruments]):
276
- return [inst for inst in self.instruments if instrument in inst]
277
402
 
278
403
 
279
404
  def _build_arrays(self):
@@ -298,6 +423,13 @@ class RV:
298
423
  [getattr(self, inst).mask for inst in self.instruments]
299
424
  )
300
425
 
426
+ # "observatory" (or instrument id)
427
+ self.obs = np.concatenate(
428
+ [np.full(getattr(self, inst).N, i+1) for i, inst in enumerate(self.instruments)],
429
+ dtype=int
430
+ )
431
+
432
+
301
433
  # all other quantities
302
434
  self._quantities = getattr(self, self.instruments[0])._quantities
303
435
  if len(self.instruments) > 1:
@@ -376,18 +508,19 @@ class RV:
376
508
  extracted_files = do_download_s2d(files[:limit], directory)
377
509
 
378
510
 
379
- from .plots import plot, plot_fwhm, plot_bis, plot_rhk
511
+ from .plots import plot, plot_fwhm, plot_bis, plot_rhk, plot_quantity
380
512
  from .plots import gls, gls_fwhm, gls_bis, gls_rhk
381
513
  from .reports import report
382
514
 
383
515
  from .instrument_specific import known_issues
384
516
 
385
517
 
386
- def remove_instrument(self, instrument):
518
+ def remove_instrument(self, instrument, strict=False):
387
519
  """ Remove all observations from one instrument
388
520
 
389
521
  Args:
390
522
  instrument (str): The instrument for which to remove observations.
523
+ strict (bool): Whether to match `instrument` exactly
391
524
 
392
525
  Note:
393
526
  A common name can be used to remove observations for several subsets
@@ -406,34 +539,37 @@ class RV:
406
539
 
407
540
  will remove observations from the specific subset.
408
541
  """
409
- if instrument not in self.instruments:
542
+ instruments = self._check_instrument(instrument, strict)
543
+
544
+ if instruments is None:
410
545
  logger.error(f"No data from instrument '{instrument}'")
411
546
  logger.info(f'available: {self.instruments}')
412
547
  return
413
548
 
414
- ind = self.instruments.index(instrument) + 1
415
- remove = np.where(self.obs == ind)
416
- self.obs = np.delete(self.obs, remove)
417
- self.obs[self.obs > ind] -= 1
418
- #
419
- self.time = np.delete(self.time, remove)
420
- self.vrad = np.delete(self.vrad, remove)
421
- self.svrad = np.delete(self.svrad, remove)
422
- #
423
- self.mask = np.delete(self.mask, remove)
424
- #
425
- # all other quantities
426
- for q in self._quantities:
427
- if q not in ('rjd', 'rv', 'rv_err'):
428
- new = np.delete(getattr(self, q), remove)
429
- setattr(self, q, new)
430
- #
431
- self.instruments.remove(instrument)
432
- #
433
- delattr(self, instrument)
549
+ for instrument in instruments:
550
+ ind = self.instruments.index(instrument) + 1
551
+ remove = np.where(self.obs == ind)
552
+ self.obs = np.delete(self.obs, remove)
553
+ self.obs[self.obs > ind] -= 1
554
+ #
555
+ self.time = np.delete(self.time, remove)
556
+ self.vrad = np.delete(self.vrad, remove)
557
+ self.svrad = np.delete(self.svrad, remove)
558
+ #
559
+ self.mask = np.delete(self.mask, remove)
560
+ #
561
+ # all other quantities
562
+ for q in self._quantities:
563
+ if q not in ('rjd', 'rv', 'rv_err'):
564
+ new = np.delete(getattr(self, q), remove)
565
+ setattr(self, q, new)
566
+ #
567
+ self.instruments.remove(instrument)
568
+ #
569
+ delattr(self, instrument)
434
570
 
435
- if self.verbose:
436
- logger.info(f"Removed observations from '{instrument}'")
571
+ if self.verbose:
572
+ logger.info(f"Removed observations from '{instrument}'")
437
573
 
438
574
  if return_self:
439
575
  return self
@@ -453,6 +589,9 @@ class RV:
453
589
  logger.errors(f'index {index} is out of bounds for N={self.N}')
454
590
  return
455
591
 
592
+ if self.verbose:
593
+ logger.info(f'removing points {index}')
594
+
456
595
  self.mask[index] = False
457
596
  self._propagate_mask_changes()
458
597
  # for i, inst in zip(index, instrument):
@@ -461,6 +600,45 @@ class RV:
461
600
  if return_self:
462
601
  return self
463
602
 
603
+ def remove_non_public(self):
604
+ if self.verbose:
605
+ n = (~self.public).sum()
606
+ logger.info(f'masking non-public observations ({n})')
607
+ self.mask = self.mask & self.public
608
+ self._propagate_mask_changes()
609
+
610
+ def remove_single_observations(self):
611
+ """ Remove instruments for which there is a single observation """
612
+ instruments = deepcopy(self.instruments)
613
+ for inst in instruments:
614
+ if getattr(self, inst).mtime.size == 1:
615
+ self.remove_instrument(inst)
616
+
617
+ def remove_prog_id(self, prog_id):
618
+ from glob import has_magic
619
+ if has_magic(prog_id):
620
+ from fnmatch import filter
621
+ matching = np.unique(filter(self.prog_id, prog_id))
622
+ mask = np.full_like(self.time, False, dtype=bool)
623
+ for m in matching:
624
+ mask |= np.isin(self.prog_id, m)
625
+ ind = np.where(mask)[0]
626
+ self.remove_point(ind)
627
+ else:
628
+ if prog_id in self.prog_id:
629
+ ind = np.where(self.prog_id == prog_id)[0]
630
+ self.remove_point(ind)
631
+ else:
632
+ if self.verbose:
633
+ logger.warning(f'no observations for prog_id "{prog_id}"')
634
+
635
+
636
+ def remove_after_bjd(self, bjd):
637
+ if (self.time > bjd).any():
638
+ ind = np.where(self.time > bjd)[0]
639
+ self.remove_point(ind)
640
+
641
+
464
642
  def _propagate_mask_changes(self):
465
643
  """ link self.mask with each self.`instrument`.mask """
466
644
  masked = np.where(~self.mask)[0]
@@ -529,31 +707,43 @@ class RV:
529
707
  if return_self:
530
708
  return self
531
709
 
532
- def sigmaclip(self, sigma=3):
533
- """ Sigma-clip RVs """
710
+ def sigmaclip(self, sigma=5):
711
+ """ Sigma-clip RVs (per instrument!) """
712
+ #from scipy.stats import sigmaclip as dosigmaclip
713
+ from .stats import sigmaclip_median as dosigmaclip
714
+
534
715
  if self._child or self._did_sigma_clip:
535
716
  return
536
- from scipy.stats import sigmaclip as dosigmaclip
537
- result = dosigmaclip(self.vrad, low=sigma, high=sigma)
538
- n = self.vrad.size - result.clipped.size
539
717
 
540
- if self.verbose and n > 0:
541
- s = 's' if (n == 0 or n > 1) else ''
542
- logger.warning(f'sigma-clip RVs will remove {n} point' + s)
718
+ for inst in self.instruments:
719
+ m = self.instrument_array == inst
720
+ result = dosigmaclip(self.vrad[m], low=sigma, high=sigma)
721
+ n = self.vrad[m].size - result.clipped.size
543
722
 
544
- ind = (self.vrad > result.lower) & (self.vrad < result.upper)
723
+ ind = m & ((self.vrad < result.lower) | (self.vrad > result.upper))
545
724
 
546
- # check if going to remove all observations from one instrument
547
- if n in self.NN.values(): # all observations
548
- insts = np.unique(self.instrument_array[~ind])
549
- if insts.size == 1: # of the same instrument?
550
- if self.verbose:
551
- logger.warning(f'would remove all observations from {insts[0]}, skipping')
552
- if return_self:
553
- return self
725
+ if self.verbose and n > 0:
726
+ s = 's' if (n == 0 or n > 1) else ''
727
+ logger.warning(f'sigma-clip RVs will remove {n} point{s} for {inst}')
728
+
729
+ # # check if going to remove all observations from one instrument
730
+ # if n in self.NN.values(): # all observations
731
+ # # insts = np.unique(self.instrument_array[~ind])
732
+ # # if insts.size == 1: # of the same instrument?
733
+ # if self.verbose:
734
+ # logger.warning(f'would remove all observations from {insts[0]}, skipping')
735
+ # if return_self:
736
+ # return self
737
+ # continue
738
+
739
+ self.mask[ind] = False
554
740
 
555
- self.mask[~ind] = False
556
741
  self._propagate_mask_changes()
742
+
743
+ if self._did_adjust_means:
744
+ self._did_adjust_means = False
745
+ self.adjust_means()
746
+
557
747
  if return_self:
558
748
  return self
559
749
 
@@ -601,31 +791,38 @@ class RV:
601
791
  bad_quantities = []
602
792
 
603
793
  for q in s._quantities:
794
+ Q = getattr(s, q)
795
+
604
796
  # treat date_night specially, basically doing a group-by
605
797
  if q == 'date_night':
606
798
  inds = binRV(s.mtime, None, None, binning_indices=True)
607
- setattr(s, q, getattr(s, q)[s.mask][inds])
799
+ setattr(s, q, Q[s.mask][inds])
608
800
  continue
609
801
 
610
- if getattr(s, q).dtype != np.float64:
802
+ if Q.dtype != np.float64:
611
803
  bad_quantities.append(q)
612
804
  all_bad_quantities.append(q)
613
805
  continue
614
806
 
615
- if np.isnan(getattr(s, q)).all():
807
+ if np.isnan(Q).all():
616
808
  yb = np.full_like(tb, np.nan)
617
809
  setattr(s, q, yb)
810
+
618
811
  elif q + '_err' in s._quantities:
619
- _, yb, eb = binRV(s.mtime,
620
- getattr(s, q)[s.mask],
621
- getattr(s, q + '_err')[s.mask])
812
+ Qerr = getattr(s, q + '_err')
813
+ if (Qerr == 0.0).all():
814
+ _, yb = binRV(s.mtime, Q[s.mask], stat='mean', tstat='mean')
815
+ else:
816
+ _, yb, eb = binRV(s.mtime, Q[s.mask], Qerr[s.mask])
817
+ setattr(s, q + '_err', eb)
818
+
622
819
  setattr(s, q, yb)
623
- setattr(s, q + '_err', eb)
820
+
624
821
  elif not q.endswith('_err'):
625
822
  with warnings.catch_warnings():
626
823
  warnings.filterwarnings('ignore', category=RuntimeWarning)
627
824
  try:
628
- _, yb = binRV(s.mtime, getattr(s, q)[s.mask],
825
+ _, yb = binRV(s.mtime, Q[s.mask],
629
826
  stat=np.nanmean, tstat=np.nanmean)
630
827
  setattr(s, q, yb)
631
828
  except TypeError:
@@ -651,7 +848,33 @@ class RV:
651
848
  snew._build_arrays()
652
849
  return snew
653
850
 
851
+ def nth_day_mean(self, n=1.0):
852
+ mask = np.abs(self.mtime[:, None] - self.mtime[None, :]) < n
853
+ z = np.full((self.mtime.size, self.mtime.size), np.nan)
854
+ z[mask] = np.repeat(self.mvrad[:, None], self.mtime.size, axis=1)[mask]
855
+ return np.nanmean(z, axis=0)
856
+
857
+ def subtract_mean(self):
858
+ """ Subtract (single) mean RV from all instruments """
859
+ self._meanRV = meanRV = self.mvrad.mean()
860
+ for inst in self.instruments:
861
+ s = getattr(self, inst)
862
+ s.vrad -= meanRV
863
+ self._build_arrays()
864
+
865
+ def _add_back_mean(self):
866
+ """ Add the (single) mean RV removed by self.subtract_mean() """
867
+ if not hasattr(self, '_meanRV'):
868
+ logger.warning("no mean RV stored, run 'self.subtract_mean()'")
869
+ return
870
+
871
+ for inst in self.instruments:
872
+ s = getattr(self, inst)
873
+ s.vrad += self._meanRV
874
+ self._build_arrays()
875
+
654
876
  def adjust_means(self, just_rv=False):
877
+ """ Subtract individual mean RVs from each instrument """
655
878
  if self._child or self._did_adjust_means:
656
879
  return
657
880
 
@@ -692,6 +915,42 @@ class RV:
692
915
  if return_self:
693
916
  return self
694
917
 
918
+ def put_at_systemic_velocity(self):
919
+ """
920
+ For instruments in which mean(RV) < ptp(RV), "move" RVs to the systemic
921
+ velocity from simbad. This is useful if some instruments are centered
922
+ at zero while others are not, and instead of calling `.adjust_means()`,
923
+ but it only works when the systemic velocity is smaller than ptp(RV).
924
+ """
925
+ changed = False
926
+ for inst in self.instruments:
927
+ s = getattr(self, inst)
928
+ if s.mask.any():
929
+ if np.abs(s.mvrad.mean()) < s.mvrad.ptp():
930
+ s.vrad += self.simbad.rvz_radvel * 1e3
931
+ changed = True
932
+ else: # all observations are masked, use non-masked arrays
933
+ if np.abs(s.vrad.mean()) < s.vrad.ptp():
934
+ s.vrad += self.simbad.rvz_radvel * 1e3
935
+ changed = True
936
+ if changed:
937
+ self._build_arrays()
938
+
939
+ def sort_instruments(self, by_first_observation=True, by_last_observation=False):
940
+ if by_last_observation:
941
+ by_first_observation = False
942
+ # if by_first_observation and by_last_observation:
943
+ # logger.error("'by_first_observation' and 'by_last_observation' can't both be true")
944
+ # return
945
+ if by_first_observation:
946
+ fun = lambda i: getattr(self, i).time.min()
947
+ self.instruments = sorted(self.instruments, key=fun)
948
+ self._build_arrays()
949
+ if by_last_observation:
950
+ fun = lambda i: getattr(self, i).time.max()
951
+ self.instruments = sorted(self.instruments, key=fun)
952
+ self._build_arrays()
953
+
695
954
  #
696
955
 
697
956
  def save(self, directory=None, instrument=None, full=False):
@@ -707,6 +966,11 @@ class RV:
707
966
  """
708
967
  star_name = self.star.replace(' ', '')
709
968
 
969
+ if directory is None:
970
+ directory = '.'
971
+ else:
972
+ os.makedirs(directory, exist_ok=True)
973
+
710
974
  files = []
711
975
 
712
976
  for inst in self.instruments:
@@ -714,26 +978,27 @@ class RV:
714
978
  if instrument not in inst:
715
979
  continue
716
980
 
717
- file = f'{star_name}_{inst}.rdb'
718
- files.append(file)
719
-
720
- if directory is not None:
721
- os.makedirs(directory, exist_ok=True)
722
- file = os.path.join(directory, file)
723
-
724
981
  _s = getattr(self, inst)
725
982
 
983
+ if not _s.mask.any(): # all observations are masked, don't save
984
+ continue
985
+
726
986
  if full:
727
987
  d = np.c_[
728
988
  _s.mtime, _s.mvrad, _s.msvrad,
729
- _s.fwhm[_s.mask], _s.fwhm_err[_s.mask]
989
+ _s.fwhm[_s.mask], _s.fwhm_err[_s.mask],
990
+ _s.rhk[_s.mask], _s.rhk_err[_s.mask],
730
991
  ]
731
- header = 'bjd\tvrad\tsvrad\tfwhm\tsfwhm\n'
732
- header += '---\t----\t-----\t----\t-----'
992
+ header = 'bjd\tvrad\tsvrad\tfwhm\tsfwhm\trhk\tsrhk\n'
993
+ header += '---\t----\t-----\t----\t-----\t---\t----'
733
994
  else:
734
995
  d = np.c_[_s.mtime, _s.mvrad, _s.msvrad]
735
996
  header = 'bjd\tvrad\tsvrad\n---\t----\t-----'
736
997
 
998
+ file = f'{star_name}_{inst}.rdb'
999
+ files.append(file)
1000
+ file = os.path.join(directory, file)
1001
+
737
1002
  np.savetxt(file, d, fmt='%9.5f', header=header, delimiter='\t', comments='')
738
1003
 
739
1004
  if self.verbose:
@@ -741,6 +1006,16 @@ class RV:
741
1006
 
742
1007
  return files
743
1008
 
1009
+ def checksum(self, write_to=None):
1010
+ from hashlib import md5
1011
+ d = np.r_[self.time, self.vrad, self.svrad]
1012
+ H = md5(d.data.tobytes()).hexdigest()
1013
+ if write_to is not None:
1014
+ with open(write_to, 'w') as f:
1015
+ f.write(H)
1016
+ return H
1017
+
1018
+
744
1019
  #
745
1020
  def run_lbl(self, instrument=None, data_dir=None,
746
1021
  skysub=False, tell=False, limit=None, **kwargs):
@@ -796,9 +1071,10 @@ class RV:
796
1071
 
797
1072
  run_lbl(self, instrument, files[:limit], **kwargs)
798
1073
 
799
- def load_lbl(self, instrument=None, tell=False):
1074
+ def load_lbl(self, instrument=None, tell=False, id=None):
800
1075
  if hasattr(self, '_did_load_lbl') and self._did_load_lbl: # don't do it twice
801
1076
  return
1077
+
802
1078
  from .lbl_wrapper import load_lbl
803
1079
 
804
1080
  if instrument is None:
@@ -818,11 +1094,25 @@ class RV:
818
1094
  instruments = instrument
819
1095
 
820
1096
  for inst in instruments:
821
- load_lbl(self, inst, tell=tell)
1097
+ if self.verbose:
1098
+ logger.info(f'loading LBL data for {inst}')
822
1099
 
1100
+ load_lbl(self, inst, tell=tell, id=id)
1101
+ # self.instruments.append(f'{inst}_LBL')
1102
+
1103
+ # self._build_arrays()
823
1104
  self._did_load_lbl = True
824
1105
 
825
1106
 
1107
+ #
1108
+ @property
1109
+ def planets(self):
1110
+ from .nasaexo_wrapper import Planets
1111
+ if not hasattr(self, '_planets'):
1112
+ self._planets = Planets(self)
1113
+ return self._planets
1114
+
1115
+
826
1116
  def fit_sine(t, y, yerr, period='gls', fix_period=False):
827
1117
  from scipy.optimize import leastsq
828
1118
  if period == 'gls':