Ejemplo n.º 1
0
    def get_aper_mask_qlp(self, sap_mask="round"):
        """
        This is an estimate of QLP aperture based on
        self.hdulist[1].header['BESTAP']

        See:
        https://archive.stsci.edu/hlsps/qlp/hlsp_qlp_tess_ffi_all_tess_v1_data-prod-desc.pdf
        """
        rad = float(self.header["BESTAP"].split(":")[0])
        self.aper_radius = round(rad)
        print(f"Estimating QLP aperture using r={rad} pix.")
        if self.ffi_cutout is None:
            # first download tpf cutout
            self.ffi_cutout = FFI_cutout(
                sector=self.sector,
                gaiaDR2id=self.gaiaid,
                toiid=self.toiid,
                ticid=self.ticid,
                search_radius=self.search_radius,
                quality_bitmask=self.quality_bitmask,
            )
        self.tpf_tesscut = self.ffi_cutout.get_tpf_tesscut()
        aper_mask = parse_aperture_mask(self.tpf_tesscut,
                                        sap_mask=sap_mask,
                                        aper_radius=self.aper_radius)
        self.aper_mask = aper_mask
        return aper_mask
Ejemplo n.º 2
0
 def get_aper_mask_cdips(self, sap_mask="round"):
     """
     This is an estimate of CDIPS aperture since
     self.hdulist[1].data.names does not contain aperture
     """
     aper_pix = CDIPS_APER_PIX[int(self.aper_idx) - 1]  # aper_idx=(1,2,3)
     print(
         f"CDIPS has no aperture info in fits. Estimating aperture instead using aper_idx={aper_pix} pix."
     )
     if self.ffi_cutout is None:
         # first download tpf cutout
         self.ffi_cutout = FFI_cutout(
             sector=self.sector,
             gaiaDR2id=self.gaiaid,
             toiid=self.toiid,
             ticid=self.ticid,
             search_radius=self.search_radius,
             quality_bitmask=self.quality_bitmask,
         )
     self.tpf_tesscut = self.ffi_cutout.get_tpf_tesscut()
     idx = int(self.aper_idx) - 1  #
     aper_mask = parse_aperture_mask(
         self.tpf_tesscut,
         sap_mask=sap_mask,
         aper_radius=CDIPS_APER_PIX[idx],
     )
     self.aper_mask = aper_mask
     return aper_mask
Ejemplo n.º 3
0
 def get_aper_mask_diamante(self, sap_mask="round"):
     """
     This is an estimate of DIAmante aperture based on aper
     """
     print(f"Estimating DIAmante aperture using r={self.aper_radius} pix.")
     if self.ffi_cutout is None:
         # first download tpf cutout
         self.ffi_cutout = FFI_cutout(
             sector=self.sector,
             gaiaDR2id=self.gaiaid,
             toiid=self.toiid,
             ticid=self.ticid,
             search_radius=self.search_radius,
             quality_bitmask=self.quality_bitmask,
         )
     self.tpf_tesscut = self.ffi_cutout.get_tpf_tesscut()
     aper_mask = parse_aperture_mask(self.tpf_tesscut,
                                     sap_mask=sap_mask,
                                     aper_radius=self.aper_radius)
     self.aper_mask = aper_mask
     return aper_mask
Ejemplo n.º 4
0
def test_tpf_cutout():
    """
    """
    t = FFI_cutout(
        ticid=TICID,
        sector=SECTOR,
        quality_bitmask="default",
        apply_data_quality_mask=False,
        cutout_size=CUTOUT_SIZE,
    )
    tpf1 = t.get_tpf_tesscut()
    assert tpf1.targetid == TICID
    assert tpf1.sector == SECTOR
    assert tpf1.flux.shape[1:] == CUTOUT_SIZE

    res = lk.search_tesscut(f"TIC {t.ticid}", sector=SECTOR)
    # assert tpf2.flux.shape[1:] == CUTOUT_SIZE

    tpf2 = res.download()
    assert f"TIC {tpf1.targetid}" == tpf2.targetid
    assert tpf1.sector == tpf2.sector
    assert tpf1.quality_bitmask == tpf2.quality_bitmask
Ejemplo n.º 5
0
 def get_aper_mask_pathos(self, sap_mask="round"):
     """
     This is an estimate of PATHOS aperture only
     """
     print(
         "PATHOS has no aperture info in fits. Estimating aperture instead."
     )
     # first download tpf cutout
     self.ffi_cutout = FFI_cutout(
         sector=self.sector,
         gaiaDR2id=self.gaiaid,
         toiid=self.toiid,
         ticid=self.ticid,
         search_radius=self.search_radius,
         quality_bitmask=self.quality_bitmask,
     )
     tpf = self.ffi_cutout.get_tpf_tesscut()
     idx = int(self.aper_idx) - 1  #
     aper_mask = parse_aperture_mask(tpf,
                                     sap_mask=sap_mask,
                                     aper_radius=idx)
     return aper_mask
