def _post_initialisation(self):
        self.t0 = 2458491.8771169
        self.period = 1.2652328
        self.k2 = 0.3**2
        self.a = 10.
        self.phase = fold(self.timea, self.period, self.t0)
        self.em = UniformModel()
        self.em.set_data(self.timea)

        self._mec = self.em.evaluate(sqrt(
            self.k2), self.t0 + 0.5 * self.period, self.period, self.a,
                                     0.5 * pi) - 1
        self._mec = 1 + self._mec / self._mec.ptp()

        phi = 2 * pi * self.phase
        self.alpha = a = abs(phi - pi)
        self._rff = (sin(a) +
                     (pi - a) * cos(a)) / pi * self._mec  # Reflected light
        self._dbf = sin(phi)  # Doppler boosting
        self._evf = -cos(2 * phi)  # Ellipsoidal variations

        self._cwl = 1e-9 * tess.wl
        self._ctm = tess.tm

        self.set_prior('ms', 'NP', star_m.n, star_m.s)
        self.set_prior('teffh', 'NP', 3300, 100)
        self.set_prior('teffc', 'UP', 100, 3300)
        self.set_prior('gp_log10_wn', 'NP', -1.74, 0.15)
Esempio n. 2
0
    def plot_folded_planets(self, bwidth: float = 10):
        fig, axs = subplots(1, self.nplanets, figsize=(13, 6), sharey=True)
        pv = self.de.minimum_location.copy()

        for ipl in range(self.nplanets):
            planets = set(arange(self.nplanets))
            planets.remove(ipl)
            mflux = self.transit_model(pv, planets=[ipl])
            rflux = self.transit_model(pv, planets=planets)
            oflux = self.ofluxa / rflux
            phase = (fold(self.timea, pv[3 + 4 * ipl], pv[2 + 4 * ipl], 0.5) -
                     0.5) * pv[3 + 4 * ipl]
            m = abs(phase) < 0.5 * self.bldur
            sids = argsort(phase[m])
            phase, mflux, oflux = phase[m][sids], mflux[m][sids], oflux[m][
                sids]
            bp, bf, be = downsample_time(phase, oflux, bwidth / 24 / 60)
            axs[ipl].errorbar(bp, bf, be, fmt='ok')
            axs[ipl].plot(phase, oflux, '.', alpha=0.25)
            axs[ipl].plot(phase, mflux, 'k')

        setp(axs,
             xlim=(-0.5 * self.bldur, 0.5 * self.bldur),
             ylim=(0.996, 1.004))
        fig.tight_layout()
        return fig
def read_eleanor(zero_epoch,
                 period,
                 bjdrefi,
                 mask_transits=False,
                 trwidth=0.053,
                 contamination=None):
    dd = root / 'photometry'
    files = sorted(dd.glob('hlsp_eleanor*.fits'))
    times, fluxes, wnids, lcids = [], [], [], []

    s = 0
    for i, f in enumerate(files):
        df = Table.read(f)
        time = df['TIME'].data + bjdrefi
        flux = df['CORR_FLUX'].data
        m = isfinite(time) & isfinite(flux)
        if mask_transits:
            p = (fold(time, period, zero_epoch, 0.5) - 0.5) * period
            m &= (abs(p) > 0.5 * trwidth)
        flux /= nanmedian(flux)
        m &= create_eleanor_sector_mask(flux, i)

        if contamination is not None:
            flux = decontaminate(flux, contamination[i])

        if m.sum() > 0:
            s += 1
            times.append(time[m])
            fluxes.append(flux[m])
            lcids.append(full(time[m].size, i, 'int'))
    wns = [diff(f).std() / sqrt(2) for f in fluxes]
    pbs = s * ['TESS']
    return times, fluxes, pbs, wns, pbs, list(arange(len(times)))
