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
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
def get_aper_mask( self, sector=None, sap_mask=None, aper_radius=None, percentile=None, threshold_sigma=None, tpf_size=None, verbose=True, ): """ """ sector = sector if sector else self.sector sap_mask = sap_mask if sap_mask else self.sap_mask aper_radius = aper_radius if aper_radius else self.aper_radius percentile = percentile if percentile else self.percentile threshold_sigma = (threshold_sigma if threshold_sigma else self.threshold_sigma) cutout_size = tpf_size if tpf_size else self.cutout_size tpf = self.get_tpf_tesscut(sector=sector, cutout_size=cutout_size) aper_mask = parse_aperture_mask( tpf, sap_mask=sap_mask, aper_radius=aper_radius, percentile=percentile, threshold_sigma=threshold_sigma, verbose=verbose, ) self.aper_mask = aper_mask return aper_mask
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_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
def get_aper_mask( self, sector=None, sap_mask=None, aper_radius=None, percentile=None, threshold_sigma=None, verbose=True, ): """ """ sector = sector if sector else self.sector sap_mask = sap_mask if sap_mask else self.sap_mask aper_radius = aper_radius if aper_radius else self.aper_radius percentile = percentile if percentile else self.percentile threshold_sigma = (threshold_sigma if threshold_sigma else self.threshold_sigma) if self.tpf is None: tpf, tpf_info = self.get_tpf(sector=sector, return_df=True) else: if self.tpf.sector == sector: tpf = self.tpf else: tpf, tpf_info = self.get_tpf(sector=sector, return_df=True) aper_mask = parse_aperture_mask( tpf, sap_mask=sap_mask, aper_radius=aper_radius, percentile=percentile, threshold_sigma=threshold_sigma, verbose=verbose, ) self.aper_mask = aper_mask return aper_mask
def make_custom_lc( self, sector=None, sap_mask=None, aper_radius=None, percentile=None, threshold_sigma=None, use_pld=True, pixel_components=3, spline_n_knots=100, spline_degree=3, background_mask=None, pca_nterms=5, with_offset=True, ): """ create a custom lightcurve with background subtraction, based on this tutorial: https://docs.lightkurve.org/tutorials/04-how-to-remove-tess-scattered-light-using-regressioncorrector.html Parameters ---------- sector : int or str specific sector or all aper_radius: int aperture mask radius percentile: float aperture mask percentile threshold_sigma: float aperture mask threshold [sigma] pca_nterms : int number of pca terms to use Returns ------- corrected_lc : lightkurve object """ if self.verbose: print("Using lightcurve with custom aperture.") sector = sector if sector is not None else self.sector sap_mask = sap_mask if sap_mask else self.sap_mask aper_radius = aper_radius if aper_radius else self.aper_radius percentile = percentile if percentile else self.percentile threshold_sigma = (threshold_sigma if threshold_sigma else self.threshold_sigma) if self.tpf is None: tpf, tpf_info = self.get_tpf(sector=sector, return_df=True) else: if self.tpf.sector == sector: tpf = self.tpf else: tpf, tpf_info = self.get_tpf(sector=sector, return_df=True) # Make an aperture mask and a raw light curve self.aper_mask = parse_aperture_mask( tpf, sap_mask=sap_mask, aper_radius=aper_radius, percentile=percentile, threshold_sigma=threshold_sigma, verbose=False, ) raw_lc = tpf.to_lightcurve(method="aperture", aperture_mask=self.aper_mask) # remove nans idx = (np.isnan(raw_lc.time) | np.isnan(raw_lc.flux) | np.isnan(raw_lc.flux_err)) self.tpf = tpf[~idx] self.raw_lc = raw_lc[~idx] if use_pld: if self.verbose: print("Removing scattered light + applying PLD") pld = lk.TessPLDCorrector(self.tpf, aperture_mask=self.aper_mask) if background_mask is None: background_mask = ~self.aper_mask corrected_lc = pld.correct( pixel_components=pixel_components, spline_n_knots=spline_n_knots, spline_degree=spline_degree, background_mask=background_mask, ) self.corrector = pld else: if self.verbose: print("Removing scattered light") # Make a design matrix and pass it to a linear regression corrector regressors = tpf.flux[~idx][:, ~self.aper_mask] dm = (lk.DesignMatrix( regressors, name="pixels").pca(pca_nterms).append_constant()) # Regression Corrector Object rc = lk.RegressionCorrector(self.raw_lc) self.corrector = rc corrected_lc = rc.correct(dm) # Optional: Remove the scattered light, allowing for the large offset from scattered light if with_offset: corrected_lc = (self.raw_lc - rc.model_lc + np.percentile(rc.model_lc.flux, q=5)) lc = corrected_lc.normalize() self.lc_custom = lc # compute Contamination if self.gaia_sources is None: gaia_sources = self.query_gaia_dr2_catalog(radius=120, verbose=False) else: gaia_sources = self.gaia_sources fluxes = get_fluxes_within_mask(self.tpf, self.aper_mask, gaia_sources) self.contratio = sum(fluxes) - 1 if self.tic_params is None: _ = self.query_tic_catalog(return_nearest_xmatch=True) tic_contratio = self.tic_params.contratio dcontratio = abs(tic_contratio - self.contratio) if (tic_contratio is not None) & (dcontratio > 0.5): print(f"contratio: {self.contratio:.2f} (TIC={tic_contratio:.2f})") # add method lc.detrend = lambda: detrend(lc) return lc
def make_custom_lc( self, sector=None, tpf_size=None, sap_mask=None, aper_radius=None, percentile=None, threshold_sigma=None, use_pld=True, pixel_components=3, spline_n_knots=100, spline_degree=3, background_mask=None, pca_nterms=5, with_offset=True, ): """ create a custom lightcurve based on this tutorial: https://docs.lightkurve.org/tutorials/04-how-to-remove-tess-scattered-light-using-regressioncorrector.html Parameters ---------- sector : int or str specific sector or all cutout_size : tuple tpf cutout size aper_radius: int aperture mask radius percentile: float aperture mask percentile threshold_sigma: float aperture mask threshold [sigma] method : float PLD (default) Returns ------- corrected_lc : lightkurve object """ if self.verbose: print("Using lightcurve with custom aperture.") sector = sector if sector is not None else self.sector sap_mask = sap_mask if sap_mask else self.sap_mask aper_radius = aper_radius if aper_radius else self.aper_radius percentile = percentile if percentile else self.percentile threshold_sigma = (threshold_sigma if threshold_sigma else self.threshold_sigma) cutout_size = tpf_size if tpf_size else self.cutout_size tpf_tesscut = self.get_tpf_tesscut(sector=sector, cutout_size=cutout_size) self.aper_mask = parse_aperture_mask( tpf_tesscut, sap_mask=sap_mask, aper_radius=aper_radius, percentile=percentile, threshold_sigma=threshold_sigma, verbose=False, ) raw_lc = tpf_tesscut.to_lightcurve(method="aperture", aperture_mask=self.aper_mask) # remove nans idx = (np.isnan(raw_lc.time) | np.isnan(raw_lc.flux) | np.isnan(raw_lc.flux_err)) self.tpf_tesscut = tpf_tesscut[~idx] self.lc_custom_raw = raw_lc[~idx] if use_pld: if self.verbose: print("Removing scattered light + applying PLD") pld = lk.TessPLDCorrector(self.tpf_tesscut, aperture_mask=self.aper_mask) if background_mask is None: background_mask = ~self.aper_mask corrected_lc = pld.correct( pixel_components=pixel_components, spline_n_knots=spline_n_knots, spline_degree=spline_degree, background_mask=background_mask, ) self.corrector = pld else: if self.verbose: print("Removing scattered light") # Make a design matrix and pass it to a linear regression corrector regressors = tpf_tesscut.flux[~idx][:, ~self.aper_mask] dm = (lk.DesignMatrix( regressors, name="regressors").pca(nterms=pca_nterms).append_constant()) rc = lk.RegressionCorrector(raw_lc) self.corrector = rc corrected_lc = rc.correct(dm) # Optional: Remove the scattered light, allowing for the large offset from scattered light if with_offset: corrected_lc = (raw_lc - rc.model_lc + np.percentile(rc.model_lc.flux, q=5)) lc = corrected_lc.normalize() self.lc_custom = lc # compute Contamination if self.gaia_sources is None: gaia_sources = self.query_gaia_dr2_catalog(radius=120) else: gaia_sources = self.gaia_sources fluxes = get_fluxes_within_mask(self.tpf_tesscut, self.aper_mask, gaia_sources) self.contratio = sum(fluxes) - 1 # add method lc.detrend = lambda: detrend(lc) return lc
def plot_gaia_sources_on_survey( tpf, target_gaiaid, gaia_sources=None, fov_rad=None, depth=0.0, kmax=1.0, sap_mask="pipeline", survey="DSS2 Red", ax=None, color_aper="C0", # pink figsize=None, invert_xaxis=False, invert_yaxis=False, pix_scale=TESS_pix_scale, verbose=True, **mask_kwargs, ): """Plot (superpose) Gaia sources on archival image Parameters ---------- target_coord : astropy.coordinates target coordinate gaia_sources : pd.DataFrame gaia sources table fov_rad : astropy.unit FOV radius survey : str image survey; see from astroquery.skyview import SkyView; SkyView.list_surveys() verbose : bool print texts ax : axis subplot axis color_aper : str aperture outline color (default=C6) kwargs : dict keyword arguments for aper_radius, percentile Returns ------- ax : axis subplot axis TODO: correct for proper motion difference between survey image and gaia DR2 positions """ if verbose: print("Plotting nearby gaia sources on survey image.") assert target_gaiaid is not None ny, nx = tpf.flux.shape[1:] if fov_rad is None: diag = np.sqrt(nx**2 + ny**2) fov_rad = (0.4 * diag * pix_scale).to(u.arcmin).round(0) target_coord = SkyCoord(ra=tpf.ra * u.deg, dec=tpf.dec * u.deg) if gaia_sources is None: print( "Querying Gaia sometimes hangs. Provide `gaia_sources` if you can." ) gaia_sources = Catalogs.query_region(target_coord, radius=fov_rad, catalog="Gaia", version=2).to_pandas() assert len(gaia_sources) > 1, "gaia_sources contains single entry" # make aperture mask mask = parse_aperture_mask(tpf, sap_mask=sap_mask, **mask_kwargs) maskhdr = tpf.hdu[2].header # make aperture mask outline contour = np.zeros((ny, nx)) contour[np.where(mask)] = 1 contour = np.lib.pad(contour, 1, PadWithZeros) highres = zoom(contour, 100, order=0, mode="nearest") extent = np.array([-1, nx, -1, ny]) if verbose: print( f"Querying {survey} ({fov_rad:.2f} x {fov_rad:.2f}) archival image" ) # -----------create figure---------------# if ax is None: # get img hdu for subplot projection try: hdu = SkyView.get_images( position=target_coord.icrs.to_string(), coordinates="icrs", survey=survey, radius=fov_rad, grid=False, )[0][0] except Exception: errmsg = "survey image not available" raise FileNotFoundError(errmsg) fig = pl.figure(figsize=figsize) # define scaling in projection ax = fig.add_subplot(111, projection=WCS(hdu.header)) # plot survey img if str(target_coord.distance) == "nan": target_coord = SkyCoord(ra=target_coord.ra, dec=target_coord.dec) nax, hdu = plot_finder_image(target_coord, ax=ax, fov_radius=fov_rad, survey=survey, reticle=False) imgwcs = WCS(hdu.header) mx, my = hdu.data.shape # plot mask _ = ax.contour( highres, levels=[0.5], extent=extent, origin="lower", linewidths=[3], colors=color_aper, transform=ax.get_transform(WCS(maskhdr)), ) idx = gaia_sources["source_id"].astype(int).isin([target_gaiaid]) target_gmag = gaia_sources.loc[idx, "phot_g_mean_mag"].values[0] for index, row in gaia_sources.iterrows(): marker, s = "o", 100 r, d, mag, id = row[["ra", "dec", "phot_g_mean_mag", "source_id"]] pix = imgwcs.all_world2pix(np.c_[r, d], 1)[0] if int(id) != int(target_gaiaid): gamma = 1 + 10**(0.4 * (mag - target_gmag)) if depth > kmax / gamma: # too deep to have originated from secondary star edgecolor = "C1" alpha = 1 # 0.5 else: # possible NEBs edgecolor = "C3" alpha = 1 else: s = 200 edgecolor = "C2" marker = "s" alpha = 1 nax.scatter( pix[0], pix[1], marker=marker, s=s, edgecolor=edgecolor, alpha=alpha, facecolor="none", ) # orient such that north is up; left is east if invert_yaxis: # ax.invert_yaxis() raise NotImplementedError() if invert_xaxis: # ax.invert_xaxis() raise NotImplementedError() if hasattr(ax, "coords"): ax.coords[0].set_major_formatter("dd:mm") ax.coords[1].set_major_formatter("dd:mm") # set img limits pl.setp( nax, xlim=(0, mx), ylim=(0, my), title="{0} ({1:.2f}' x {1:.2f}')".format(survey, fov_rad.value), ) return ax
def plot_gaia_sources_on_tpf( tpf, target_gaiaid, gaia_sources=None, sap_mask="pipeline", depth=None, kmax=1, dmag_limit=8, fov_rad=None, cmap="viridis", figsize=None, ax=None, invert_xaxis=False, invert_yaxis=False, pix_scale=TESS_pix_scale, verbose=True, **mask_kwargs, ): """ plot gaia sources brighter than dmag_limit; only annotated with starids are those that are bright enough to cause reproduce the transit depth; starids are in increasing separation dmag_limit : float maximum delta mag to consider; computed based on depth if None TODO: correct for proper motion difference between survey image and gaia DR2 positions """ if verbose: print("Plotting nearby gaia sources on tpf.") assert target_gaiaid is not None img = np.nanmedian(tpf.flux, axis=0) # make aperture mask mask = parse_aperture_mask(tpf, sap_mask=sap_mask, **mask_kwargs) ax = plot_aperture_outline(img, mask=mask, imgwcs=tpf.wcs, figsize=figsize, cmap=cmap, ax=ax) if fov_rad is None: nx, ny = tpf.shape[1:] diag = np.sqrt(nx**2 + ny**2) fov_rad = (0.4 * diag * pix_scale).to(u.arcmin).round(0) if gaia_sources is None: print( "Querying Gaia sometimes hangs. Provide `gaia_sources` if you can." ) target_coord = SkyCoord(ra=tpf.header["RA_OBJ"], dec=tpf.header["DEC_OBJ"], unit="deg") gaia_sources = Catalogs.query_region(target_coord, radius=fov_rad, catalog="Gaia", version=2).to_pandas() assert len(gaia_sources) > 1, "gaia_sources contains single entry" # find sources within mask # target is assumed to be the first row idx = gaia_sources["source_id"].astype(int).isin([target_gaiaid]) target_gmag = gaia_sources.loc[idx, "phot_g_mean_mag"].values[0] # sources_inside_aperture = [] if depth is not None: # compute delta mag limit given transit depth dmag_limit = (np.log10(kmax / depth - 1) if dmag_limit is None else dmag_limit) # get min_gmag inside mask ra, dec = gaia_sources[["ra", "dec"]].values.T pix_coords = tpf.wcs.all_world2pix(np.c_[ra, dec], 0) contour_points = measure.find_contours(mask, level=0.1)[0] isinside = [ is_point_inside_mask(contour_points, pix) for pix in pix_coords ] # sources_inside_aperture.append(isinside) min_gmag = gaia_sources.loc[isinside, "phot_g_mean_mag"].min() if (target_gmag - min_gmag) != 0: print( f"target Gmag={target_gmag:.2f} is not the brightest within aperture (Gmag={min_gmag:.2f})" ) else: min_gmag = gaia_sources.phot_g_mean_mag.min() # brightest dmag_limit = (gaia_sources.phot_g_mean_mag.max() if dmag_limit is None else dmag_limit) base_ms = 128.0 # base marker size starid = 1 # if very crowded, plot only top N gmags = gaia_sources.phot_g_mean_mag dmags = gmags - target_gmag rank = np.argsort(dmags.values) for index, row in gaia_sources.iterrows(): # FIXME: why some indexes are missing? ra, dec, gmag, id = row[["ra", "dec", "phot_g_mean_mag", "source_id"]] dmag = gmag - target_gmag pix = tpf.wcs.all_world2pix(np.c_[ra, dec], 0)[0] contour_points = measure.find_contours(mask, level=0.1)[0] color, alpha = "red", 1.0 # change marker color and transparency depending on the location and dmag if is_point_inside_mask(contour_points, pix): if int(id) == int(target_gaiaid): # plot x on target ax.plot( pix[1], pix[0], marker="x", ms=base_ms / 16, c="k", zorder=3, ) if depth is not None: # compute flux ratio with respect to brightest star gamma = 1 + 10**(0.4 * (min_gmag - gmag)) if depth > kmax / gamma: # orange if flux is insignificant color = "C1" else: # outside aperture color, alpha = "C1", 0.5 ax.scatter( pix[1], pix[0], s=base_ms / 2**dmag, # fainter -> smaller c=color, alpha=alpha, zorder=2, edgecolor=None, ) # choose which star to annotate if len(gmags) < 20: # sparse: annotate all ax.text(pix[1], pix[0], str(starid), color="white", zorder=100) elif len(gmags) > 50: # crowded: annotate only 15 smallest dmag ones if rank[starid - 1] < 15: ax.text(pix[1], pix[0], str(starid), color="white", zorder=100) elif (color == "red") & (dmag < dmag_limit): # plot if within aperture and significant source of dilution ax.text(pix[1], pix[0], str(starid), color="white", zorder=100) elif color == "red": # neither sparse nor crowded # annotate if inside aperture ax.text(pix[1], pix[0], str(starid), color="white", zorder=100) starid += 1 # Make legend with 4 sizes representative of delta mags dmags = dmags[dmags < dmag_limit] _, dmags = pd.cut(dmags, 3, retbins=True) for dmag in dmags: size = base_ms / 2**dmag # -1, -1 is outside the fov # dmag = 0 if float(dmag)==0 else 0 ax.scatter( -1, -1, s=size, c="red", alpha=0.6, edgecolor=None, zorder=10, clip_on=True, label=r"$\Delta m= $" + f"{dmag:.1f}", ) ax.legend(fancybox=True, framealpha=0.5) # set img limits xdeg = (nx * pix_scale).to(u.arcmin) ydeg = (ny * pix_scale).to(u.arcmin) # orient such that north is up; east is left if invert_yaxis: # ax.invert_yaxis() # increasing upward raise NotImplementedError() if invert_xaxis: # ax.invert_xaxis() #decresing rightward raise NotImplementedError() if hasattr(ax, "coords"): ax.coords[0].set_major_formatter("dd:mm") ax.coords[1].set_major_formatter("dd:mm") pl.setp(ax, xlim=(0, nx), ylim=(0, ny), xlabel=f"({xdeg:.2f} x {ydeg:.2f})") return ax