Ejemplo n.º 6
0
class QLP(Target):
    """
    http://archive.stsci.edu/hlsp/qlp
    """
    def __init__(
        self,
        sector=None,
        name=None,
        toiid=None,
        ticid=None,
        epicid=None,
        gaiaDR2id=None,
        ra_deg=None,
        dec_deg=None,
        quality_bitmask=None,
        search_radius=3,
        aper="best",
        lctype="KSPSAP",
        mission="tess",
        verbose=True,
        clobber=True,
    ):
        super().__init__(
            name=name,
            toiid=toiid,
            ticid=ticid,
            epicid=epicid,
            gaiaDR2id=gaiaDR2id,
            ra_deg=ra_deg,
            dec_deg=dec_deg,
            search_radius=search_radius,
            verbose=verbose,
        )
        """Initialize QLP.
        See http://archive.stsci.edu/hlsp/qlp

        Attributes
        ----------
        lctype : str
            KSPSAP : Normalized light curve detrended by kepler spline
        aper : str
            best, small, large
        """
        self.sector = sector
        if self.sector is None:
            print(f"Available sectors: {self.all_sectors}")
            if len(self.all_sectors) != 1:
                idx = [
                    True if s in QLP_SECTORS else False
                    for s in self.all_sectors
                ]
                if sum(idx) == 0:
                    msg = f"QLP lc is currently available for sectors={QLP_SECTORS}\n"
                    raise ValueError(msg)
                if sum(idx) == 1:
                    self.sector = self.all_sectors[idx][
                        0]  # get first available
                else:
                    self.sector = self.all_sectors[idx][
                        0]  # get first available
                    # get first available
                    print(
                        f"QLP lc may be available for sectors {self.all_sectors[idx]}"
                    )
            print(f"Using sector={self.sector}.")

        if self.gaiaid is None:
            _ = self.query_gaia_dr2_catalog(return_nearest_xmatch=True)

        self.aper = aper
        self.apers = ["best", "small", "large"]
        if self.aper not in self.apers:
            raise ValueError(f"Type not among {self.apers}")
        self.quality_bitmask = quality_bitmask
        self.fits_url = None
        self.hdulist = None
        self.header0 = None
        self.lctype = lctype.upper()
        self.lctypes = ["SAP", "KSPSAP"]
        if self.lctype not in self.lctypes:
            raise ValueError(f"Type not among {self.lctypes}")
        self.data, self.header = self.get_qlp_fits()
        self.lc = self.get_qlp_lc()
        self.lc.targetid = self.ticid
        self.cadence = self.header["TIMEDEL"] * u.d
        self.time = self.lc.time
        self.flux = self.lc.flux
        self.err = self.lc.flux_err
        self.sap_mask = "round"
        self.threshold_sigma = 5  # dummy
        self.percentile = 95  # dummy
        self.cutout_size = (15, 15)  # dummy
        self.aper_radius = None
        self.tpf_tesscut = None
        self.ffi_cutout = None
        self.aper_mask = None
        self.contratio = None

    def get_qlp_url(self):
        """
        hlsp_qlp_tess_ffi_<sector>-<tid>_tess_v01_llc.<exten>
        where:

        <sector> = The Sector represented as a 4-digit, zero-padded string,
                    preceded by an 's', e.g., 's0026' for Sector 26.
        <tid> = The full, 16-digit, zeo-padded TIC ID.
        <exten> = The light curve data type, either "fits" or "txt".
        """
        base = "https://archive.stsci.edu/hlsps/qlp/"
        assert self.sector is not None
        sec = str(self.sector).zfill(4)
        tic = str(self.ticid).zfill(16)
        fp = (
            base +
            f"s{sec}/{tic[:4]}/{tic[4:8]}/{tic[8:12]}/{tic[12:16]}/hlsp_qlp_tess_ffi_s{sec}-{tic}_tess_v01_llc.fits"
        )
        return fp

    def get_qlp_fits(self):
        """get qlp target and light curve header and data
        """
        fp = self.get_qlp_url()
        try:
            hdulist = fits.open(fp)
            if self.verbose:
                print(hdulist.info())
            lc_data = hdulist[1].data
            lc_header = hdulist[1].header

            # set
            self.fits_url = fp
            self.hdulist = hdulist
            self.header0 = hdulist[0].header
            return lc_data, lc_header

        except Exception:
            msg = f"File not found:\n{fp}\n"
            raise ValueError(msg)

    def get_qlp_lc(self, lc_type=None, aper=None, sort=True):
        """
        Parameters
        ----------
        lc_type : str
            {SAP, KSPSAP}
        """
        lc_type = lc_type.upper() if lc_type is not None else self.lctype
        aper = aper.upper() if aper is not None else self.aper
        assert lc_type in self.lctypes
        assert aper in self.apers

        if self.verbose:
            print(f"Using QLP {lc_type} (rad={self.aper}) lightcurve.")

        time = self.data["TIME"] + 2457000  # BJD, days
        if aper == "small":
            flux = self.data["KSPSAP_FLUX_SML"]
        elif aper == "large":
            flux = self.data["KSPSAP_FLUX_LAG"]
        else:
            flux = self.data[f"{lc_type}_FLUX"]
        if lc_type == "KSPSAP":
            err = self.data[f"{lc_type}_FLUX_ERR"]
        else:
            err = np.ones_like(flux) * np.std(flux)
        x = self.data["SAP_X"]
        y = self.data["SAP_Y"]
        quality = self.data["QUALITY"]
        cadence = self.data["CADENCENO"]
        if sort:
            idx = np.argsort(time)
        else:
            idx = np.ones_like(time, bool)
        # hack tess lightkurve
        return TessLightCurve(
            time=time[idx],
            flux=flux[idx],
            flux_err=err[idx],
            # FIXME: only day works when using lc.to_periodogram()
            time_format="jd",  # TIMEUNIT is d in fits header
            time_scale="tdb",  # TIMESYS in fits header
            centroid_col=x,
            centroid_row=y,
            quality=quality,
            quality_bitmask=self.quality_bitmask,
            cadenceno=cadence,
            sector=self.sector,
            targetid=self.toi_params["TIC ID"]
            if self.toi_params is not None else self.ticid,
            ra=self.target_coord.ra.deg,
            dec=self.target_coord.dec.deg,
            label=None,
            meta=None,
        ).normalize()

    def validate_target_header(self):
        """
        see self.header0
        """
        raise NotImplementedError()

    def get_aper_mask_qlp(self, sap_mask="round"):
        """
        This is an estimate of QLP aperture based on
        self.hdulist[1].header['BESTAP']

        See:
        https://archive.stsci.edu/hlsps/qlp/hlsp_qlp_tess_ffi_all_tess_v1_data-prod-desc.pdf
        """
        rad = float(self.header["BESTAP"].split(":")[0])
        self.aper_radius = round(rad)
        print(f"Estimating QLP aperture using r={rad} pix.")
        if self.ffi_cutout is None:
            # first download tpf cutout
            self.ffi_cutout = FFI_cutout(
                sector=self.sector,
                gaiaDR2id=self.gaiaid,
                toiid=self.toiid,
                ticid=self.ticid,
                search_radius=self.search_radius,
                quality_bitmask=self.quality_bitmask,
            )
        self.tpf_tesscut = self.ffi_cutout.get_tpf_tesscut()
        aper_mask = parse_aperture_mask(self.tpf_tesscut,
                                        sap_mask=sap_mask,
                                        aper_radius=self.aper_radius)
        self.aper_mask = aper_mask
        return aper_mask