Esempio n. 4
0
    def plot_folded_tess_transit(self, method='de', pv=None, figsize=None, ylim=None):
        assert method in ('de', 'mc')
        if pv is None:
            if method == 'de':
                pv = self.de.minimum_location
            else:
                df = self.posterior_samples(derived_parameters=False)
                pv = df.median.values

        etess = self._ntess
        t = self.timea[:etess]
        fo = self.ofluxa[:etess]
        fm = squeeze(self.transit_model(self.de.population))[self.de.minimum_index, :etess]
        bl = squeeze(self.baseline(self.de.population))[self.de.minimum_index, :etess]

        fig, ax = subplots(figsize=figsize)
        phase = pv[1] * (fold(t, pv[1], pv[0], 0.5) - 0.5)
        sids = argsort(phase)
        phase = phase[sids]
        bp, bf, be = downsample_time(phase, (fo / bl)[sids], 4 / 24 / 60)
        ax.plot(phase, (fo / bl)[sids], 'k.', alpha=0.2)
        ax.errorbar(bp, bf, be, fmt='ko')
        ax.plot(phase, fm[sids], 'k')
        setp(ax, ylim=ylim, xlabel='Time', ylabel='Normalised flux')
        return fig
Esempio n. 5
0
    def __call__(self):
        self.logger = getLogger(
            f"{self.name}:{self.ts.name.lower().replace('_','-')}")
        self.logger.info(
            "Testing for a significant non-transit-like periodic variability")

        self.time_before = self.ts.time
        self.flux_before = self.ts.flux
        self.time_after = self.ts.time

        phase = fold(self.time_before, self.ts.ls.period)
        bp, bf = running_median(phase, self.flux_before, 0.05, 100)
        pv = interp1d(bp, bf)(phase)

        df = abs(diff(pv) / diff(self.time_before))
        ps = percentile(df, [80, 99.5])

        pv_amplitude = bf.ptp()
        bfn = -bf
        bfn -= bfn.min() - 1e-12
        bfn /= bfn.sum()
        pv_entropy = -(bfn * log(bfn)).sum()

        max_slope = df.max()
        slope_percentile_ratio = ps[0] / ps[1]

        self._bp = bp
        self._bf = bf

        self.phase = phase * self.ts.ls.period
        self.model = pv
        self.spr = slope_percentile_ratio
        self.amplitude = pv_amplitude
        self.entropy = pv_entropy
        self.max_slope = max_slope
        self.peak_siginifance = dip_significance(24 * self.ts.ls.period * bp,
                                                 bf)

        self.is_sigificant = pv_amplitude > 0.001
        self.is_transit_like = (slope_percentile_ratio < 0.1
                                or self.peak_siginifance > 3.0)

        if self.is_sigificant and not self.is_transit_like:
            self.logger.info("Found and removed a periodic signal")
            self.model_removed = True
            flux = self.ts.flux - self.model + 1
            self.flux_after = flux
            self.ts.update_data('pvarstep', self.ts.time, flux, self.ts.ferr)
        else:
            self.flux_after = self.ts.flux
            self.model_removed = False
            if self.is_sigificant:
                self.logger.info(
                    "Found a periodic signal but it's too transit-like to be removed"
                )
            else:
                self.logger.info(
                    "Didn't find any significant periodic signals")
