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.
@@ -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.
@@ -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
+
@@ -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,3 @@
1
+ "TESS Aperture Photometry Pollution eStimator"
2
+
3
+ __version__ = "0.1.1"
@@ -0,0 +1,2 @@
1
+ from tapps.main import main
2
+ if __name__ == "__main__": main()
@@ -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")