Ejemplo n.º 7
0
class Diamante(Target):
    def __init__(
        self,
        sector=None,
        name=None,
        toiid=None,
        ticid=None,
        epicid=None,
        gaiaDR2id=None,
        ra_deg=None,
        dec_deg=None,
        quality_bitmask="hardest",
        search_radius=3,
        # mission="tess",
        aper_radius=2,
        lc_num=1,
        verbose=True,
        clobber=True,
    ):
        super().__init__(
            name=name,
            toiid=toiid,
            ticid=ticid,
            epicid=epicid,
            gaiaDR2id=gaiaDR2id,
            ra_deg=ra_deg,
            dec_deg=dec_deg,
            search_radius=search_radius,
            verbose=verbose,
        )
        """Initialize Diamante.
        See http://archive.stsci.edu/hlsp/diamante

        Attributes
        ----------
        lc_num : int
            [1,2]
            1 : co-trended lc; 2: normalized lc
        aper_radius : int
            [1,2] pix (default=2)
        """
        self.base_url = "https://archive.stsci.edu/hlsps/diamante"
        self.diamante_catalog = self.get_diamante_catalog()
        self.new_diamante_candidates = self.get_new_diamante_candidates()
        self.candidate_params = self.get_candidate_ephemeris()
        self.sectors = self.all_sectors  # multi-sectors
        self.sector = self.sectors[0]

        if self.gaiaid is None:
            _ = self.query_gaia_dr2_catalog(return_nearest_xmatch=True)

        self.lc_num = lc_num
        self.lc_nums = [1, 2]
        if self.lc_num not in self.lc_nums:
            raise ValueError(f"Type not among {self.lc_nums}")
        self.aper_radius = aper_radius
        self.apers = [1, 2]
        if self.aper_radius not in self.apers:
            raise ValueError(f"Type not among {self.apers}")
        self.quality_bitmask = quality_bitmask
        self.fits_url = None
        self.hdulist = None
        self.header0 = None
        self.data, self.header = self.get_diamante_fits()
        self.lc = self.get_diamante_lc()
        self.lc.targetid = self.ticid
        self.time = self.lc.time
        self.flux = self.lc.flux
        self.err = self.lc.flux_err
        self.sap_mask = "round"
        # self.threshold_sigma = 5  # dummy
        # self.percentile = 95  # dummy
        self.cutout_size = (15, 15)  # dummy
        self.tpf_tesscut = None
        self.ffi_cutout = None
        self.aper_mask = None
        self.contratio = None

    def get_diamante_catalog(self):
        """
        """
        diamante_catalog_fp = Path(DATA_PATH, "diamante_catalog.csv")
        if diamante_catalog_fp.exists():
            df = pd.read_csv(diamante_catalog_fp)
        else:
            url = f"{self.base_url}/hlsp_diamante_tess_lightcurve_catalog_tess_v1_cat.csv"
            df = pd.read_csv(url)
            df.to_csv(diamante_catalog_fp, index=False)
        return df

    def get_new_diamante_candidates(self):
        """
        """
        tois = get_tois()
        df = self.diamante_catalog.copy()
        idx = df["#ticID"].isin(tois["TIC ID"])
        return df[~idx]

    def get_diamante_url(self, ext="fits"):
        """
        hlsp_diamante_tess_lightcurve_tic-<id>_tess_v1_<ext>
        where:

        <id> = the full, zero-padded, 16-digit TIC ID
        <ext> = type of file product, one of "llc.fits", "llc.txt", or "dv.pdf"

        https://archive.stsci.edu/hlsps/diamante/0000/0009/0167/4675/
        hlsp_diamante_tess_lightcurve_tic-0000000901674675_tess_v1_llc.fits
        """
        if not np.any(self.diamante_catalog["#ticID"].isin([self.ticid])):
            raise ValueError(f"TIC {self.ticid} not in DIAmante catalog.")
        tid = f"{self.ticid}".zfill(16)
        dir = f"{tid[0:4]}/{tid[4:8]}/{tid[8:12]}/{tid[12:16]}"
        fp = f"{self.base_url}/{dir}/hlsp_diamante_tess_lightcurve_tic-{tid}_tess_v1_llc.{ext}"
        return fp

    def get_diamante_fits(self):
        """get target and light curve header and data
        """
        fp = self.get_diamante_url()
        try:
            hdulist = fits.open(fp)
            if self.verbose:
                print(hdulist.info())
            lc_data = hdulist[1].data
            lc_header = hdulist[1].header

            # set
            self.fits_url = fp
            self.hdulist = hdulist
            self.header0 = hdulist[0].header
            return lc_data, lc_header

        except Exception:
            msg = f"File not found:\n{fp}\n"
            raise ValueError(msg)

    def get_diamante_lc(self, lc_num=None, aper_radius=None, sort=True):
        """
        Parameters
        ----------
        lc_type : int

        """
        aper_radius = self.aper_radius if aper_radius is None else aper_radius
        lc_num = self.lc_num if lc_num is None else lc_num
        assert lc_num in self.lc_nums
        assert aper_radius in self.apers

        if self.verbose:
            print(f"Using DIAmante LC{lc_num} (rad={aper_radius}) lightcurve.")

        time = self.data["BTJD"] + 2457000  # BJD, days
        flux = self.data[f"LC{lc_num}_AP{aper_radius}"]
        err = self.data[f"ELC{lc_num}_AP{aper_radius}"]
        quality = self.data[f"FLAG_AP{lc_num}"]
        if sort:
            idx = np.argsort(time)
        else:
            idx = np.ones_like(time, bool)
        if self.quality_bitmask == "hardest":
            idx2 = quality == 0
        else:
            idx2 = np.ones_like(quality, bool)

        # hack tess lightkurve
        return TessLightCurve(
            time=time[idx],
            flux=flux[idx],
            flux_err=err[idx],
            # FIXME: only day works when using lc.to_periodogram()
            time_format="jd",  # TIMEUNIT is d in fits header
            time_scale="tdb",  # TIMESYS in fits header
            # centroid_col=None,
            # centroid_row=None,
            quality=quality,
            quality_bitmask=self.quality_bitmask,
            # cadenceno=cadence,
            sector=self.sectors,
            targetid=self.ticid,
            ra=self.target_coord.ra.deg,
            dec=self.target_coord.dec.deg,
            label=None,
            meta=None,
        ).normalize()[idx2]

    def plot_all_lcs(self, sigma=10):
        """
        """
        # lcs = {}
        fig, ax = pl.subplots(1, 1, figsize=(10, 6))
        for aper in [1, 2]:
            lc = self.get_diamante_lc(
                lc_num=1, aper_radius=aper).remove_outliers(sigma=sigma)
            lc.scatter(ax=ax, label=f"aper={aper}")
            # lcs[aper] = lc
        ax.set_title(f"{self.target_name} (sector {self.sector})")
        ax.legend()
        return fig

    def validate_target_header(self):
        """
        see self.header0
        """
        assert self.header0["OBJECT"] == self.target_name
        raise NotImplementedError()

    def get_aper_mask_diamante(self, sap_mask="round"):
        """
        This is an estimate of DIAmante aperture based on aper
        """
        print(f"Estimating DIAmante aperture using r={self.aper_radius} pix.")
        if self.ffi_cutout is None:
            # first download tpf cutout
            self.ffi_cutout = FFI_cutout(
                sector=self.sector,
                gaiaDR2id=self.gaiaid,
                toiid=self.toiid,
                ticid=self.ticid,
                search_radius=self.search_radius,
                quality_bitmask=self.quality_bitmask,
            )
        self.tpf_tesscut = self.ffi_cutout.get_tpf_tesscut()
        aper_mask = parse_aperture_mask(self.tpf_tesscut,
                                        sap_mask=sap_mask,
                                        aper_radius=self.aper_radius)
        self.aper_mask = aper_mask
        return aper_mask

    def get_candidate_ephemeris(self):
        df = self.new_diamante_candidates
        d = df[df["#ticID"] == self.ticid].squeeze()
        self.period = d["periodBLS"]
        self.epoch = d["t0Fit"] + TESS_TIME_OFFSET
        self.duration = d["duration"]
        self.depth = d["trdepth"] * 1e-6
        return d.copy()

    def get_flat_lc(
        self,
        lc=None,
        period=None,
        epoch=None,
        duration=None,
        window_length=None,
        method="biweight",
        sigma_upper=None,
        sigma_lower=None,
        return_trend=False,
    ):
        """
        """
        lc = self.lc if lc is None else lc
        period = self.period if period is None else period
        epoch = self.epoch if epoch is None else epoch
        duration = self.duration if duration is None else duration
        duration_hours = duration * 24
        if duration_hours < 1:
            raise ValueError("Duration should be in hours.")
        if window_length is None:
            window_length = 0.5 if duration is None else duration * 3
        if self.verbose:
            print(
                f"Using {method} filter with window_length={window_length:.2f} day"
            )
        if (period is not None) & (epoch is not None) & (duration is not None):
            tmask = get_transit_mask(lc.time,
                                     period=period,
                                     t0=epoch,
                                     dur=duration_hours / 24)
        else:
            tmask = np.zeros_like(lc.time, dtype=bool)
        # dummy holder
        flat, trend = lc.flatten(return_trend=True)
        # flatten using wotan
        wflat, wtrend = flatten(
            lc.time,
            lc.flux,
            method=method,
            window_length=window_length,
            mask=tmask,
            return_trend=True,
        )
        # overwrite
        flat.flux = wflat
        trend.flux = wtrend
        # clean lc
        sigma_upper = 5 if sigma_upper is None else sigma_upper
        sigma_lower = 10 if sigma_lower is None else sigma_lower
        flat = (
            flat.remove_nans()
        )  # .remove_outliers(sigma_upper=sigma_upper, sigma_lower=sigma_lower)
        if return_trend:
            return flat, trend
        else:
            return flat

    def plot_trend_flat_lcs(
        self,
        lc=None,
        period=None,
        epoch=None,
        duration=None,
        binsize=10,
        **kwargs,
    ):
        """
        plot trend and falt lightcurves (uses TOI ephemeris by default)
        """
        lc = self.lc if lc is None else lc
        period = self.period if period is None else period
        epoch = self.epoch if epoch is None else epoch
        duration = self.duration if duration is None else duration
        duration_hours = duration * 24
        if duration_hours < 1:
            raise ValueError("Duration should be in hours.")
        assert ((period is not None) & (epoch is not None) &
                (duration is not None))
        if self.verbose:
            print(
                f"Using period={period:.4f} d, epoch={epoch:.2f} BTJD, duration={duration_hours:.2f} hr"
            )
        fig, axs = pl.subplots(2,
                               1,
                               figsize=(12, 10),
                               constrained_layout=True,
                               sharex=True)

        if (period is not None) & (epoch is not None) & (duration is not None):
            tmask = get_transit_mask(lc.time,
                                     period=period,
                                     t0=epoch,
                                     dur=duration_hours / 24)
        else:
            tmask = np.zeros_like(lc.time, dtype=bool)
        ax = axs.flatten()
        flat, trend = self.get_flat_lc(
            lc,
            period=period,
            duration=duration_hours,
            return_trend=True,
            **kwargs,
        )
        lc[tmask].scatter(ax=ax[0], c="r", zorder=5, label="transit")
        if np.any(tmask):
            lc[~tmask].scatter(ax=ax[0], c="k", alpha=0.5, label="_nolegend_")
        ax[0].set_title(f"{self.target_name} (sector {lc.sector})")
        ax[0].set_xlabel("")
        trend.plot(ax=ax[0], c="b", lw=2, label="trend")

        if (period is not None) & (epoch is not None) & (duration is not None):
            tmask2 = get_transit_mask(flat.time,
                                      period=period,
                                      t0=epoch,
                                      dur=duration_hours / 24)
        else:
            tmask2 = np.zeros_like(lc.time, dtype=bool)
        flat.scatter(ax=ax[1], c="k", alpha=0.5, label="flat")
        if np.any(tmask2):
            flat[tmask2].scatter(ax=ax[1],
                                 zorder=5,
                                 c="r",
                                 s=10,
                                 label="transit")
        flat.bin(binsize).scatter(ax=ax[1],
                                  s=10,
                                  c="C1",
                                  label=f"bin ({binsize})")
        fig.subplots_adjust(hspace=0)
        return fig

    def run_tls(self, flat, plot=True, **tls_kwargs):
        """
        """
        tls = transitleastsquares(t=flat.time, y=flat.flux, dy=flat.flux_err)
        tls_results = tls.power(**tls_kwargs)
        self.tls_results = tls_results
        if plot:
            fig = plot_tls(tls_results)
            fig.axes[0].set_title(f"{self.target_name} (sector {flat.sector})")
            return fig

    def plot_fold_lc(self,
                     flat,
                     period=None,
                     epoch=None,
                     duration=None,
                     binsize=10,
                     ax=None):
        """
        plot folded lightcurve (uses TOI ephemeris by default)
        """
        period = self.period if period is None else period
        epoch = self.epoch if epoch is None else epoch
        duration = self.duration if duration is None else duration
        if duration is None:
            if self.tls_results is not None:
                duration = self.tls_results.duration
        duration_hours = duration * 24
        if duration_hours < 1:
            raise ValueError("Duration should be in hours.")
        if ax is None:
            fig, ax = pl.subplots(figsize=(12, 8))
        errmsg = "Provide period and epoch."
        assert (period is not None) & (epoch is not None), errmsg
        fold = flat.fold(period=period, t0=epoch)
        fold.scatter(ax=ax, c="k", alpha=0.5, label="folded")
        fold.bin(binsize).scatter(ax=ax,
                                  s=20,
                                  c="C1",
                                  label=f"bin ({binsize})")
        if duration is not None:
            xlim = 3 * duration / period
            ax.set_xlim(-xlim, xlim)
        ax.set_title(f"{self.target_name} (sector {flat.sector})")
        return ax

    def plot_odd_even(self,
                      flat,
                      period=None,
                      epoch=None,
                      duration=None,
                      ylim=None):
        """
        """
        period = self.period if period is None else period
        epoch = self.epoch - TESS_TIME_OFFSET if epoch is None else epoch
        duration = self.duration if duration is None else duration
        if (period is None) or (epoch is None):
            if self.tls_results is None:
                print("Running TLS")
                _ = self.run_tls(flat, plot=False)
            period = self.tls_results.period
            epoch = self.tls_results.T0
            ylim = self.tls_results.depth if ylim is None else ylim
        if ylim is None:
            ylim = 1 - self.depth
        fig = plot_odd_even(flat,
                            period=period,
                            epoch=epoch,
                            duration=duration,
                            yline=ylim)
        fig.suptitle(f"{self.target_name} (sector {flat.sector})")
        return fig

    def get_transit_mask(self, lc, period, epoch, duration_hours):
        """
        """
        period = self.period if period is None else period
        epoch = self.epoch if epoch is None else epoch
        duration_hours = (self.duration *
                          24 if duration_hours is None else duration_hours)
        if duration_hours < 1:
            raise ValueError("Duration should be in hours.")
        tmask = get_transit_mask(lc.time,
                                 period=period,
                                 t0=epoch,
                                 dur=duration_hours / 24)
        return tmask