Esempio n. 6
0
    def plot_folded_tess_transit(self,
                                 method: str = 'de',
                                 pv: ndarray = None,
                                 binwidth: float = 1,
                                 plot_model: bool = True,
                                 plot_unbinned: bool = True,
                                 plot_binned: bool = True,
                                 xlim: tuple = None,
                                 ylim: tuple = None,
                                 ax=None,
                                 figsize: tuple = None):
        assert method in ('de', 'mc')
        if pv is None:
            if method == 'de':
                pv = self.de.minimum_location
            else:
                df = self.posterior_samples(derived_parameters=False)
                pv = df.median().values

        if ax is None:
            fig, ax = subplots(figsize=figsize)
        else:
            fig, ax = None, ax

        ax.autoscale(enable=True, axis='x', tight=True)

        etess = self._ntess
        t = self.timea[:etess]
        fo = self.ofluxa[:etess]
        fm = squeeze(self.transit_model(pv))[:etess]
        bl = squeeze(self.baseline(pv))[:etess]

        phase = 24 * pv[1] * (fold(t, pv[1], pv[0], 0.5) - 0.5)
        sids = argsort(phase)
        phase = phase[sids]
        bp, bf, be = downsample_time(phase, (fo / bl)[sids], binwidth / 60)
        if plot_unbinned:
            ax.plot(phase, (fo / bl)[sids], 'k.', alpha=0.1, ms=2)
        if plot_binned:
            ax.errorbar(bp, bf, be, fmt='ko', ms=3)
        if plot_model:
            ax.plot(phase, fm[sids], 'k')
        setp(ax,
             ylim=ylim,
             xlim=xlim,
             xlabel='Time - T$_c$ [h]',
             ylabel='Normalised flux')

        if fig is not None:
            fig.tight_layout()

        return fig
 def __init__(self, name: str):
     times, fluxes, _, _, _, _ = read_tess(tess_file,
                                           zero_epoch,
                                           period,
                                           use_pdc=True,
                                           baseline_duration_d=period)
     phase, time, flux = [], [], []
     for t, f in zip(times, fluxes):
         ph = (fold(t, period.n, zero_epoch.n, 0.5) - 0.5) * period.n
         mask = abs(ph) > 0.03
         phase.append(ph[mask])
         time.append(t[mask])
         flux.append(f[mask])
     super().__init__(name, ['TESS'],
                      time,
                      flux,
                      wnids=arange(len(flux)),
                      lnlikelihood='celerite')
Esempio n. 8
0
    def plot_folded_tess_transit(self, solution: str = 'de', pv: ndarray = None, binwidth: float = 1,
                                 plot_model: bool = True, plot_unbinned: bool = True, plot_binned: bool = True,
                                 xlim: tuple = None, ylim: tuple = None, ax=None, figsize: tuple = None):

        if pv is None:
            if solution.lower() == 'local':
                pv = self._local_minimization.x
            elif solution.lower() in ('de', 'global'):
                pv = self.de.minimum_location
            elif solution.lower() in ('mcmc', 'mc'):
                pv = self.posterior_samples().median().values
            else:
                raise NotImplementedError("'solution' should be either 'local', 'global', or 'mcmc'")

        if ax is None:
            fig, ax = subplots(figsize=figsize)
        else:
            fig, ax = None, ax

        ax.autoscale(enable=True, axis='x', tight=True)

        etess = self._ntess
        t = self.timea[:etess]
        fo = self.ofluxa[:etess]
        fm = squeeze(self.transit_model(pv))[:etess]
        bl = squeeze(self.baseline(pv))[:etess]

        phase = 24 * pv[1] * (fold(t, pv[1], pv[0], 0.5) - 0.5)
        sids = argsort(phase)
        phase = phase[sids]
        bp, bf, be = downsample_time(phase, (fo / bl)[sids], binwidth / 60)
        if plot_unbinned:
            ax.plot(phase, (fo / bl)[sids], 'k.', alpha=0.1, ms=2)
        if plot_binned:
            ax.errorbar(bp, bf, be, fmt='ko', ms=3)
        if plot_model:
            ax.plot(phase, fm[sids], 'k')
        setp(ax, ylim=ylim, xlim=xlim, xlabel='Time - T$_c$ [h]', ylabel='Normalised flux')

        if fig is not None:
            fig.tight_layout()
        return fig
