diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b34309..ff9a428 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +## [Unreleased](https://github.com/kjkoeller/Binary_Star_Research_Package/compare/v4.1.4...HEAD) + +### Merged + +* Incorrect Importing of vseq script [`#382`](https://github.com/kjkoeller/Binary_Star_Research_Package/pull/382) +* Kjkoeller patch 3 [`#381`](https://github.com/kjkoeller/Binary_Star_Research_Package/pull/381) +* GUI Startup [`#380`](https://github.com/kjkoeller/Binary_Star_Research_Package/pull/380) +* Documentation Updates [`#379`](https://github.com/kjkoeller/Binary_Star_Research_Package/pull/379) +* Paper Reference for GAIA-TESS Calculations [`#378`](https://github.com/kjkoeller/Binary_Star_Research_Package/pull/378) +* Potential Crashing [`#377`](https://github.com/kjkoeller/Binary_Star_Research_Package/pull/377) +* Bump braces from 3.0.2 to 3.0.3 in /changelog [`#376`](https://github.com/kjkoeller/Binary_Star_Research_Package/pull/376) +* Header Correct Command Line [`#374`](https://github.com/kjkoeller/Binary_Star_Research_Package/pull/374) +* Turn into a Callable Function [`#375`](https://github.com/kjkoeller/Binary_Star_Research_Package/pull/375) +* Header Correction [`#373`](https://github.com/kjkoeller/Binary_Star_Research_Package/pull/373) +* Functions for Validating Files/Directories [`#372`](https://github.com/kjkoeller/Binary_Star_Research_Package/pull/372) +* Allows for standard version printing [`#371`](https://github.com/kjkoeller/Binary_Star_Research_Package/pull/371) +* Latex Output Error [`#369`](https://github.com/kjkoeller/Binary_Star_Research_Package/pull/369) +* Pipeline Capabilities and Cleanup [`#367`](https://github.com/kjkoeller/Binary_Star_Research_Package/pull/367) +* Significant Comments Added [`#368`](https://github.com/kjkoeller/Binary_Star_Research_Package/pull/368) + +### Commits + +* Calling Package Crashing [`a47b588`](https://github.com/kjkoeller/Binary_Star_Research_Package/commit/a47b58806932f606ea62a5a9e048222858ea6be2) +* Misspelling [`834be2c`](https://github.com/kjkoeller/Binary_Star_Research_Package/commit/834be2cc6a3d3638e97e5f905cf23a714faff875) +* Rename header-correct.py to headerCorrect.py [`55a87c0`](https://github.com/kjkoeller/Binary_Star_Research_Package/commit/55a87c0db91f9995736bb68965e655988feb7254) + ## [v4.1.4](https://github.com/kjkoeller/Binary_Star_Research_Package/compare/v4.1.3...v4.1.4) - 2024-04-27 ### Merged diff --git a/EclipsingBinaries/menu.py b/EclipsingBinaries/menu.py index 4be10bd..f81fef4 100644 --- a/EclipsingBinaries/menu.py +++ b/EclipsingBinaries/menu.py @@ -1,28 +1,24 @@ """ -The main program helps centralize all the other programs into one selection routine that can be run and call all -other programs. +A GUI to centralize all the scripts and capabilities of this package for ease of use for the user and +making it more convenient to use and access than a command line or individual scripts. Author: Kyle Koeller Created: 8/29/2022 -Last Updated: 12/11a/2024 +Last Updated: 12/16/2024 """ - import tkinter as tk from tkinter import messagebox, filedialog, ttk from pathlib import Path import threading from astropy.nddata import CCDData from matplotlib import pyplot as plt -# from astropy.io import fits -import pandas as pd import traceback -# Import the updated IRAF Reduction script - -from .IRAF_Reduction import run_reduction -from .tess_data_search import run_tess_search -from .apass import comparison_selector +from IRAF_Reduction import run_reduction +from tess_data_search import run_tess_search +from apass import comparison_selector +from multi_aperture_photometry import main as multi_ap class ProgramLauncher(tk.Tk): @@ -90,7 +86,7 @@ def create_menu(self): ("Find Minimum (WIP)", self.dummy_action), ("TESS Database Search/Download", self.show_tess_search), ("AIJ Comparison Star Selector", self.show_aij_comparison_selector), - ("Multi-Aperture Calculation", self.dummy_action), + ("Multi-Aperture Calculation", self.show_multi_aperture_photometry), ("BSUO or SARA/TESS Night Filters", self.dummy_action), ("O-C Plotting", self.dummy_action), ("Gaia Search", self.dummy_action), @@ -344,6 +340,42 @@ def show_aij_comparison_selector(self): # Create scrollbar for the log area self.create_scrollbar_and_log(8) + def show_multi_aperture_photometry(self): + """Display the Multi-Aperture Photometry panel.""" + self.clear_right_frame() + + # Configure grid + self.right_frame.grid_columnconfigure(0, weight=1) + self.right_frame.grid_columnconfigure(1, weight=1) + + # Add title + tk.Label(self.right_frame, text="Multi-Aperture Photometry", font=self.header_font, bg="#ffffff").grid( + row=0, column=0, columnspan=2, pady=10, sticky="ew" + ) + + # Input fields + obj_name = self.create_input_field(self.right_frame, "Object Name:", row=1) + reduced_images_path = self.create_input_field(self.right_frame, "Reduced Images Path:", row=2) + radec_b_file = self.create_input_field(self.right_frame, "RADEC File (B Filter):", row=3) + radec_v_file = self.create_input_field(self.right_frame, "RADEC File (V Filter):", row=4) + radec_r_file = self.create_input_field(self.right_frame, "RADEC File (R Filter):", row=5) + + # Run button + self.create_run_button(self.right_frame, self.run_multi_aperture_photometry, row=6, + obj_name=obj_name, + reduced_images_path=reduced_images_path, + radec_b_file=radec_b_file, + radec_v_file=radec_v_file, + radec_r_file=radec_r_file) + + # Log display area + tk.Label(self.right_frame, text="Output Log:", font=self.label_font, bg="#ffffff").grid( + row=7, column=0, columnspan=2, pady=5 + ) + + # Create scrollbar for the log area + self.create_scrollbar_and_log(8) + def create_scrollbar_and_log(self, row): # Create a frame to hold the log area and scrollbar log_frame = tk.Frame(self.right_frame, bg="#ffffff") @@ -551,6 +583,54 @@ def selector_task(): self.run_task(selector_task) + def run_multi_aperture_photometry(self, obj_name, reduced_images_path, radec_b_file, radec_v_file, radec_r_file): + """Run the Multi-Aperture Photometry script in a separate thread.""" + + def photometry_task(): + try: + self.create_cancel_button(self.right_frame, self.cancel_task, row=6) + + obj_name_value = obj_name.get().strip() + reduced_path_value = reduced_images_path.get().strip() + radec_b_path = radec_b_file.get().strip() + radec_v_path = radec_v_file.get().strip() + radec_r_path = radec_r_file.get().strip() + + # Validate inputs + if not reduced_path_value: + self.write_to_log("Error: Reduced images path is required.") + return + if not (radec_b_path and radec_v_path and radec_r_path): + self.write_to_log("Error: RADEC files for all filters are required.") + return + + # Log setup + self.write_to_log(f"Object Name: {obj_name_value}") + self.write_to_log(f"Reduced Images Path: {reduced_path_value}") + self.write_to_log(f"RADEC File (B Filter): {radec_b_path}") + self.write_to_log(f"RADEC File (V Filter): {radec_v_path}") + self.write_to_log(f"RADEC File (R Filter): {radec_r_path}") + + # Run the multi-aperture photometry script + multi_ap( + path=reduced_path_value, + pipeline=False, + radec_list=[radec_b_path, radec_v_path, radec_r_path], + obj_name=obj_name_value, + write_callback=self.write_to_log, + cancel_event=self.cancel_event # Pass cancel_event + ) + + if not self.cancel_event.is_set(): + self.write_to_log("Multi-Aperture Photometry completed successfully.") + else: + self.write_to_log("Multi-Aperture Photometry was canceled.") + except Exception as e: + self.write_to_log(f"An error occurred during Multi-Aperture Photometry: {e}") + + # Run the photometry task in a separate thread + self.run_task(photometry_task) + def dummy_action(self): """Dummy action for unimplemented features""" messagebox.showinfo("Action", "This feature is not implemented yet.") diff --git a/EclipsingBinaries/multi_aperture_photometry.py b/EclipsingBinaries/multi_aperture_photometry.py index 2e01b2a..b0a5d19 100644 --- a/EclipsingBinaries/multi_aperture_photometry.py +++ b/EclipsingBinaries/multi_aperture_photometry.py @@ -3,7 +3,7 @@ Author: Kyle Koeller Created: 05/07/2023 -Last Updated: 08/24/2023 +Last Updated: 12/16/2024 """ # Python imports @@ -11,6 +11,7 @@ import pandas as pd from pathlib import Path import matplotlib.pyplot as plt +import matplotlib # import time import warnings from tqdm import tqdm @@ -31,101 +32,95 @@ # turn off this warning that just tells the user, # "The warning raised when the contents of the FITS header have been modified to be standards compliant." warnings.filterwarnings("ignore", category=wcs.FITSFixedWarning) +matplotlib.use('Agg') -def main(path="", pipeline=False, radec_list=None, obj_name=""): +def main(path="", pipeline=False, radec_list=None, obj_name="", write_callback=None, cancel_event=None): """ - Main function for aperture photometry + Perform multi-aperture photometry on images. - Parameters + Parameters: ---------- - radec_list: list - RADEC files for each filter - obj_name: str - Name of the target path : str - Path to the folder containing the images. + Path to the folder containing the reduced images. pipeline : bool - If True, then the program is being run from the pipeline and will not ask for user input. - Returns + If True, suppresses prompts and expects all inputs to be provided. + radec_list : list + List of RADEC files for each filter. + obj_name : str + Name of the target object (for file naming). + write_callback : callable + Function to write logs to the GUI. + cancel_event : threading.Event + Event to allow task cancellation. + + Returns: ------- - N/A + None """ filt_list = ["Empty/B", "Empty/V", "Empty/R"] - if not pipeline: - # path = "D:\Research\Data\\NSVS_254037\\2018.09.18-reduced" # For testing purposes - path = input( - "Please enter a file pathway (i.e. C:\\folder1\\folder2\\[raw]) to where the reduced images are or type " - "the word 'Close' to leave: ") - # allows the user to input where the raw images are and where the calibrated images go to - radec_file = "" - while True: - try: - images_path = Path(path) - break - except FileNotFoundError: - print("File not found. Please try again.") - path = input( - "Please enter a file pathway (i.e. C:\\folder1\\folder2\\[reduced]) to where the reduced images are or type " - "the word 'Close' to leave: ") - - if path.lower() == "close": - exit() - - science_imagetyp = 'LIGHT' - files = ccdp.ImageFileCollection(images_path) - for filt in filt_list: - if "/B" in filt: - radec_file = input("Enter the file location for the RADEC file for the B filter: ") - elif "/V" in filt: - radec_file = input("Enter the file location for the RADEC file for the V filter: ") - elif "/R" in filt: - radec_file = input("Enter the file location for the RADEC file for the R filter: ") - - image_list = files.files_filtered(imagetyp=science_imagetyp, filter=filt) - multiple_AP(image_list, images_path, filt, pipeline=pipeline, radec_file=radec_file) - else: - images_path = Path(path) - - science_imagetyp = 'LIGHT' + def log(message): + if write_callback: + write_callback(message) + else: + print(message) + try: + images_path = Path(path) files = ccdp.ImageFileCollection(images_path) - for filt in filt_list: - if "/B" in filt: - radec_file = radec_list[0] - elif "/V" in filt: - radec_file = radec_list[1] - elif "/R" in filt: - radec_file = radec_list[2] - image_list = files.files_filtered(imagetyp=science_imagetyp, filter=filt) - substring_to_match = obj_name - filtered_image_list = [file for file in image_list if substring_to_match in file] - multiple_AP(filtered_image_list, images_path, filt, pipeline=pipeline, radec_file=radec_file) - - -def multiple_AP(image_list, path, filter, pipeline=False, radec_file=""): + for filt, radec_file in zip(filt_list, radec_list): + if cancel_event and cancel_event.is_set(): + log("Multi-Aperture Photometry was canceled.") + return + + image_list = files.files_filtered(imagetyp='LIGHT', filter=filt) + log(f"Processing {len(image_list)} images for {filt} filter.") + multiple_AP( + image_list=image_list, + path=images_path, + filter=filt, + pipeline=pipeline, + obj_name=obj_name, + radec_file=radec_file, + write_callback=write_callback, + cancel_event=cancel_event + ) + except Exception as e: + log(f"An error occurred in Multi-Aperture Photometry: {e}") + + +def multiple_AP(image_list, path, filter, pipeline=False, obj_name="", radec_file="", write_callback=None, cancel_event=None): """ - Perform multi-aperture photometry on a list of images for a single target + Perform photometry on a list of images for a given filter. - Parameters + Parameters: ---------- - filter: String - Filter used for the images - pipeline: Boolean - If True, then the program is being run from the pipeline and will not ask for user input. - radec_file: string - Location of a radec file. If not given, the user will be prompted to enter one. - path : pathway + image_list : list + List of image filenames. + path : Path Path to the folder containing the images. - image_list : List - Images to perform multi-aperture photometry on. - - Returns + filter : str + Filter type (B, V, or R). + pipeline : bool + If True, suppresses user prompts. + radec_file : str + Path to the RADEC file for the current filter. + write_callback : callable + Function to write logs to the GUI. + cancel_event : threading.Event + Event to allow task cancellation. + + Returns: ------- None """ + def log(message): + if write_callback: + write_callback(message) + else: + print(message) # Define the aperture parameters # Define the aperture and annulus radii @@ -136,216 +131,208 @@ def multiple_AP(image_list, path, filter, pipeline=False, radec_file=""): # gain = 1.43 # * u.electron / u.adu # gathered from fits headers manually # F_dark = 0.01 # dark current in u.electron / u.pix / u.s - if not pipeline: - while True: - try: - # df = pd.read_csv('NSVS_254037-B.radec', skiprows=7, sep=",", header=None) - df = pd.read_csv(radec_file, skiprows=7, sep=",", header=None) - break - except FileNotFoundError: - print("File not found. Please try again.") - radec_file = input("Please enter the RADEC file (i.e. C://folder1//folder2//[file name]: ") - else: + try: df = pd.read_csv(radec_file, skiprows=7, sep=",", header=None) - print("RADEC file found.\n") - - magnitudes_comp = df[4] - - magnitudes_comp = magnitudes_comp.replace(99.999, pd.NA).dropna().reset_index(drop=True) - - ra = df[0] - dec = df[1] - # ref_star = df[2] - # centroid = df[3] # Not used (I don't think at least) - - magnitudes = [] - mag_err = [] - hjd = [] - bjd = [] - - for icount, image_file in tqdm(enumerate(image_list), desc="Performing aperture photometry on {} images".format(len(image_list))): - image_data, header = fits.getdata(path / image_file, header=True) - # All the following up till the 'if' statement stays under the for loop due to needing the header information - wcs_ = WCS(header) - - # ccd = CCDData(image_data, wcs=wcs, unit='adu') - - # Convert RA and DEC to pixel positions - sky_coords = SkyCoord(ra, dec, unit=(u.h, u.deg), frame='icrs') - pixel_coords = wcs_.world_to_pixel(sky_coords) - - x_coords, y_coords = pixel_coords - - # target_position = np.array(pixel_coords[0]) - # comparison_positions = np.array(pixel_coords[1:]) - target_position = (x_coords[0], y_coords[0]) - comparison_positions = list(zip(x_coords[1:], y_coords[1:])) - - hjd.append(header['HJD-OBS']) - bjd.append(header['BJD-OBS']) - - # Create the apertures and annuli - target_aperture = CircularAperture(target_position, r=aperture_radius) - target_annulus = CircularAnnulus(target_position, *annulus_radii) - - comparison_aperture = [CircularAperture(pos1, r=aperture_radius) for pos1 in comparison_positions] - comparison_annulus = [CircularAnnulus(pos2, *annulus_radii) for pos2 in comparison_positions] - target_phot_table = aperture_photometry(image_data, target_aperture) - # comparison_phot_table = aperture_photometry(image_data, comparison_aperture) - - if icount == 0: - im_plot(image_data, target_aperture, comparison_aperture, target_annulus, comparison_annulus) - # Create a figure and axis - _, ax = plt.subplots(figsize=(11, 8)) - - comparison_phot_table = [] - for comp_aperture, comp_annulus in zip(comparison_aperture, comparison_annulus): - # Perform aperture photometry on the star - aperture_phot_table = aperture_photometry(image_data, comp_aperture) - - # Perform aperture photometry on the annulus (background) - annulus_phot_table = aperture_photometry(image_data, comp_annulus) - - # Store the result in the comparison_phot_table list - comparison_phot_table.append((aperture_phot_table, annulus_phot_table)) - - # Perform annulus photometry to estimate the background - target_bkg_mean = ApertureStats(image_data, target_annulus).mean - # comparison_bkg_mean = ApertureStats(image_data, comparison_annulus).mean - - # Calculate the total background for the comparison stars - comparison_bkg_mean = [] - for annulus in comparison_annulus: - stats = ApertureStats(image_data, annulus) - if np.isnan(stats.mean) or np.isinf(stats.mean): - comparison_bkg_mean.append(0) - else: - comparison_bkg_mean.append(stats.mean) - - # Calculate the total background for the target star - if np.isnan(target_bkg_mean) or np.isinf(target_bkg_mean): - target_bkg_mean = 0 - - # Multiply the background mean by the aperture area to get the total background - target_bkg = target_bkg_mean * target_aperture.area - comparison_bkg = [bkg_mean * aperture.area for bkg_mean, aperture in - zip(comparison_bkg_mean, comparison_aperture)] - - # target_bkg = ApertureStats(image_data, target_aperture, local_bkg=target_bkg_mean).sum - # # comparison_bkg = ApertureStats(image_data, comparison_aperture, local_bkg=comparison_bkg_mean).sum - # - # comparison_bkg = [] - # for aperture, bkg_mean in zip(comparison_aperture, comparison_bkg_mean): - # stats = ApertureStats(image_data, aperture, local_bkg=bkg_mean) - # comparison_bkg.append(stats.sum) - - # Calculate the background subtracted counts - target_flx = target_phot_table['aperture_sum'] - target_bkg - target_flux_err = np.sqrt(target_phot_table['aperture_sum'] + target_aperture.area * read_noise**2) - # comparison_flx = comparison_phot_table['aperture_sum'] - comparison_bkg - # comp_flux_err = np.sqrt(comparison_phot_table['aperture_sum'] + comparison_aperture.area * read_noise ** 2) - comparison_flx = [phot_table[0]['aperture_sum'] - bkg - for phot_table, bkg in zip(comparison_phot_table, comparison_bkg)] - - comp_flux_err = [np.sqrt(phot_table[0]['aperture_sum'] + aperture.area * read_noise ** 2) - for phot_table, aperture in zip(comparison_phot_table, comparison_aperture)] - comp_flux_err = np.array(comp_flux_err) - - # calculate the relative flux for each comparison star and the target star - # rel_flx_T1 = target_flx / sum(comparison_flx) - count = 0 - rel_flux_comps = [] - for i in comparison_flx: - if i == comparison_flx[count]: - rel_flux_c = i / (sum(comparison_flx) - i) - rel_flux_comps.append(rel_flux_c) - count += 1 - - # rel_flux_comps = np.array(rel_flux_comps) - - # find the number of pixels used to estimate the sky background - # n_b = (np.pi * annulus_radii[1]**2) - (np.pi * annulus_radii[0] ** 2) # main equation - # n_b_mask_comp = comparison_annulus.to_mask(method="center") - # n_b_comp = np.sum(n_b_mask_comp) - - # n_b_mask_tar = target_annulus.to_mask(method="center") - # n_b_tar = np.sum(n_b_mask_tar.data) - - """ - # find the number of pixels used in the aperture if the radius of the apertures is in arcseconds not pixels - focal_length = 4114 # mm - pixel_size = 9 # microns - pixel_size = pixel_size * 10 ** -3 # mm - ap_area = np.pi * aperture_radius.area**2 # area of the aperture in mm^2 - plate_scale = 1/focal_length # rad/mm - plate_scale = plate_scale * 206265 # arcsec/mm - n_pix = ap_area / (plate_scale * pixel_size)**2 # number of pixels in the aperture - """ - """ - n_pix = np.pi * aperture_radius**2 # number of pixels in the aperture - - # Calculate the total noise - sigma_f = 0.289 # quoted from Collins 2017 https://iopscience.iop.org/article/10.3847/1538-3881/153/2/77/pdf - F_s = 0.01 # number of sky background counts per pixel in ADU - - # N_comp = np.sqrt(gain * comparison_flx + n_pix * (1 + (n_pix / n_b)) * - # (gain * F_s + F_dark + read_noise ** 2 + gain ** 2 + sigma_f ** 2)) / gain - N_comp = [np.sqrt(gain * flx + n_pix * (1 + (n_pix / n_b)) * - (gain * F_s + F_dark + read_noise ** 2 + gain ** 2 + sigma_f ** 2)) / gain - for flx in comparison_flx] - N_tar = np.sqrt(gain * target_flx + n_pix * (1 + (n_pix / n_b)) * - (gain * F_s + F_dark + read_noise ** 2 + gain ** 2 + sigma_f ** 2)) / gain - - # calculate the total comparison ensemble noise - N_e_comp = np.sqrt(np.sum(np.array(N_comp) ** 2)) - - rel_flux_err = (rel_flx_T1/rel_flux_comps)*np.sqrt((N_tar**2/target_flx**2) + - (N_e_comp**2/sum(comparison_flx)**2)) - """ - # calculate the total target magnitude and error - target_magnitude = (-np.log(sum(2.512**-magnitudes_comp))/np.log(2.512)) - \ - (2.5*np.log10(target_flx/sum(comparison_flx))) - - target_magnitude_error = 2.5*np.log10(1 + np.sqrt(((target_flux_err**2)/(target_flx**2)) + - (sum(comp_flux_err**2)/sum(comparison_flx)**2))) - - # comparison_magnitude = -(2.5*np.log10(target_flx/sum(comparison_flx))) - - # Append the calculated magnitude and error to the lists - magnitudes.append(target_magnitude.value[0]) - mag_err.append(target_magnitude_error.value[0]) - - # Plot the magnitudes with error bars - # noinspection PyUnboundLocalVariable - ax.errorbar(hjd, magnitudes, yerr=mag_err, fmt='o', label="Source_AMag_T1") - # ax.scatter(hjd, magnitudes, marker='o', color='black') - - # Set the labels and parameters - fontsize = 14 - ax.set_xlabel('HJD', fontsize=fontsize) - ax.set_ylabel('Source_AMag_T1', fontsize=fontsize) - ax.invert_yaxis() - ax.grid() - - ax.legend(loc="upper right", fontsize=fontsize).set_draggable(True) - ax.tick_params(axis='both', which='major', labelsize=fontsize) - - plt.show() - - light_curve_data = pd.DataFrame({ - 'HJD': hjd, - 'BJD': bjd, - 'Source_AMag_T1': magnitudes, - 'Source_AMag_T1_Error': mag_err - }) - - if not pipeline: - output_file = input("Enter an output file name and location for the final light curve data in the {} filter " - "(ex: C:\\folder1\\folder2\\APASS_254037_B.txt): ".format(filter)) - else: - output_file = path + "//APASS_254037_" + filter + "_LC_dat.txt" - - light_curve_data.to_csv(output_file, index=False) + magnitudes_comp = df[4].replace(99.999, pd.NA).dropna().reset_index(drop=True) + ra = df[0] + dec = df[1] + + magnitudes = [] + mag_err = [] + hjd = [] + bjd = [] + + for icount, image_file in tqdm(enumerate(image_list), desc=f"Processing {filter} images"): + if cancel_event and cancel_event.is_set(): + log("Task canceled during photometry.") + return + + log(f"Processing {image_file}") + + image_data, header = fits.getdata(path / image_file, header=True) + # All the following up till the 'if' statement stays under the for loop due to needing the header information + wcs_ = WCS(header) + + # ccd = CCDData(image_data, wcs=wcs, unit='adu') + + # Convert RA and DEC to pixel positions + sky_coords = SkyCoord(ra, dec, unit=(u.h, u.deg), frame='icrs') + pixel_coords = wcs_.world_to_pixel(sky_coords) + + x_coords, y_coords = pixel_coords + + # target_position = np.array(pixel_coords[0]) + # comparison_positions = np.array(pixel_coords[1:]) + target_position = (x_coords[0], y_coords[0]) + comparison_positions = list(zip(x_coords[1:], y_coords[1:])) + + hjd.append(header['HJD-OBS']) + bjd.append(header['BJD-OBS']) + + # Create the apertures and annuli + target_aperture = CircularAperture(target_position, r=aperture_radius) + target_annulus = CircularAnnulus(target_position, *annulus_radii) + + comparison_aperture = [CircularAperture(pos1, r=aperture_radius) for pos1 in comparison_positions] + comparison_annulus = [CircularAnnulus(pos2, *annulus_radii) for pos2 in comparison_positions] + target_phot_table = aperture_photometry(image_data, target_aperture) + # comparison_phot_table = aperture_photometry(image_data, comparison_aperture) + + # if icount == 0: + # im_plot(image_data, target_aperture, comparison_aperture, target_annulus, comparison_annulus) + # # Create a figure and axis + # _, ax = plt.subplots(figsize=(11, 8)) + + comparison_phot_table = [] + for comp_aperture, comp_annulus in zip(comparison_aperture, comparison_annulus): + # Perform aperture photometry on the star + aperture_phot_table = aperture_photometry(image_data, comp_aperture) + + # Perform aperture photometry on the annulus (background) + annulus_phot_table = aperture_photometry(image_data, comp_annulus) + + # Store the result in the comparison_phot_table list + comparison_phot_table.append((aperture_phot_table, annulus_phot_table)) + + # Perform annulus photometry to estimate the background + target_bkg_mean = ApertureStats(image_data, target_annulus).mean + # comparison_bkg_mean = ApertureStats(image_data, comparison_annulus).mean + + # Calculate the total background for the comparison stars + comparison_bkg_mean = [] + for annulus in comparison_annulus: + stats = ApertureStats(image_data, annulus) + if np.isnan(stats.mean) or np.isinf(stats.mean): + comparison_bkg_mean.append(0) + else: + comparison_bkg_mean.append(stats.mean) + + # Calculate the total background for the target star + if np.isnan(target_bkg_mean) or np.isinf(target_bkg_mean): + target_bkg_mean = 0 + + # Multiply the background mean by the aperture area to get the total background + target_bkg = target_bkg_mean * target_aperture.area + comparison_bkg = [bkg_mean * aperture.area for bkg_mean, aperture in + zip(comparison_bkg_mean, comparison_aperture)] + + # target_bkg = ApertureStats(image_data, target_aperture, local_bkg=target_bkg_mean).sum + # # comparison_bkg = ApertureStats(image_data, comparison_aperture, local_bkg=comparison_bkg_mean).sum + # + # comparison_bkg = [] + # for aperture, bkg_mean in zip(comparison_aperture, comparison_bkg_mean): + # stats = ApertureStats(image_data, aperture, local_bkg=bkg_mean) + # comparison_bkg.append(stats.sum) + + # Calculate the background subtracted counts + target_flx = target_phot_table['aperture_sum'] - target_bkg + target_flux_err = np.sqrt(target_phot_table['aperture_sum'] + target_aperture.area * read_noise ** 2) + # comparison_flx = comparison_phot_table['aperture_sum'] - comparison_bkg + # comp_flux_err = np.sqrt(comparison_phot_table['aperture_sum'] + comparison_aperture.area * read_noise ** 2) + comparison_flx = [phot_table[0]['aperture_sum'] - bkg + for phot_table, bkg in zip(comparison_phot_table, comparison_bkg)] + + comp_flux_err = [np.sqrt(phot_table[0]['aperture_sum'] + aperture.area * read_noise ** 2) + for phot_table, aperture in zip(comparison_phot_table, comparison_aperture)] + comp_flux_err = np.array(comp_flux_err) + + # calculate the relative flux for each comparison star and the target star + # rel_flx_T1 = target_flx / sum(comparison_flx) + count = 0 + rel_flux_comps = [] + for i in comparison_flx: + if i == comparison_flx[count]: + rel_flux_c = i / (sum(comparison_flx) - i) + rel_flux_comps.append(rel_flux_c) + count += 1 + + # rel_flux_comps = np.array(rel_flux_comps) + + # find the number of pixels used to estimate the sky background + # n_b = (np.pi * annulus_radii[1]**2) - (np.pi * annulus_radii[0] ** 2) # main equation + # n_b_mask_comp = comparison_annulus.to_mask(method="center") + # n_b_comp = np.sum(n_b_mask_comp) + + # n_b_mask_tar = target_annulus.to_mask(method="center") + # n_b_tar = np.sum(n_b_mask_tar.data) + + """ + # find the number of pixels used in the aperture if the radius of the apertures is in arcseconds not pixels + focal_length = 4114 # mm + pixel_size = 9 # microns + pixel_size = pixel_size * 10 ** -3 # mm + ap_area = np.pi * aperture_radius.area**2 # area of the aperture in mm^2 + plate_scale = 1/focal_length # rad/mm + plate_scale = plate_scale * 206265 # arcsec/mm + n_pix = ap_area / (plate_scale * pixel_size)**2 # number of pixels in the aperture + """ + """ + n_pix = np.pi * aperture_radius**2 # number of pixels in the aperture + + # Calculate the total noise + sigma_f = 0.289 # quoted from Collins 2017 https://iopscience.iop.org/article/10.3847/1538-3881/153/2/77/pdf + F_s = 0.01 # number of sky background counts per pixel in ADU + + # N_comp = np.sqrt(gain * comparison_flx + n_pix * (1 + (n_pix / n_b)) * + # (gain * F_s + F_dark + read_noise ** 2 + gain ** 2 + sigma_f ** 2)) / gain + N_comp = [np.sqrt(gain * flx + n_pix * (1 + (n_pix / n_b)) * + (gain * F_s + F_dark + read_noise ** 2 + gain ** 2 + sigma_f ** 2)) / gain + for flx in comparison_flx] + N_tar = np.sqrt(gain * target_flx + n_pix * (1 + (n_pix / n_b)) * + (gain * F_s + F_dark + read_noise ** 2 + gain ** 2 + sigma_f ** 2)) / gain + + # calculate the total comparison ensemble noise + N_e_comp = np.sqrt(np.sum(np.array(N_comp) ** 2)) + + rel_flux_err = (rel_flx_T1/rel_flux_comps)*np.sqrt((N_tar**2/target_flx**2) + + (N_e_comp**2/sum(comparison_flx)**2)) + """ + # calculate the total target magnitude and error + target_magnitude = (-np.log(sum(2.512 ** -magnitudes_comp)) / np.log(2.512)) - \ + (2.5 * np.log10(target_flx / sum(comparison_flx))) + + target_magnitude_error = 2.5 * np.log10(1 + np.sqrt(((target_flux_err ** 2) / (target_flx ** 2)) + + (sum(comp_flux_err ** 2) / sum(comparison_flx) ** 2))) + + # comparison_magnitude = -(2.5*np.log10(target_flx/sum(comparison_flx))) + + # Append the calculated magnitude and error to the lists + magnitudes.append(target_magnitude.value[0]) + mag_err.append(target_magnitude_error.value[0]) + + # Plot the magnitudes with error bars + _, ax = plt.subplots(figsize=(11, 8)) + # noinspection PyUnboundLocalVariable + ax.errorbar(hjd, magnitudes, yerr=mag_err, fmt='o', label="Source_AMag_T1") + # ax.scatter(hjd, magnitudes, marker='o', color='black') + + # Set the labels and parameters + fontsize = 14 + ax.set_xlabel('HJD', fontsize=fontsize) + ax.set_ylabel('Source_AMag_T1', fontsize=fontsize) + ax.invert_yaxis() + ax.grid() + + ax.legend(loc="upper right", fontsize=fontsize).set_draggable(True) + ax.tick_params(axis='both', which='major', labelsize=fontsize) + + filter_letter = filter.split("/") + plt.savefig(path / f"{obj_name}_{filter_letter[1]}_figure.jpg") + + # Save the data + light_curve_data = pd.DataFrame({ + 'HJD': hjd, + 'BJD': bjd, + 'Source_AMag_T1': magnitudes, + 'Source_AMag_T1_Error': mag_err + }) + + output_file = path / f"{obj_name}_{filter_letter[1]}_data.csv" + light_curve_data.to_csv(output_file, index=False) + log(f"Saved light curve data for {filter} to {output_file}") + + except Exception as e: + log(f"Error during photometry for {filter}: {e}") def im_plot(image_data, target_aperture, comparison_apertures, target_annulus, comparison_annuli): diff --git a/EclipsingBinaries/splash_screen.py b/EclipsingBinaries/splash_screen.py new file mode 100644 index 0000000..cdece80 --- /dev/null +++ b/EclipsingBinaries/splash_screen.py @@ -0,0 +1,151 @@ +""" + A splash screen that greets the user when they first start the package and displays a progressbar of the packages + being imported + +Author: Kyle Koeller +Created: 12/16/2024 +Last Updated: 12/16/2024 +""" + +import tkinter as tk +from tkinter import ttk +import threading +import queue +import time + + +def import_packages(progress_queue): + """ + Simulate importing packages and update progress. + Replace `time.sleep()` with actual imports in real scenarios. + """ + packages = [ + "Importing tkinter...", + "Importing pathlib...", + "Importing astropy...", + "Importing matplotlib...", + "Importing Custom Scripts...", + "Finishing..." + ] + total = len(packages) + + try: + # Simulate importing with delays + import tkinter.messagebox # Simulate step 1 + progress_queue.put((1, total, packages[0])) + time.sleep(0.5) + + from pathlib import Path # Simulate step 2 + progress_queue.put((2, total, packages[1])) + time.sleep(0.5) + + from astropy.nddata import CCDData # Simulate step 3 + progress_queue.put((3, total, packages[2])) + time.sleep(0.5) + + from matplotlib import pyplot as plt # Simulate step 4 + progress_queue.put((4, total, packages[3])) + time.sleep(0.5) + + # Custom scripts + from IRAF_Reduction import run_reduction + from tess_data_search import run_tess_search + from apass import comparison_selector + from multi_aperture_photometry import main as multi_ap + progress_queue.put((5, total, packages[4])) + time.sleep(0.5) + + except Exception as e: + print(f"Error importing packages: {e}") + + # Signal completion + progress_queue.put(None) + + +class SplashScreen(tk.Toplevel): + """ + Splash Screen class to display the progress bar and messages during package loading. + """ + def __init__(self, root, progress_queue, on_close_callback): + super().__init__(root) + self.progress_queue = progress_queue + self.on_close_callback = on_close_callback + self.configure(bg="#003366") + + # Center the splash screen + screen_width = self.winfo_screenwidth() + screen_height = self.winfo_screenheight() + splash_width = int(screen_width * 0.4) + splash_height = int(screen_height * 0.3) + x_position = (screen_width - splash_width) // 2 + y_position = (screen_height - splash_height) // 2 + self.geometry(f"{splash_width}x{splash_height}+{x_position}+{y_position}") + self.overrideredirect(True) # Hide title bar + + # Splash screen UI elements + self.title_label = tk.Label(self, text="EclipsingBinaries", font=("Helvetica", 20, "bold"), fg="white", bg="#003366") + self.title_label.pack(pady=10) + + self.message_label = tk.Label(self, text="Initializing...", font=("Helvetica", 14), fg="white", bg="#003366") + self.message_label.pack(pady=10) + + self.progress = ttk.Progressbar(self, orient="horizontal", length=int(splash_width * 0.8), mode="determinate") + self.progress.pack(pady=20) + + # Start monitoring the queue + self.after(100, self.monitor_progress) + + def monitor_progress(self): + """ + Monitor the progress queue and update the splash screen. + """ + try: + progress_data = self.progress_queue.get_nowait() + if progress_data is None: + self.on_close_callback() + self.destroy() # Close the splash screen when done + else: + current, total, message = progress_data + self.progress["value"] = (current / total) * 100 + self.message_label.config(text=message) + self.after(100, self.monitor_progress) # Check again after 100 ms + except queue.Empty: + self.after(100, self.monitor_progress) # Check again if the queue is empty + + +def launch_main_gui(): + """ + Launch the main GUI after the splash screen. + """ + from menu import ProgramLauncher # Import the main GUI + app = ProgramLauncher() + app.mainloop() + + +def main(): + # Create a hidden root window for the splash screen + root = tk.Tk() + root.withdraw() + + # Create a queue for progress updates + progress_queue = queue.Queue() + + def start_main_gui(): + root.quit() # Exit the splash screen's event loop + + # Initialize the splash screen + SplashScreen(root, progress_queue, on_close_callback=start_main_gui) + + # Start a thread to simulate importing modules + threading.Thread(target=import_packages, args=(progress_queue,), daemon=True).start() + + # Run the splash screen's event loop + root.mainloop() + + # Once the splash screen closes, launch the main GUI + root.destroy() # Clean up the splash screen's hidden root window + launch_main_gui() + + +if __name__ == "__main__": + main() diff --git a/EclipsingBinaries/version.py b/EclipsingBinaries/version.py index 050f70f..bfd2e90 100644 --- a/EclipsingBinaries/version.py +++ b/EclipsingBinaries/version.py @@ -1,3 +1,3 @@ # Version -__version__ = "5.0.0a4" +__version__ = "5.0.0a5" diff --git a/setup.cfg b/setup.cfg index ba83ccf..fcd8922 100644 --- a/setup.cfg +++ b/setup.cfg @@ -98,7 +98,7 @@ select = E101,E111,E112,E113,E221,E222,E223,E224,E225,E241,E242,E251,E271,E272,E [options.entry_points] console_scripts = - EclipsingBinaries = EclipsingBinaries.menu:main + EclipsingBinaries = EclipsingBinaries.splash_screen:main EB_pipeline = EclipsingBinaries.pipeline:monitor_directory EB_HCorr = EclipsingBinaries.headerCorrect:header_correct