Ejemplo n.º 8
0
class PATHOS(Target):
    def __init__(
        self,
        sector=None,
        cam=None,
        ccd=None,
        name=None,
        toiid=None,
        ticid=None,
        epicid=None,
        gaiaDR2id=None,
        ra_deg=None,
        dec_deg=None,
        quality_bitmask=None,
        search_radius=3,
        lctype="corr",
        aper_idx=4,
        mission="tess",
        verbose=True,
        clobber=False,
        # mission=("Kepler", "K2", "TESS"),
        # quarter=None,
        # month=None,
        # campaign=None,
        # limit=None,
    ):
        super().__init__(
            name=name,
            toiid=toiid,
            ticid=ticid,
            epicid=epicid,
            gaiaDR2id=gaiaDR2id,
            ra_deg=ra_deg,
            dec_deg=dec_deg,
            search_radius=search_radius,
            verbose=verbose,
        )
        """Initialize PATHOS.
        See http://archive.stsci.edu/hlsp/pathos

        Attributes
        ----------
        aper_idx : int
            PATHOS aperture index: [1,2,3,4] pix in radius
        lctype: str
            PATHOS lc types: ["raw", "corr"]
        """
        if self.verbose:
            print("Using PATHOS lightcurve.")
        self.sector = sector
        if self.sector is None:
            print(f"Available sectors: {self.all_sectors}")
            if len(self.all_sectors) != 1:
                idx = [
                    True if s in PATHOS_SECTORS else False
                    for s in self.all_sectors
                ]
                if sum(idx) == 0:
                    msg = f"PATHOS lc is currently available for sectors={PATHOS_SECTORS}\n"
                    raise ValueError(msg)
                if sum(idx) == 1:
                    self.sector = self.all_sectors[idx][
                        0]  # get first available
                else:
                    self.sector = self.all_sectors[idx][
                        0]  # get first available
                    # get first available
                    print(
                        f"PATHOS lc may be available for sectors {self.all_sectors[idx]}"
                    )
            print(f"Using sector={self.sector}.")
        self.mast_table = self.get_mast_table()
        if self.gaiaid is None:
            _ = self.query_gaia_dr2_catalog(return_nearest_xmatch=True)
        self.lctype = lctype
        self.lctypes = ["raw", "corr"]
        self.aper_idx = str(aper_idx)
        assert self.aper_idx in [
            "1",
            "2",
            "3",
            "4",
        ], "PATHOS has only [1,2,3,4] pix aperture radius"
        self.fits_url = None
        self.header0 = None  # target header
        self.hdulist = None
        self.data, self.header = self.get_pathos_fits()
        self.quality_bitmask = quality_bitmask
        self.lc = self.get_pathos_lc()
        self.pathos_candidates = self.get_pathos_candidates()
        self.tpf_tesscut = None
        self.ffi_cutout = None
        self.aper_mask = None

    def get_pathos_candidates(self):
        fp = Path(DATA_PATH, "pathos_candidates.csv")
        if (not fp.exists()) or self.clobber:
            d = pd.read_html("http://archive.stsci.edu/hlsp/pathos")[0]
            d.to_csv(fp, index=False)
            if self.clobber:
                print("Saved: ", fp)
        else:
            d = pd.read_csv(fp)
            if self.clobber:
                print("Loaded: ", fp)
        return d

    def get_mast_table(self):
        """https://archive.stsci.edu/hlsp/cdips
        """
        if self.gaia_params is None:
            _ = self.query_gaia_dr2_catalog(return_nearest_xmatch=True)
        if self.tic_params is None:
            _ = self.query_tic_catalog(return_nearest_xmatch=True)
        if not self.validate_gaia_tic_xmatch():
            raise ValueError("Gaia and Tic Catalog match failed")
        mast_table = Observations.query_criteria(target_name=self.ticid,
                                                 provenance_name="PATHOS")
        if len(mast_table) == 0:
            raise ValueError("No PATHOS lightcurve in MAST.")
        else:
            print(f"Found {len(mast_table)} PATHOS lightcurves.")
        return mast_table.to_pandas()

    def get_pathos_url(self):
        """
        Each target has a FITS and TXT version of the light curves available.
        The files are stored in sub-directories based on the Sector they are
        in as a 4-digit, zero-padded number, e.g., "s0001/" for Sector 1.
        The data file naming convention is:

        hlsp_pathos_tess_lightcurve_tic-<ticid>-<sector>_tess_v1_<ext>
        """
        base = "https://archive.stsci.edu/hlsps/pathos/"
        assert self.sector is not None
        assert self.gaiaid is not None
        tic = str(self.ticid).zfill(10)
        sect = str(self.sector).zfill(4)
        url = (
            base +
            f"s{sect}/hlsp_pathos_tess_lightcurve_tic-{tic}-s{sect}_tess_v1_llc.fits"
        )
        return url

    def get_pathos_fits(self):
        """get pathos target and light curve header and data
        """
        fp = self.get_pathos_url()
        try:
            hdulist = fits.open(fp)
            if self.verbose:
                print(hdulist.info())
            lc_data = hdulist[1].data
            lc_header = hdulist[1].header

            # set
            self.fits_url = fp
            self.hdulist = hdulist
            self.header0 = hdulist[0].header
            return lc_data, lc_header

        except Exception:
            msg = f"File not found:\n{fp}\n"
            # msg += f"Using sector={self.sector} in {self.all_sectors}.\n"
            raise ValueError(msg)

    def get_pathos_lc(self, lctype=None, aper_idx=None, sort=True):
        """
        Parameters
        ----------
        """
        aper = aper_idx if aper_idx is not None else self.aper_idx
        lctype = lctype if lctype is not None else self.lctype

        tstr = "TIME"
        if lctype == "raw":
            fstr = f"AP{aper}_FLUX_RAW"
        elif lctype == "corr":
            # tstr = "TIMECORR"
            fstr = f"AP{aper}_FLUX_COR"
        else:
            raise ValueError("use raw or corr")
        # barycentric-corrected, truncated TESS Julian Date (BJD - 2457000.0)
        time = self.data[tstr]
        flux = self.data[fstr]
        # err = self.data[estr]
        xpos = self.data["X_POSITION"]
        ypos = self.data["Y_POSITION"]
        if sort:
            idx = np.argsort(time)
        else:
            idx = np.ones_like(time, bool)
        # hack tess lightkurve
        return TessLightCurve(
            time=time[idx],
            flux=flux[idx],
            # flux_err=err[idx],
            # FIXME: only day works when using lc.to_periodogram()
            time_format="jd",  # TIMEUNIT is bjd in fits header
            time_scale="tdb",  # TIMESYS in fits header
            centroid_col=ypos,
            centroid_row=xpos,
            quality=None,
            quality_bitmask=self.quality_bitmask,
            cadenceno=None,
            sector=self.sector,
            camera=self.header0["CAMERA"],
            ccd=self.header0["CCD"],
            targetid=self.toi_params["TIC ID"]
            if self.toi_params is not None else self.ticid,
            ra=self.target_coord.ra.deg,
            dec=self.target_coord.dec.deg,
            label=None,
            meta=None,
        ).normalize()

    def get_aper_mask_pathos(self, sap_mask="round"):
        """
        This is an estimate of PATHOS aperture only
        """
        print(
            f"PATHOS has no aperture info in fits. Estimating aperture instead using aper_idx={self.aper_idx} pix."
        )
        if self.ffi_cutout is None:
            # first download tpf cutout
            self.ffi_cutout = FFI_cutout(
                sector=self.sector,
                gaiaDR2id=self.gaiaid,
                toiid=self.toiid,
                ticid=self.ticid,
                search_radius=self.search_radius,
                quality_bitmask=self.quality_bitmask,
            )
        self.tpf_tesscut = self.ffi_cutout.get_tpf_tesscut()
        idx = int(self.aper_idx) - 1  #
        aper_mask = parse_aperture_mask(self.tpf_tesscut,
                                        sap_mask=sap_mask,
                                        aper_radius=idx)
        self.aper_mask = aper_mask
        return aper_mask

    def validate_target_header(self):
        """
        see self.header0['sector']==self.sector
        """
        raise NotImplementedError()

    def plot_all_lcs(self, lctype="corr", sigma=10):
        """
        """
        pathos_lcs = {}
        fig, ax = pl.subplots(1, 1, figsize=(10, 6))
        for aper in [1, 2, 3, 4]:
            lc = self.get_pathos_lc(lctype=lctype,
                                    aper_idx=aper).remove_outliers(sigma=sigma)
            lc.scatter(ax=ax, label=f"aper={aper}")
            pathos_lcs[aper] = lc
        ax.set_title(f"{self.target_name} (sector {self.sector})")
        ax.legend(title=f"lc={lctype}")
        return fig

    def get_flat_lc(
        self,
        lc,
        period=None,
        epoch=None,
        duration=None,
        window_length=None,
        method="biweight",
        sigma_upper=None,
        sigma_lower=None,
        return_trend=False,
    ):
        """
        """
        if duration < 1:
            raise ValueError("Duration should be in hours.")
        if window_length is None:
            window_length = 0.5 if duration is None else duration / 24 * 3
        if self.verbose:
            print(
                f"Using {method} filter with window_length={window_length:.2f} day"
            )
        if (period is not None) & (epoch is not None) & (duration is not None):
            tmask = get_transit_mask(lc.time,
                                     period=period,
                                     t0=epoch,
                                     dur=duration / 24)
        else:
            tmask = np.zeros_like(lc.time, dtype=bool)
        # dummy holder
        flat, trend = lc.flatten(return_trend=True)
        # flatten using wotan
        wflat, wtrend = flatten(
            lc.time,
            lc.flux,
            method=method,
            window_length=window_length,
            mask=tmask,
            return_trend=True,
        )
        # overwrite
        flat.flux = wflat
        trend.flux = wtrend
        # clean lc
        sigma_upper = 5 if sigma_upper is None else sigma_upper
        sigma_lower = 10 if sigma_lower is None else sigma_lower
        flat = flat.remove_nans().remove_outliers(sigma_upper=sigma_upper,
                                                  sigma_lower=sigma_lower)
        if return_trend:
            return flat, trend
        else:
            return flat

    def plot_trend_flat_lcs(self,
                            lc,
                            period,
                            epoch,
                            duration,
                            binsize=10,
                            **kwargs):
        """
        plot trend and falt lightcurves (uses TOI ephemeris by default)
        """
        if duration < 1:
            raise ValueError("Duration should be in hours.")
        assert ((period is not None) & (epoch is not None) &
                (duration is not None))
        if self.verbose:
            print(
                f"Using period={period:.4f} d, epoch={epoch:.2f} BTJD, duration={duration:.2f} hr"
            )
        fig, axs = pl.subplots(2,
                               1,
                               figsize=(12, 10),
                               constrained_layout=True,
                               sharex=True)

        if (period is not None) & (epoch is not None) & (duration is not None):
            tmask = get_transit_mask(lc.time,
                                     period=period,
                                     t0=epoch,
                                     dur=duration / 24)
        else:
            tmask = np.zeros_like(lc.time, dtype=bool)
        ax = axs.flatten()
        flat, trend = self.get_flat_lc(lc,
                                       period=period,
                                       duration=duration,
                                       return_trend=True,
                                       **kwargs)
        lc[tmask].scatter(ax=ax[0], c="r", zorder=5, label="transit")
        if np.any(tmask):
            lc[~tmask].scatter(ax=ax[0], c="k", alpha=0.5, label="_nolegend_")
        ax[0].set_title(f"{self.target_name} (sector {lc.sector})")
        ax[0].set_xlabel("")
        trend.plot(ax=ax[0], c="b", lw=2, label="trend")

        if (period is not None) & (epoch is not None) & (duration is not None):
            tmask2 = get_transit_mask(flat,
                                      period=period,
                                      epoch=epoch,
                                      duration_hours=duration)
        else:
            tmask2 = np.zeros_like(lc.time, dtype=bool)
        flat.scatter(ax=ax[1], c="k", alpha=0.5, label="flat")
        if np.any(tmask2):
            flat[tmask2].scatter(ax=ax[1],
                                 zorder=5,
                                 c="r",
                                 s=10,
                                 label="transit")
        flat.bin(binsize).scatter(ax=ax[1],
                                  s=10,
                                  c="C1",
                                  label=f"bin ({binsize})")
        fig.subplots_adjust(hspace=0)
        return fig

    def run_tls(self, flat, plot=True, **tls_kwargs):
        """
        """
        tls = transitleastsquares(t=flat.time, y=flat.flux, dy=flat.flux_err)
        tls_results = tls.power(**tls_kwargs)
        self.tls_results = tls_results
        if plot:
            fig = plot_tls(tls_results)
            fig.axes[0].set_title(f"{self.target_name} (sector {flat.sector})")
            return fig

    def plot_fold_lc(self,
                     flat,
                     period,
                     epoch,
                     duration=None,
                     binsize=10,
                     ax=None):
        """
        plot folded lightcurve (uses TOI ephemeris by default)
        """
        if ax is None:
            fig, ax = pl.subplots(figsize=(12, 8))
        errmsg = "Provide period and epoch."
        assert (period is not None) & (epoch is not None), errmsg
        fold = flat.fold(period=period, t0=epoch)
        fold.scatter(ax=ax, c="k", alpha=0.5, label="folded")
        fold.bin(binsize).scatter(ax=ax,
                                  s=20,
                                  c="C1",
                                  label=f"bin ({binsize})")
        if duration is None:
            if self.tls_results is not None:
                duration = self.tls_results.duration
        if duration is not None:
            xlim = 3 * duration / period
            ax.set_xlim(-xlim, xlim)
        ax.set_title(f"{self.target_name} (sector {flat.sector})")
        return ax

    def plot_odd_even(self, flat, period=None, epoch=None, ylim=None):
        """
        """
        period = self.toi_period if period is None else period
        epoch = self.toi_epoch - TESS_TIME_OFFSET if epoch is None else epoch
        if (period is None) or (epoch is None):
            if self.tls_results is None:
                print("Running TLS")
                _ = self.run_tls(flat, plot=False)
            period = self.tls_results.period
            epoch = self.tls_results.T0
            ylim = self.tls_results.depth if ylim is None else ylim
        if ylim is None:
            ylim = 1 - self.toi_depth
        fig = plot_odd_even(flat, period=period, epoch=epoch, yline=ylim)
        fig.suptitle(f"{self.target_name} (sector {flat.sector})")
        return fig

    def get_transit_mask(self, lc, period, epoch, duration_hours):
        """
        """
        tmask = get_transit_mask(lc.time,
                                 period=period,
                                 t0=epoch,
                                 dur=duration_hours / 24)
        return tmask