Esempio n. 9
0
    def plot_rv_vs_phase(self,
                         planet: int,
                         method='de',
                         pv=None,
                         nsamples: int = 200,
                         ntimes: int = 500,
                         axs=None):
        if axs is None:
            fig, axs = subplots(2,
                                1,
                                gridspec_kw={'height_ratios': (3, 1)},
                                sharex='all')
        else:
            fig, axs = None, axs

        if pv is None:
            if method == 'de':
                if self.lpf.de is None:
                    raise ValueError(
                        "The global optimizer hasn't been initialized.")
                pvp = None
                pv = self.lpf.de.minimum_location
            elif method == 'mcmc':
                if self.lpf.sampler is None:
                    raise ValueError("The sampler hasn't been initialized.")
                df = self.lpf.posterior_samples()
                pvp = permutation(df.values)[:nsamples, :]
                pv = median(pvp, 0)
        else:
            if pv.ndim == 1:
                pvp = None
                pv = pv
            else:
                pvp = permutation(pv)[:nsamples, :]
                pv = median(pvp, 0)

        rv_time = linspace(self._timea.min() - 1,
                           self._timea.max() + 1,
                           num=ntimes)

        all_planets = set(range(self.nplanets))
        other_planets = all_planets.difference([planet])

        if pvp is None:
            rv_model = self.rv_model(pv,
                                     rv_time + self._tref, [planet],
                                     add_sv=False)
            rv_others = self.rv_model(pv, planets=other_planets, add_sv=False)
            rv_model_limits = None
        else:
            rv_percentiles = percentile(
                self.rv_model(pvp,
                              rv_time + self._tref, [planet],
                              add_sv=False), [50, 16, 84, 2.5, 97.5], 0)
            rv_model = rv_percentiles[0]
            rv_model_limits = rv_percentiles[1:]
            rv_others = median(
                self.rv_model(pvp, planets=other_planets, add_sv=False), 0)

        period = pv[self.ps.names.index(f'p_{planet + 1}')]
        tc = pv[self.ps.names.index(f'tc_{planet + 1}')] - self._tref

        phase = (fold(self._timea, period, tc, 0.5) - 0.5) * period
        phase_model = (fold(rv_time, period, tc, 0.5) - 0.5) * period
        msids = argsort(phase_model)

        if pvp is not None:
            axs[0].fill_between(phase_model[msids],
                                rv_model_limits[2, msids],
                                rv_model_limits[3, msids],
                                facecolor='blue',
                                alpha=0.15)
            axs[0].fill_between(phase_model[msids],
                                rv_model_limits[0, msids],
                                rv_model_limits[1, msids],
                                facecolor='darkblue',
                                alpha=0.25)

        axs[0].errorbar(phase,
                        self._rva - rv_others - self.rv_shifts(pv),
                        self._rvea,
                        fmt='ok')
        axs[0].plot(phase_model[msids], rv_model[msids], 'k')
        axs[1].errorbar(phase,
                        self._rva - self.rv_model(pv),
                        self._rvea,
                        fmt='ok')

        setp(axs[0], ylabel='RV [m/s]')
        setp(axs[1], xlabel='Phase [d]', ylabel='O-M [m/s]')

        axs[0].autoscale(axis='x', tight=True)
        if fig is not None:
            fig.tight_layout()
        return fig
Esempio n. 10
0
 def phase(self) -> Optional[ndarray]:
     if self.zero_epoch is not None:
         return fold(self.time, self.period, self.zero_epoch, 0.5) * self.period
     else:
         return None
