tessapps 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tessapps-0.1.1/.gitignore +1 -0
- tessapps-0.1.1/LICENSE +21 -0
- tessapps-0.1.1/PKG-INFO +17 -0
- tessapps-0.1.1/README.md +1 -0
- tessapps-0.1.1/pyproject.toml +19 -0
- tessapps-0.1.1/src/tapps/__init__.py +3 -0
- tessapps-0.1.1/src/tapps/__main__.py +2 -0
- tessapps-0.1.1/src/tapps/data_to_contamination.py +53 -0
- tessapps-0.1.1/src/tapps/main.py +68 -0
- tessapps-0.1.1/src/tapps/nearby_stars_to_data.py +48 -0
- tessapps-0.1.1/src/tapps/tpf_to_nearby_stars.py +38 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
dist/
|
tessapps-0.1.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Leonidas Liponis
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
tessapps-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: tessapps
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: TESS Aperture Photometry Pollution eStimator
|
|
5
|
+
Author-email: Leonidas Liponis <leolip128@proton.me>
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Requires-Dist: numpy
|
|
10
|
+
Requires-Dist: scipy
|
|
11
|
+
Requires-Dist: matplotlib
|
|
12
|
+
Requires-Dist: astropy
|
|
13
|
+
Requires-Dist: astroquery
|
|
14
|
+
Requires-Dist: lightkurve
|
|
15
|
+
|
|
16
|
+
TESS Aperture Photometry Pollution eStimator
|
|
17
|
+
|
tessapps-0.1.1/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
TESS Aperture Photometry Pollution eStimator
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["flit_core >=3.2,<4"]
|
|
3
|
+
build-backend = "flit_core.buildapi"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tessapps"
|
|
7
|
+
authors = [{name = "Leonidas Liponis", email = "leolip128@proton.me"}]
|
|
8
|
+
license = {file = "LICENSE"}
|
|
9
|
+
classifiers = ["License :: OSI Approved :: MIT License"]
|
|
10
|
+
dynamic = ["version", "description"]
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
requires-python = ">=3.9"
|
|
13
|
+
dependencies = ["numpy", "scipy", "matplotlib", "astropy", "astroquery", "lightkurve"]
|
|
14
|
+
|
|
15
|
+
[project.scripts]
|
|
16
|
+
tapps = "tapps.main:main"
|
|
17
|
+
|
|
18
|
+
[tool.flit.module]
|
|
19
|
+
name = "tapps"
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from scipy import integrate
|
|
3
|
+
|
|
4
|
+
def pixels_in_mask(tpf): # this function returns an array that contains all pixels used for photometry
|
|
5
|
+
raw_pixels_in_mask = np.where(tpf.pipeline_mask == True) # it finds the pixel coordinates
|
|
6
|
+
return np.array([raw_pixels_in_mask[0], raw_pixels_in_mask[1]]).T # then pairs them up in this way
|
|
7
|
+
|
|
8
|
+
def gaussian(x, y, area, sigma): return area / (2 * np.pi * sigma**2) * np.exp(-(x**2 + y**2) / (2 * sigma**2)) # the standard 2D gaussian function
|
|
9
|
+
|
|
10
|
+
def background_gaussian_field(tpf, data, sigma): # this function calculates the ideal gaussian field comprised of all stars' contributions
|
|
11
|
+
pixels = pixels_in_mask(tpf) # these are the pixels of interest: all calculations are done for these only
|
|
12
|
+
field = np.zeros((tpf.shape[1:])) # the field is defined as an 11x11 matrix of zeros, filled iteratively
|
|
13
|
+
error = np.zeros_like(field) # its error is also the same matrix
|
|
14
|
+
total_error_squared = 0.0 # total error is initialized at 0 and grows after each star
|
|
15
|
+
|
|
16
|
+
for star in data[1:]: # data[0] is the target star, since the data is sorted. data[1:] has all other stars
|
|
17
|
+
total_star_flux = 0.0 # this will accumulate the flux of each pixel in the mask
|
|
18
|
+
for i, j in pixels: # i -> y, j -> x. the integration below is done for the entirety of the pixel
|
|
19
|
+
gauss, _ = integrate.dblquad(lambda y, x: gaussian(x - star["x"], y - star["y"], star["flux"], sigma), j - 0.5, j + 0.5, i - 0.5, i + 0.5)
|
|
20
|
+
field[i, j] += gauss # the field grows in this pixel by the flux of this specific star
|
|
21
|
+
error[i, j] += (star["flux_er"] * (gauss / star["flux"])) ** 2 # the error grows too, by this formula (error propagation)
|
|
22
|
+
total_star_flux += gauss # the total flux of this star accumulates the gauss area from each pixel
|
|
23
|
+
total_error_squared += (star["flux_er"] * total_star_flux / star["flux"]) ** 2 # the full error has a similar formula
|
|
24
|
+
return field, np.sqrt(error), field.sum(), np.sqrt(total_error_squared) # also returns the total flux of background stars (with error)
|
|
25
|
+
|
|
26
|
+
def target_gaussian_field(tpf, data, sigma): # this function returns the gaussian field of the target star in the aperture pixels
|
|
27
|
+
star = data[0] # it is the same function as 'background_gaussian_field', without the loop for each star
|
|
28
|
+
field = np.zeros((tpf.shape[1:]))
|
|
29
|
+
error = np.zeros_like(field)
|
|
30
|
+
total_star_flux = 0.0
|
|
31
|
+
|
|
32
|
+
for i, j in pixels_in_mask(tpf):
|
|
33
|
+
gauss, _ = integrate.dblquad(lambda y, x: gaussian(x - star["x"], y - star["y"], star["flux"], sigma), j - 0.5, j + 0.5, i - 0.5, i + 0.5)
|
|
34
|
+
field[i, j] += gauss
|
|
35
|
+
error[i, j] += (star["flux_er"] * (gauss / star["flux"])) ** 2
|
|
36
|
+
total_star_flux += gauss
|
|
37
|
+
total_error = star["flux_er"] * total_star_flux / star["flux"]
|
|
38
|
+
return field, np.sqrt(error), field.sum(), total_error # returns the field, the field of errors in each pixel, the total flux, and its error
|
|
39
|
+
|
|
40
|
+
def contribution_calculator(tpf, data, sigma, digits, result): # this function calculates the fields for the target star and all background stars
|
|
41
|
+
background_field, background_field_error, background_total_flux, background_total_error = background_gaussian_field(tpf, data, sigma)
|
|
42
|
+
target_field, target_field_error, target_total_flux, target_total_error = target_gaussian_field(tpf, data, sigma)
|
|
43
|
+
|
|
44
|
+
background_contribution = background_total_flux / (background_total_flux + target_total_flux) # the observed field is ideally the sum of the two
|
|
45
|
+
target_contribution = target_total_flux / (background_total_flux + target_total_flux) # in reality both will be biased in some way
|
|
46
|
+
|
|
47
|
+
error_term = 1 / (background_total_flux + target_total_flux)**2
|
|
48
|
+
total_error = error_term * np.sqrt((background_total_flux * target_total_error)**2 + (target_total_flux * background_total_error)**2)
|
|
49
|
+
|
|
50
|
+
if result:
|
|
51
|
+
print(f"The target star contributes ({100 * target_contribution :.{digits}f} ± {100 * total_error :.{digits}f})% of the total flux.")
|
|
52
|
+
|
|
53
|
+
return np.round(target_contribution, digits + 2), np.round(total_error, digits + 2)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
from erfa import ErfaWarning
|
|
3
|
+
warnings.filterwarnings("ignore", category=ErfaWarning)
|
|
4
|
+
warnings.filterwarnings("ignore", message=".*tpfmodel submodule is not available.*")
|
|
5
|
+
|
|
6
|
+
import lightkurve as lk
|
|
7
|
+
import numpy as np
|
|
8
|
+
from scipy import integrate
|
|
9
|
+
import astropy.units as u
|
|
10
|
+
from astropy.table import Table
|
|
11
|
+
from astropy.time import Time
|
|
12
|
+
from astropy.coordinates import SkyCoord
|
|
13
|
+
from astropy.wcs.utils import proj_plane_pixel_scales
|
|
14
|
+
from astroquery.mast import Catalogs
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import matplotlib.pyplot as plt
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
import tapps.tpf_to_nearby_stars as ttns
|
|
21
|
+
import tapps.nearby_stars_to_data as nstd
|
|
22
|
+
import tapps.data_to_contamination as dtc
|
|
23
|
+
|
|
24
|
+
def run(tpf, pixel_overshoot, sigma, digits, info, result, plot, output_directory):
|
|
25
|
+
if tpf is None: tpf = lk.search_targetpixelfile("HAT-P-1", mission="TESS", sector=84, exptime=120).download()
|
|
26
|
+
stars = ttns.tpf_to_nearby_stars(tpf, pixel_overshoot, info)
|
|
27
|
+
data = nstd.data_builder(tpf, stars)
|
|
28
|
+
target_contribution, total_error = dtc.contribution_calculator(tpf, data, sigma, digits, result)
|
|
29
|
+
|
|
30
|
+
print(data[:6])
|
|
31
|
+
|
|
32
|
+
if plot:
|
|
33
|
+
fig, ax = plt.subplots(figsize=(8, 8))
|
|
34
|
+
tpf.plot(ax=ax, aperture_mask=tpf.pipeline_mask)
|
|
35
|
+
text = f"Target star contributes ({100 * target_contribution :.{digits}f} ± {100 * total_error :.{digits}f})% of the total flux"
|
|
36
|
+
fig.text(0.5 , 0.01, text, ha="center", fontsize=14)
|
|
37
|
+
out_path = Path(f"{output_directory}/TIC_{tpf.targetid}.png")
|
|
38
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
fig.savefig(out_path, dpi=200, bbox_inches="tight")
|
|
40
|
+
plt.show
|
|
41
|
+
return [float(target_contribution), float(total_error)]
|
|
42
|
+
|
|
43
|
+
def main(argv=None):
|
|
44
|
+
parser = argparse.ArgumentParser()
|
|
45
|
+
parser.add_argument("--tpf", default=None)
|
|
46
|
+
parser.add_argument("--overshoot", type=float, default=1)
|
|
47
|
+
parser.add_argument("--sigma", type=float, default=1)
|
|
48
|
+
parser.add_argument("--digits", type=int, default=3)
|
|
49
|
+
parser.add_argument("--info", dest="info", action="store_true")
|
|
50
|
+
parser.add_argument("--result", dest="result", action="store_true")
|
|
51
|
+
parser.add_argument("--plot", dest="plot", action="store_true")
|
|
52
|
+
parser.add_argument("--out_dir", default="tapps-results")
|
|
53
|
+
args = parser.parse_args(argv)
|
|
54
|
+
run(args.tpf, args.overshoot, args.sigma, args.digits, args.info, args.result, args.plot, args.out_dir)
|
|
55
|
+
|
|
56
|
+
# tpf_file = lk.search_targetpixelfile("HAT-P-1", mission="TESS", sector=84, exptime=120).download()
|
|
57
|
+
# tpf_file = lk.search_targetpixelfile("WASP-12", mission="TESS", sector=20, exptime=120).download()
|
|
58
|
+
# tpf_file = lk.search_targetpixelfile("TOI-122", mission="TESS", exptime=1800).download()
|
|
59
|
+
# tpf_file = lk.search_targetpixelfile("TOI-122", mission="TESS")[-1].download()
|
|
60
|
+
# tpf_file = lk.search_targetpixelfile('20:45:09.5323 -31:20:27.241')[4].download() # AUMic
|
|
61
|
+
# tpf_file = lk.search_targetpixelfile('55 Cnc')[6].download()
|
|
62
|
+
# tpf_file = lk.search_targetpixelfile('CoRoT 1')[1].download()
|
|
63
|
+
# tpf_file = lk.search_targetpixelfile('19:24:15.2868 +00:44:46.003')[1].download() # CoRoT 10
|
|
64
|
+
|
|
65
|
+
# tpf_file.plot(aperture_mask=tpf_file.pipeline_mask)
|
|
66
|
+
# run(tpf_file, result=True, plot=True)
|
|
67
|
+
|
|
68
|
+
# main()
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import astropy.units as u
|
|
3
|
+
from astropy.table import Table
|
|
4
|
+
from astropy.time import Time
|
|
5
|
+
from astropy.coordinates import SkyCoord
|
|
6
|
+
|
|
7
|
+
def stars_to_raw_data(stars): # this function extracts TIC, positions, Tmag, Tmag_error from the mast catalog
|
|
8
|
+
def converter(name): return np.asarray(stars[name].filled(np.nan), dtype=float)
|
|
9
|
+
|
|
10
|
+
tic = np.asarray(stars["ID"], dtype=np.int64) # the tess id for each star
|
|
11
|
+
ra = converter("ra") * u.deg # it needs degrees as a unit for SkyCoords
|
|
12
|
+
dec = converter("dec") * u.deg # degrees here as well
|
|
13
|
+
pm_ra_cosdec = np.nan_to_num(converter("pmRA")) * u.mas / u.yr # mas = milli-arcseconds per year (tess gives these units)
|
|
14
|
+
pm_dec = np.nan_to_num(converter("pmDEC")) * u.mas / u.yr # the above line has cos(dec) to get the actual angular speed across the sky
|
|
15
|
+
time = Time(2000.0, format="jyear") # the stars have the listed coordinates in this date exactly
|
|
16
|
+
tmag = converter("Tmag") # Tess magnitude, in order to find electrons
|
|
17
|
+
tmag_error = converter("e_Tmag") # error in Tess magnitude
|
|
18
|
+
|
|
19
|
+
positions = SkyCoord(ra=ra, dec=dec, pm_ra_cosdec=pm_ra_cosdec, pm_dec=pm_dec, obstime=time) # these are the positions in the past
|
|
20
|
+
return tic, positions, tmag, tmag_error
|
|
21
|
+
|
|
22
|
+
def flux_calculator(tmag, tmag_error): # this function finds the flux from Tmag for all stars at once
|
|
23
|
+
tess_zero_point, m = 15000, 10 # The TESS cameras create 15,000 e−/s for a star of m = 10, as per the handbook
|
|
24
|
+
flux = tess_zero_point * 10 ** (-0.4 * (tmag - m)) # this is the standard formula for flux in e−/s
|
|
25
|
+
flux_error = flux * 0.4 * np.log(10) * tmag_error # derived from error propagation
|
|
26
|
+
return flux, np.nan_to_num(flux_error) # these are arrays, containing flux for every star
|
|
27
|
+
|
|
28
|
+
def data_builder(tpf, stars): # this function uses the above 2 functions to make a table containing relevant data
|
|
29
|
+
tic, positions, tmag, tmag_error = stars_to_raw_data(stars) # first it calculates the stars' (ra, dec) positions and extracts Tmag
|
|
30
|
+
positions = positions.apply_space_motion(new_obstime=Time(tpf.time.mean())) # these are the positions at the time of observation
|
|
31
|
+
flux, flux_error = flux_calculator(tmag, tmag_error) # then it finds the flux of each star
|
|
32
|
+
x, y = tpf.wcs.world_to_pixel(positions) # and their positions in pixels
|
|
33
|
+
|
|
34
|
+
data = Table({"TIC": tic, "Tmag": tmag, "Tmag_er": tmag_error, "flux": flux, "flux_er": flux_error, "x": x, "y": y}) # build the table
|
|
35
|
+
data["Tmag"].info.format = ".2f" # these are custom formatting options
|
|
36
|
+
data["Tmag_er"].info.format = ".3f" # so that it appears nice when printing the data
|
|
37
|
+
data["flux"].info.format = ".2f" # no rounding is applied to any number, only when showing them
|
|
38
|
+
data["flux_er"].info.format = ".2f" # data contains for each star: TIC, Tmag and flux (with errors), pixel positions (without errors)
|
|
39
|
+
data["x"].info.format = ".2f" # x and y pixel positions are considered not to have error because it is too small to be important
|
|
40
|
+
data["y"].info.format = ".2f" # and would make error propagation difficult when calculating the gaussians
|
|
41
|
+
|
|
42
|
+
print(f"{(~np.isfinite(data["flux"])).sum()} of {len(data)} stars removed (NaN or infinite flux)")
|
|
43
|
+
|
|
44
|
+
data = data[np.isfinite(data["flux"])] # filter out NaN cases, substitute with 0 (where there are no values for Tmag, rare)
|
|
45
|
+
is_target = np.asarray(data["TIC"]) == int(tpf.targetid) # get the target star's position in the list
|
|
46
|
+
data = data[np.lexsort((data["Tmag"], ~is_target))] # sort by ascending magnitude (bright to faint), with the target star always first
|
|
47
|
+
assert data[0]["TIC"] == int(tpf.targetid), "Error: Target star missing from data" # hasn't happened yet
|
|
48
|
+
return data
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import astropy.units as u
|
|
3
|
+
from astropy.wcs.utils import proj_plane_pixel_scales
|
|
4
|
+
from astroquery.mast import Catalogs
|
|
5
|
+
|
|
6
|
+
def tpf_to_nearby_stars(tpf, pixel_overshoot, info): # this function takes a tpf file and returns the search radius
|
|
7
|
+
pixels_y, pixels_x = tpf.shape[1:] # it should return 11x11 (pixels). for tpf files, y is first, x is second
|
|
8
|
+
pixels_max = np.maximum(pixels_x, pixels_y) # in case it doesn't, take the bigger value of the two
|
|
9
|
+
diagonal = pixels_max * np.sqrt(2) # the diagonal of the 11x11 square (or the square of the biggest pixel length)
|
|
10
|
+
sharp_radius_p = diagonal / 2 # the radius of the circumscribed circle around the square, in pixels
|
|
11
|
+
safe_radius_p = sharp_radius_p + pixel_overshoot # this is bigger to account for projection errors and distortions, to get more stars
|
|
12
|
+
|
|
13
|
+
pixel_scale_x, pixel_scale_y = proj_plane_pixel_scales(tpf.wcs) # pixel scale from wcs (coordinates). x is first, y is second
|
|
14
|
+
pixel_scale = (pixel_scale_x + pixel_scale_y)/2 # the average should suffice as an approximation
|
|
15
|
+
scale_difference = np.abs((pixel_scale_x - pixel_scale_y)/2) / pixel_scale # here is the relative difference between the scales
|
|
16
|
+
safe_radius_d = safe_radius_p * pixel_scale * u.deg # convert to degrees using pixel scale
|
|
17
|
+
|
|
18
|
+
center_x = (pixels_x - 1) / 2 # the center of the image (x coordinate): -1 because index starts at 0
|
|
19
|
+
center_y = (pixels_y - 1) / 2 # the center of the image (y coordinate)
|
|
20
|
+
center = tpf.wcs.pixel_to_world(center_x, center_y)
|
|
21
|
+
|
|
22
|
+
if info: print(f""" TICID: {tpf.targetid}
|
|
23
|
+
Time:
|
|
24
|
+
Observation start and stop time in TJD: {tpf.get_header(ext=0)["TSTART"]:.3f}, {tpf.get_header(ext=0)["TSTOP"]:.3f}
|
|
25
|
+
Observation start and stop time in UTC: {tpf.get_header(ext=0)["DATE-OBS"]}, {tpf.get_header(ext=0)["DATE-END"]}
|
|
26
|
+
Observed for {tpf.get_header(ext=0)["TSTOP"] - tpf.get_header(ext=0)["TSTART"]:.3f} days
|
|
27
|
+
Place:
|
|
28
|
+
Center of the {pixels_x} by {pixels_y} frames is at RA: {center.ra:.3f}, DEC = {center.dec:.3f}
|
|
29
|
+
The image spans {pixels_x * pixel_scale_x:.3f} by {pixels_y * pixel_scale_y:.3f} degrees on the sky
|
|
30
|
+
Results:
|
|
31
|
+
The background stars in the images will be searched in a circle of radius {safe_radius_d:.3f} from the center
|
|
32
|
+
This circle circumscribes the square image, and has a {safe_radius_p / sharp_radius_p * 100:.2f}% bigger radius than needed
|
|
33
|
+
The circle covers {(np.pi * safe_radius_p**2 - pixels_x * pixels_y) / (pixels_x * pixels_y) * 100:.1f}% more area around the square
|
|
34
|
+
Errors:
|
|
35
|
+
Pixel scale is {pixel_scale:.3f} deg/pixel with {scale_difference * 100:.2f}% scale difference
|
|
36
|
+
The error is due to differences in horizontal ({pixel_scale_x:.5f}) and vertical ({pixel_scale_y:.5f}) pixel scales
|
|
37
|
+
If the error is more than 5%, then increase 'pixel_overshoot' to make sure the search finds enough stars""")
|
|
38
|
+
return Catalogs.query_region(center, radius=safe_radius_d, catalog="TIC")
|