Ejemplo n.º 9
0
class CDIPS(Target):
    """
    The primary header contains information about the target star, including the
    catalogs that claimed cluster membership or youth (`CDIPSREF`), and a key that
    enables back-referencing to those catalogs in order to discover whatever those
    investigators said about the object (`CDEXTCAT`). Membership claims based on
    Gaia-DR2 data are typically the highest quality claims. Cross-matches against
    TICv8 and Gaia-DR2 are also included.
    """
    def __init__(
        self,
        sector=None,
        cam=None,
        ccd=None,
        name=None,
        toiid=None,
        ticid=None,
        epicid=None,
        gaiaDR2id=None,
        ra_deg=None,
        dec_deg=None,
        quality_bitmask=None,
        search_radius=3,
        lctype="flux",
        aper_idx=1,
        mission="tess",
        verbose=True,
        clobber=True,
        # mission=("Kepler", "K2", "TESS"),
        # quarter=None,
        # month=None,
        # campaign=None,
        # limit=None,
    ):
        super().__init__(
            name=name,
            toiid=toiid,
            ticid=ticid,
            epicid=epicid,
            gaiaDR2id=gaiaDR2id,
            ra_deg=ra_deg,
            dec_deg=dec_deg,
            search_radius=search_radius,
            verbose=verbose,
        )
        """Initialize CDIPS

        Attributes
        ----------
        aper_idx : str
            CDIPS aperture index: [1,2,3] which is [1,1.5,2.25] pix in radius
        lctype: str
            CDIPS lc types: ["flux", "mag", "tfa", "pca"]
        """
        if self.verbose:
            print("Using CDIPS lightcurve.")
        self.sector = sector
        if self.sector is None:
            print(f"Available sectors: {self.all_sectors}")
            if len(self.all_sectors) == 1:
                self.sector = self.all_sectors[0]
            else:
                idx = [
                    True if s in CDIPS_SECTORS else False
                    for s in self.all_sectors
                ]
                if sum(idx) == 0:
                    msg = f"CDIPS lc is currently available for sectors={CDIPS_SECTORS}\n"
                    raise ValueError(msg)
                if sum(idx) == 1:
                    self.sector = self.all_sectors[idx][
                        0]  # get first available
                else:
                    self.sector = self.all_sectors[idx][
                        0]  # get first available
                    # get first available
                    print(
                        f"CDIPS lc may be available for sectors {self.all_sectors[idx]}"
                    )
            print(f"Using sector={self.sector}.")
        self.mast_table = self.get_mast_table()
        self.cam = cam
        self.ccd = ccd
        if (self.sector is None) | (self.cam is None) | (self.ccd is None):
            # overwrite
            sector0, cam0, ccd0 = get_sector_cam_ccd(self.target_coord,
                                                     self.sector)
            self.cam = cam0
            self.ccd = ccd0
        else:
            assert self.cam == cam0
            assert self.ccd == ccd

        if self.gaiaid is None:
            _ = self.query_gaia_dr2_catalog(return_nearest_xmatch=True)

        # self.mission = mission
        self.lctype = lctype
        self.lctypes = ["flux", "mag", "tfa", "pca"]
        self.aper_idx = str(aper_idx)
        assert self.aper_idx in [
            "1",
            "2",
            "3",
        ], "CDIPS has only [1,2,3] aperture indices"
        self.quality_bitmask = quality_bitmask
        self.fits_url = None
        self.header0 = None  # target header
        self.catalog_ref = None  # references
        self.catalog_gaiaids = None  # gaia id(s) in catalog_ref
        self.hdulist = None
        # self.ccd_info = Tesscut.get_sectors(self.target_coord).to_pandas()
        self.data, self.header = self.get_cdips_fits()
        self.lc = self.get_cdips_lc()
        self.lc.targetid = self.ticid
        self.cadence = self.header["XPOSURE"] * u.second  # .to(u.minute)
        self.time = self.lc.time
        self.flux = self.lc.flux
        self.err = self.lc.flux_err
        if self.lctype not in self.lctypes:
            raise ValueError(f"Type not among {self.lctypes}")
        ctois = get_ctois()
        self.cdips_candidates = ctois[ctois["User"] == "bouma"]
        self.ffi_cutout = None

    def get_mast_table(self):
        """https://archive.stsci.edu/hlsp/cdips
        """
        if self.gaia_params is None:
            _ = self.query_gaia_dr2_catalog(return_nearest_xmatch=True)
        if self.tic_params is None:
            _ = self.query_tic_catalog(return_nearest_xmatch=True)
        if not self.validate_gaia_tic_xmatch():
            raise ValueError("Gaia and Tic Catalog match failed")
        mast_table = Observations.query_criteria(target_name=self.ticid,
                                                 provenance_name="CDIPS")
        if len(mast_table) == 0:
            raise ValueError("No CDIPS lightcurve in MAST.")
        else:
            print(f"Found {len(mast_table)} CDIPS lightcurves.")
        return mast_table.to_pandas()

    def get_cdips_url(self):
        """
        Each target is stored in a sub-directory based on the Sector it was observed in
        as a 4-digit zero-padded number.  They are further divided into sub-directories
        based on the camera and chip number they are on.  For example, 's0006/cam1_ccd1/' for
         Sector 6 light curves that are on CCD #1 on Camera #1.

        The light curves are in a `.fits` format familiar to users of the Kepler, K2,
        and TESS-short cadence light curves made by the NASA Ames team.  Their file names
        follow this convention:

        hlsp_cdips_tess_ffi_gaiatwo<gaiaid>-<sectornum>_tess_v01_llc.fits

        where:
          <gaiaid> = full Gaia DR2 target id, e.g., '0003321416308714545920'
          <sectornum? = 4-digit, zero-padded Sector number, e.g., '0006'
        """
        base = "https://archive.stsci.edu/hlsps/cdips/"
        assert self.sector is not None
        assert self.cam is not None
        assert self.ccd is not None
        assert self.gaiaid is not None
        sec = str(self.sector).zfill(4)
        gid = str(self.gaiaid).zfill(22)
        fp = (base + f"s{sec}/cam{self.cam}_ccd{self.ccd}" +
              f"/hlsp_cdips_tess_ffi_gaiatwo{gid}-" +
              f"{sec}-cam{self.cam}-ccd{self.ccd}" + f"_tess_v01_llc.fits")
        return fp

    def get_cdips_fits(self):
        """get cdips target and light curve header and data
        """
        fp = self.get_cdips_url()
        try:
            hdulist = fits.open(fp)
            if self.verbose:
                print(hdulist.info())
            lc_data = hdulist[1].data
            lc_header = hdulist[1].header

            # set
            self.fits_url = fp
            self.hdulist = hdulist
            self.header0 = hdulist[0].header
            self.catalog_ref = self.header0["CDIPSREF"]
            self.catalog_gaiaids = self.header0["CDEXTCAT"]
            if self.verbose:
                print(self.header0[20:38])
                print(self.header0[-45:-25])
            return lc_data, lc_header

        except Exception:
            msg = f"File not found:\n{fp}\n"
            # msg += f"Using sector={self.sector} in {self.all_sectors}.\n"
            raise ValueError(msg)

    def validate_target_header(self):
        """
        see self.header0[20:38], [-45:-25] and self.header0['CDIPSREF']
        for useful target information
        """
        raise NotImplementedError()

    def get_cdips_lc(self, lc_type=None, aper_idx=None, sort=True):
        """
        Parameters
        ----------
        lc_type : str
            lightcurve type: [flux,tfa,pca,mag]
        aper_idx : int
            aperture [1,2,3] are [1,1.5,2.25] pix in radius
        normalize
        """
        aper = aper_idx if aper_idx is not None else self.aper_idx
        lctype = lc_type if lc_type is not None else self.lctype

        if lctype == "mag":
            # magnitude
            typstr1 = "IRM"
            typstr2 = "IRE"
        elif lctype == "tfa":
            # detrended light curve found by applying TFA with a fixed number of template stars
            typstr1 = "TFA"
            typstr2 = "IRE"
        elif lctype == "pca":
            # detrended light curve that regresses against the number of
            # principal components noted in the light curve's header
            typstr1 = "PCA"
            typstr2 = "IRE"
        else:
            # instrumental flux measured from differenced images
            typstr1 = "IFL"
            typstr2 = "IFE"
        time = self.data["TMID_BJD"]  # exposure mid-time at
        flux = self.data[f"{typstr1}{str(aper)}"]
        err = self.data[f"{typstr2}{str(aper)}"]
        if sort:
            idx = np.argsort(time)
        else:
            idx = np.ones_like(time, bool)
        # hack tess lightkurve
        return _TessLightCurve(
            time=time[idx],
            flux=flux[idx],
            flux_err=err[idx],
            # FIXME: only day works when using lc.to_periodogram()
            time_format="jd",  # TIMEUNIT is bjd in fits header
            time_scale="tdb",  # TIMESYS in fits header
            centroid_col=None,
            centroid_row=None,
            quality=None,
            quality_bitmask=self.quality_bitmask,
            cadenceno=None,
            sector=self.sector,
            camera=self.cam,
            ccd=self.ccd,
            targetid=self.toi_params["TIC ID"]
            if self.toi_params is not None else self.ticid,
            ra=self.target_coord.ra.deg,
            dec=self.target_coord.dec.deg,
            label=None,
            meta=None,
        ).normalize()

    def get_aper_mask_cdips(self, sap_mask="round"):
        """
        This is an estimate of CDIPS aperture since
        self.hdulist[1].data.names does not contain aperture
        """
        print(
            "CDIPS has no aperture info in fits. Estimating aperture instead.")
        # first download tpf cutout
        self.ffi_cutout = FFI_cutout(
            sector=self.sector,
            gaiaDR2id=self.gaiaid,
            toiid=self.toiid,
            ticid=self.ticid,
            search_radius=self.search_radius,
            quality_bitmask=self.quality_bitmask,
        )
        tpf = self.ffi_cutout.get_tpf_tesscut()
        idx = int(self.aper_idx) - 1  #
        aper_mask = parse_aperture_mask(tpf,
                                        sap_mask=sap_mask,
                                        aper_radius=CDIPS_APER_PIX[idx])
        return aper_mask

    def plot_all_lcs(self):
        """
        """
        cdips_lcs = {}
        fig, ax = pl.subplots(1, 1, figsize=(10, 6))
        for aper in [1, 2, 3]:
            lc = self.get_cdips_lc(aper_idx=aper)
            lc.plot(ax=ax, label=f"aper={aper}")
            cdips_lcs[aper] = lc
        ax.set_title(f"{self.target_name} (sector {self.sector})")
        return fig

    def get_flat_lc(
        self,
        lc,
        period=None,
        epoch=None,
        duration=None,
        window_length=None,
        method="biweight",
        sigma_upper=None,
        sigma_lower=None,
        return_trend=False,
    ):
        """
        """
        if duration < 1:
            print("Duration should be in hours.")
        if window_length is None:
            window_length = 0.5 if duration is None else duration / 24 * 3
        if self.verbose:
            print(
                f"Using {method} filter with window_length={window_length:.2f} day"
            )
        if (period is not None) & (epoch is not None) & (duration is not None):
            tmask = get_transit_mask(lc,
                                     period=period,
                                     epoch=epoch,
                                     duration_hours=duration)
        else:
            tmask = np.zeros_like(lc.time, dtype=bool)
        # dummy holder
        flat, trend = lc.flatten(return_trend=True)
        # flatten using wotan
        wflat, wtrend = flatten(
            lc.time,
            lc.flux,
            method=method,
            window_length=window_length,
            mask=tmask,
            return_trend=True,
        )
        # overwrite
        flat.flux = wflat
        trend.flux = wtrend
        # clean lc
        sigma_upper = 5 if sigma_upper is None else sigma_upper
        sigma_lower = 10 if sigma_lower is None else sigma_lower
        flat = flat.remove_nans().remove_outliers(sigma_upper=sigma_upper,
                                                  sigma_lower=sigma_lower)
        if return_trend:
            return flat, trend
        else:
            return flat

    def plot_trend_flat_lcs(self,
                            lc,
                            period,
                            epoch,
                            duration,
                            binsize=10,
                            **kwargs):
        """
        plot trend and falt lightcurves (uses TOI ephemeris by default)
        """
        if duration < 1:
            print("Duration should be in hours.")
        assert ((period is not None) & (epoch is not None) &
                (duration is not None))
        if self.verbose:
            print(
                f"Using period={period:.4f} d, epoch={epoch:.2f} BTJD, duration={duration:.2f} hr"
            )
        fig, axs = pl.subplots(2,
                               1,
                               figsize=(12, 10),
                               constrained_layout=True,
                               sharex=True)

        if (period is not None) & (epoch is not None) & (duration is not None):
            tmask = get_transit_mask(lc,
                                     period=period,
                                     epoch=epoch,
                                     duration_hours=duration)
        else:
            tmask = np.zeros_like(lc.time, dtype=bool)
        ax = axs.flatten()
        flat, trend = self.get_flat_lc(lc,
                                       period=period,
                                       duration=duration,
                                       return_trend=True,
                                       **kwargs)
        lc[tmask].scatter(ax=ax[0], c="r", zorder=5, label="transit")
        if np.any(tmask):
            lc[~tmask].scatter(ax=ax[0], c="k", alpha=0.5, label="_nolegend_")
        ax[0].set_title(f"{self.target_name} (sector {lc.sector})")
        ax[0].set_xlabel("")
        trend.plot(ax=ax[0], c="b", lw=2, label="trend")

        if (period is not None) & (epoch is not None) & (duration is not None):
            tmask2 = get_transit_mask(flat,
                                      period=period,
                                      epoch=epoch,
                                      duration_hours=duration)
        else:
            tmask2 = np.zeros_like(lc.time, dtype=bool)
        flat.scatter(ax=ax[1], c="k", alpha=0.5, label="flat")
        if np.any(tmask2):
            flat[tmask2].scatter(ax=ax[1],
                                 zorder=5,
                                 c="r",
                                 s=10,
                                 label="transit")
        flat.bin(binsize).scatter(ax=ax[1],
                                  s=10,
                                  c="C1",
                                  label=f"bin ({binsize})")
        fig.subplots_adjust(hspace=0)
        return fig

    def run_tls(self, flat, plot=True, **tls_kwargs):
        """
        """
        tls = transitleastsquares(t=flat.time, y=flat.flux, dy=flat.flux_err)
        tls_results = tls.power(**tls_kwargs)
        self.tls_results = tls_results
        if plot:
            fig = plot_tls(tls_results)
            fig.axes[0].set_title(f"{self.target_name} (sector {flat.sector})")
            return fig

    def plot_fold_lc(self,
                     flat,
                     period,
                     epoch,
                     duration=None,
                     binsize=10,
                     ax=None):
        """
        plot folded lightcurve (uses TOI ephemeris by default)
        """
        if ax is None:
            fig, ax = pl.subplots(figsize=(12, 8))
        errmsg = "Provide period and epoch."
        assert (period is not None) & (epoch is not None), errmsg
        fold = flat.fold(period=period, t0=epoch)
        fold.scatter(ax=ax, c="k", alpha=0.5, label="folded")
        fold.bin(binsize).scatter(ax=ax,
                                  s=20,
                                  c="C1",
                                  label=f"bin ({binsize})")
        if duration is None:
            if self.tls_results is not None:
                duration = self.tls_results.duration
        if duration is not None:
            xlim = 3 * duration / period
            ax.set_xlim(-xlim, xlim)
        ax.set_title(f"{self.target_name} (sector {flat.sector})")
        return ax

    def plot_odd_even(self, flat, period=None, epoch=None, ylim=None):
        """
        """
        period = self.toi_period if period is None else period
        epoch = self.toi_epoch - TESS_TIME_OFFSET if epoch is None else epoch
        if (period is None) or (epoch is None):
            if self.tls_results is None:
                print("Running TLS")
                _ = self.run_tls(flat, plot=False)
            period = self.tls_results.period
            epoch = self.tls_results.T0
            ylim = self.tls_results.depth if ylim is None else ylim
        if ylim is None:
            ylim = 1 - self.toi_depth
        fig = plot_odd_even(flat, period=period, epoch=epoch, yline=ylim)
        fig.suptitle(f"{self.target_name} (sector {flat.sector})")
        return fig

    def get_transit_mask(self, lc, period, epoch, duration_hours):
        """
        """
        tmask = get_transit_mask(lc,
                                 period=period,
                                 epoch=epoch,
                                 duration_hours=duration_hours)
        return tmask