Esempio n. 11
0
    def __call__(self, npop: int = 40, de_niter: int = 1000, mcmc_niter: int = 200, mcmc_repeats: int = 3, initialize_only: bool = False):
        self.logger = getLogger(f"{self.name}:{self.ts.name.lower().replace('_','-')}")
        self.logger.info(f"Fitting {self.mode} transits")
        self.ts.transit_fits[self.mode] = self

        epochs = epoch(self.ts.time, self.ts.zero_epoch, self.ts.period)

        if self.mode == 'all':
            mask = ones(self.ts.time.size, bool)
        elif self.mode == 'even':
            mask = epochs % 2 == 0
        elif self.mode == 'odd':
            mask = epochs % 2 == 1
        else:
            raise NotImplementedError

        mask &= abs(self.ts.phase - 0.5*self.ts.period) < 4 * 0.5 * self.ts.duration

        self.ts.transit_fit_masks[self.mode] = self.mask = mask

        self.epochs = epochs = epochs[mask]
        self.time = self.ts.time[mask]
        self.fobs = self.ts.flux[mask]

        tref = floor(self.time.min())
        tm = QuadraticModelCL(klims=(0.01, 0.60)) if self.use_opencl else QuadraticModel(interpolate=False)
        self.lpf = lpf = SearchLPF(times=self.time, fluxes=self.fobs, epochs=epochs, tm=tm,
                        nsamples=self.nsamples, exptimes=self.exptime, tref=tref)

        # TODO: V-shaped transits are not always modelled well. Need to set smarter priors (or starting population)
        #       for the impact parameter and stellar density.
        lpf.set_prior('rho', 'UP', 0.01, 25)
        if self.mode == 'all':
            d  = min(self.ts.depth, 0.75)
            lpf.set_prior('tc', 'NP', self.ts.zero_epoch, 0.01)
            lpf.set_prior('p',  'NP', self.ts.period, 0.001)
            lpf.set_prior('k2', 'UP', max(0.01**2, 0.5*d), min(max(0.08**2, 4*d), 0.75**2))
        else:
            pr = self.ts.tf_all.parameters
            lpf.set_prior('tc', 'NP', pr.tc.med, 5*pr.tc.err)
            lpf.set_prior('p',  'NP', pr.p.med, pr.p.err)
            lpf.set_prior('k2', 'UP', max(0.01**2, 0.5 * pr.k2.med), max(0.08**2, min(0.6**2, 2 * pr.k2.med)))
            lpf.set_prior('q1', 'NP', pr.q1.med, pr.q1.err)
            lpf.set_prior('q2', 'NP', pr.q2.med, pr.q2.err)

        # TODO: The limb darkening table has been computed for TESS. Needs to be made flexible.
        if self.ts.teff is not None:
            ldcs = Table.read(Path(__file__).parent / "data/ldc_table.fits").to_pandas()
            ip = interp1d(ldcs.teff, ldcs[['q1', 'q2']].T)
            q1, q2 = ip(clip(self.ts.teff, 2000., 12000.))
            lpf.set_prior('q1', 'NP', q1, 1e-5)
            lpf.set_prior('q2', 'NP', q2, 1e-5)

        if initialize_only:
            return
        else:
            lpf.optimize_global(niter=de_niter, npop=npop, use_tqdm=self.use_tqdm, plot_convergence=False)
            lpf.sample_mcmc(mcmc_niter, repeats=mcmc_repeats, use_tqdm=self.use_tqdm, leave=False)
            df = lpf.posterior_samples(derived_parameters=True)
            df = pd.DataFrame((df.median(), df.std()), index='med err'.split())
            pv = lpf.posterior_samples(derived_parameters=False).median().values
            self.phase = fold(self.time, pv[1], pv[0], 0.5) * pv[1] - 0.5 * pv[1]
            self.fmod = lpf.flux_model(pv)
            self.ftra = lpf.transit_model(pv)
            self.fbase = lpf.baseline(pv)

            # Calculate the per-orbit log likelihood differences
            # --------------------------------------------------
            ues = unique(epochs)
            lnl = zeros(ues.size)
            err = 10 ** pv[7]

            def lnlike_normal(o, m, e):
                npt = o.size
                return -npt * log(e) - 0.5 * npt * log(2. * pi) - 0.5 * sum((o - m) ** 2 / e ** 2)

            for i, e in enumerate(ues):
                m = epochs == e
                lnl[i] = lnlike_normal(self.fobs[m], self.fmod[m], err) - lnlike_normal(self.fobs[m], 1.0, err)

            self.parameters = df
            self.dll_epochs = ues
            self.dll_values = lnl

            self.zero_epoch = df.tc.med
            self.period = df.p.med
            self.duration = df.t14.med
            self.depth = df.k2.med

            if self.mode == 'all':
                self.delta_bic = self.ts.dbic = delta_bic(lnl.sum(), 0, 9, self.time.size)
                self.ts.update_ephemeris(self.zero_epoch, self.period, self.duration, self.depth)
Esempio n. 12
0
    def plot_folded_planets(self,
                            passband: str,
                            method: str = 'de',
                            bwidth: float = 10,
                            axs=None,
                            pv: ndarray = None,
                            nsamples: int = 100,
                            limp=(2.5, 97.5, 16, 84),
                            limc: str = 'darkblue',
                            lima: float = 0.15,
                            ylines=None):
        from pytransit.lpf.tesslpf import downsample_time

        if axs is None:
            fig, axs = subplots(1, self.nplanets, sharey='all')
        else:
            fig, axs = None, axs

        if pv is None:
            if method == 'de':
                if self.de is None:
                    raise ValueError(
                        "The global optimizer hasn't been initialized.")
                pvp = None
                pv = self.de.minimum_location
            elif method == 'mcmc':
                if self.sampler is None:
                    raise ValueError("The sampler hasn't been initialized.")
                df = self.posterior_samples(derived_parameters=False)
                pvp = permutation(df.values)[:nsamples, :]
                pv = median(pvp, 0)
        else:
            if pv.ndim == 1:
                pvp = None
                pv = pv
            else:
                pvp = permutation(pv)[:nsamples, :]
                pv = median(pvp, 0)

        is_pb = self.pbids == self.passbands.index(passband)
        pbmask = zeros(self.timea.size, 'bool')
        for sl, cpb in zip(self.lcslices, is_pb):
            if cpb:
                pbmask[sl] = 1

        tcids = [
            self.ps.names.index(f'tc_{i + 1}') for i in range(self.nplanets)
        ]
        prids = [
            self.ps.names.index(f'p_{i + 1}') for i in range(self.nplanets)
        ]
        t0s = pv[tcids]
        prs = pv[prids]

        for ipl in range(self.nplanets):
            planets = set(arange(self.nplanets))
            planets.remove(ipl)
            if pvp is None:
                mflux = squeeze(self.transit_model(pv, planets=[ipl]))[pbmask]
                rflux = squeeze(self.transit_model(pv,
                                                   planets=planets))[pbmask]
                mflim = None
                fbline = self.baseline(pv)[pbmask]
            else:
                mfluxes = self.transit_model(pvp, planets=[ipl])[:, pbmask]
                rfluxes = self.transit_model(pvp, planets=planets)[:, pbmask]
                fblines = self.baseline(pvp)[:, pbmask]
                mflux = median(mfluxes, 0)
                mflim = percentile(mfluxes, limp, 0)
                rflux = median(rfluxes, 0)
                fbline = median(fblines, 0)

            oflux = self.ofluxa[pbmask] / rflux / fbline
            phase = (fold(self.timea[pbmask], prs[ipl], t0s[ipl], 0.5) -
                     0.5) * prs[ipl]
            m = abs(phase) < 0.5 * self.bldur
            sids = argsort(phase[m])
            if m.sum() > 0:
                phase, mflux, oflux = phase[m][sids], mflux[m][sids], oflux[m][
                    sids]
                bp, bf, be = downsample_time(phase, oflux, bwidth / 24 / 60)
                if mflim is not None:
                    for il in range(mflim.shape[0] // 2):
                        axs[ipl].fill_between(phase,
                                              mflim[2 * il, m][sids],
                                              mflim[2 * il + 1, m][sids],
                                              fc=limc,
                                              alpha=lima)
                axs[ipl].errorbar(bp, bf, be, fmt='ok')
                axs[ipl].plot(phase, oflux, '.', alpha=0.25)
                axs[ipl].plot(phase, mflux, 'k')

                if ylines is not None:
                    axs[ipl].fill_between(phase, mflux, 1, fc='w', zorder=-99)
                    for yl in ylines:
                        axs[ipl].axhline(yl,
                                         lw=1,
                                         ls='--',
                                         c='k',
                                         alpha=0.5,
                                         zorder=-100)

        setp(axs,
             xlim=(-0.5 * self.bldur, 0.5 * self.bldur),
             ylim=(0.996, 1.004))
        if fig is not None:
            fig.tight_layout()
        return fig
Esempio n. 13
0
    def plot_gb_transits(self,
                         method='de',
                         pv: ndarray = None,
                         remove_baseline: bool = True,
                         figsize: tuple = (14, 2),
                         axes=None,
                         ncol: int = 4,
                         xlim: tuple = None,
                         ylim: tuple = None,
                         nsamples: int = 200):

        if pv is None:
            if method == 'de':
                if self.de is None:
                    raise ValueError(
                        "The global optimizer hasn't been initialized.")
                pvp = None
                pv = self.de.minimum_location
            elif method == 'mcmc':
                if self.sampler is None:
                    raise ValueError("The sampler hasn't been initialized.")
                df = self.posterior_samples(derived_parameters=False)
                pvp = permutation(df.values)[:nsamples, :]
                pv = median(pvp, 0)
        else:
            if pv.ndim == 1:
                pvp = None
                pv = pv
            else:
                pvp = permutation(pv)[:nsamples, :]
                pv = median(pvp, 0)

        if pvp is None:
            if remove_baseline:
                fobs = self.ofluxa / squeeze(self.baseline(pv))
                fmodel = squeeze(self.transit_model(pv))
                fbasel = ones_like(self.ofluxa)
            else:
                fobs = self.ofluxa
                fmodel = squeeze(self.flux_model(pv))
                fbasel = squeeze(self.baseline(pv))
            fmodel_limits = None
        else:
            if remove_baseline:
                fobs = self.ofluxa / squeeze(self.baseline(pv))
                fmodels = percentile(self.transit_model(pvp),
                                     [50, 16, 84, 2.5, 97.5], 0)
                fbasel = ones_like(self.ofluxa)
            else:
                fobs = self.ofluxa
                fmodels = percentile(self.flux_model(pvp),
                                     [50, 16, 84, 2.5, 97.5], 0)
                fbasel = median(self.baseline(pvp), 0)
            fmodel = fmodels[0]
            fmodel_limits = fmodels[1:]

        tcids = [
            self.ps.names.index(f'tc_{i + 1}') for i in range(self.nplanets)
        ]
        prids = [
            self.ps.names.index(f'p_{i + 1}') for i in range(self.nplanets)
        ]

        t0s = pv[tcids]
        prs = pv[prids]

        tcs = array([t.mean() for t in self.times[self._stess:]])
        tds = array([
            abs(fold(tcs, prs[i], t0s[i], 0.5) - 0.5)
            for i in range(self.nplanets)
        ])
        pids = argmin(tds, 0)

        nlc = self.nlc - self._stess
        nrow = int(ceil(nlc / ncol))

        if axes is None:
            fig, axs = subplots(nrow,
                                ncol,
                                figsize=figsize,
                                sharex='all',
                                sharey='all',
                                squeeze=False)
        else:
            fig, axs = None, axes

        [ax.autoscale(enable=True, axis='x', tight=True) for ax in axs.flat]

        etess = self._stess
        for iax, i in enumerate(range(self.nlc - etess)):
            ax = axs.flat[iax]
            sl = self.lcslices[etess + i]
            t = self.times[etess + i]
            e = epoch(t.mean(), t0s[pids[i]], prs[pids[i]])
            tc = t0s[pids[i]] + e * prs[pids[i]]
            tt = 24 * (t - tc)

            if fmodel_limits is not None:
                ax.fill_between(tt,
                                fmodel_limits[2, sl],
                                fmodel_limits[3, sl],
                                facecolor='blue',
                                alpha=0.15)
                ax.fill_between(tt,
                                fmodel_limits[0, sl],
                                fmodel_limits[1, sl],
                                facecolor='darkblue',
                                alpha=0.25)
            ax.plot(tt, fobs[sl], 'k.', alpha=0.2)
            ax.plot(tt, fmodel[sl], 'k')

        setp(axs, xlim=xlim, ylim=ylim)
        setp(axs[-1, :], xlabel='Time - T$_c$ [h]')
        setp(axs[:, 0], ylabel='Normalised flux')
        fig.tight_layout()
        return fig