"""
High-Resolution Spectroscopy generation GUI Panel
This module provides a comprehensive graphical user interface for configuring and
executing synthetic high-resolution spectroscopic observations of exoplanets.
The GUI panel allows users to:
- Configure target parameters and observational setup
- Set atmospheric conditions and instrumental parameters
- Control PWV (precipitable water vapor) modeling
- Select radiation transfer modes and cross-correlation models
- Execute dataset generations with real-time progress monitoring
- View diagnostic plots and results
Classes:
UpdatableImage: Dynamic image widget for displaying generation results
FrameGenerationHRData: Main GUI panel for HR generation configuration
Functions:
ask_stellar_params: Convenience wrapper for stellar parameter dialog
"""
import pickle
import subprocess
from pathlib import Path
from tkinter import messagebox
import tkinter as tk
from GUIBRUSHR.GUI.DataInterface.DataInterface import get_target_nights, get_target_instruments, get_target_list, \
get_list_cross_corr_models, get_target_info
from GUIBRUSHR.GUI.DataInterface.DBSQLite3 import DBSQLite3
from GUIBRUSHR.GUI.Input_Output_Panels.Input_Panels.TabPanels.FrameGenerationHRData.shuffle_HR import shuffle_HR
from GUIBRUSHR.GUI.Input_Output_Panels.Input_Panels.TabPanels.FrameGenerationHRData.MathematicalFunctions import MathematicalFunctions
from GUIBRUSHR.GUI.LAYOUT.MyPanel import MyPanel
from GUIBRUSHR.GUI.LAYOUT.MyTabPanel import MyTabPanel
from GUIBRUSHR.GUI.LAYOUT.ScaleManager import ScaleManager
from GUIBRUSHR.GUI.WIDGET.MyButton import MyButton
from GUIBRUSHR.GUI.WIDGET.MyCheckBox import MyCheckBox
from GUIBRUSHR.GUI.WIDGET.MyDropDown import MyDropdown
from GUIBRUSHR.GUI.WIDGET.MyEntry import MyEntry
from GUIBRUSHR.GUI.WIDGET.MyImage import MyImage
from GUIBRUSHR.GUI.WIDGET.MyLabel import MyLabel
from GUIBRUSHR.GUI.WIDGET.MyTextField import MyTextField
from GUIBRUSHR.GUI.WIDGET.MyPDFWindow import MyPDFWindow
from GUIBRUSHR.General_Constants.Classes.Target import Target
from GUIBRUSHR.General_Constants.Classes.HelpButton import HelpButton
from GUIBRUSHR.General_Constants.FunctionsAndConstants.Constant_Variables import ConstantVariables
from GUIBRUSHR.General_Constants.FunctionsAndConstants.general_functions import create_path_night, \
download_star_spectrum
# ----------------------------------------------------------------------
# Custom updatable image widget
# ----------------------------------------------------------------------
[docs]
class UpdatableImage(MyImage):
"""
A specialized image widget that supports dynamic updating of displayed images.
This class extends MyImage to provide the ability to change the displayed
image at runtime, which is useful for showing updated plots, diagnostic
images, or simulation results without recreating the widget.
Inherits from MyImage and maintains all its functionality while adding
dynamic update capabilities through PIL/Pillow image processing.
"""
[docs]
def __init__(self, parent: tk.Widget, row: int, column: int, width: int, height: int,
initial_path: str, **kwargs) -> None:
"""
Initialize the updatable image widget.
Parameters
----------
parent : tk.Widget
Parent widget container
row : int
Row position in the parent's grid layout
column : int
Column position in the parent's grid layout
width : int
Desired image width in pixels
height : int
Desired image height in pixels
initial_path : str
Path to the initial image file to display
**kwargs
Additional keyword arguments passed to MyImage parent class
Notes
-----
The widget automatically resizes images to fit the specified dimensions
while maintaining aspect ratio handling as configured in the parent class.
"""
super().__init__(parent, row, column, width, height, initial_path, **kwargs)
[docs]
def update_image(self, new_path: str) -> None:
"""
Update the displayed image with a new image file.
Dynamically loads and displays a new image while preserving the widget's
position and configuration. The new image is automatically resized to
match the widget's dimensions.
Parameters
----------
new_path : str
Path to the new image file to display. Supports common formats
including PNG, JPEG, GIF, BMP, and TIFF.
Raises
------
FileNotFoundError
If the specified image file cannot be found
PIL.UnidentifiedImageError
If the file is not a valid image format
Exception
For other image loading or display errors
Notes
-----
The method handles both canvas-based and label-based image display
mechanisms automatically. A reference to the new image is maintained
to prevent garbage collection.
"""
try:
from PIL import Image, ImageTk
# Create new PhotoImage
new_image = ImageTk.PhotoImage(
Image.open(new_path).resize((int(self.width), int(self.height)))
)
# Update the image
if hasattr(self, 'canvas'):
# Using canvas
self.canvas.delete("all")
self.canvas.create_image(0, 0, anchor=tk.NW, image=new_image)
self.image = new_image # Keep reference
else:
# Using label - need to find the label widget
for child in self.winfo_children():
if isinstance(child, tk.Label):
child.configure(image=new_image)
child.image = new_image # Keep reference
break
self.image = new_image
except Exception as e:
print(f"Error updating image: {e}")
[docs]
class FrameGenerationHRData(MyPanel):
"""
Comprehensive GUI panel for configuring and executing high-resolution spectroscopy generations.
This panel provides a complete user interface for setting up synthetic exoplanet
observations with full control over:
**Core Configuration:**
- Target selection and orbital parameters
- Instrument selection and spectral orders
- Radiation transfer modes (transmission/emission)
- Cross-correlation model selection
**Atmospheric Modeling:**
- PWV (precipitable water vapor) functions and time series
- Observatory parameters and atmospheric conditions
- Telluric absorption modeling with SkyCalc integration
- Airmass calculations and observational coordinates
**Advanced Options:**
- Wavelength coverage and resolution settings
- Stellar parameter configuration via dialogs
- Discontinuous vs continuous PWV modeling
- Real-time simulation monitoring and progress tracking
**Output Management:**
- FITS file generation with proper headers
- Diagnostic plot creation and PDF viewing
- Result visualization and quality assessment
- Background process management for long generations
The panel integrates with the broader GUIBRUSHR framework and maintains
compatibility with standard astronomical data reduction pipelines.
Attributes
----------
synthetic_process : subprocess.Popen or None
Background process handle for running generations
synthetic_running : bool
Flag indicating if a generation is currently executing
math_functions : MathematicalFunctions
Utility class for PWV and atmospheric function calculations
"""
[docs]
def __init__(
self,
parent: tk.Widget,
color: str,
row: int,
column: int,
path_default: str,
frame_input,
window: tk.Toplevel,
width_GUI: int,
height_frame: int,
**kwargs
) -> None:
"""
Initialize the FrameGenerationHRData panel with all UI components and configurations.
Parameters
----------
parent : tk.Widget
Parent widget container for this panel
color : str
Background color for the panel (hex color code or named color)
row : int
Row position in the parent widget's grid layout
column : int
Column position in the parent widget's grid layout
path_default : str
Root path to GUIBRUSHR directory structure containing models,
telluric grids, and configuration files
frame_input : FrameInput
Input frame object containing target folder path and related
configuration information
window : tk.Toplevel
Main application window for parent references and modal dialogs
width_GUI : int
Total width of the GUI in pixels for layout calculations
height_frame : int
Height allocated for this frame in pixels
**kwargs : dict
Additional keyword arguments passed to MyPanel parent class
Notes
-----
The initialization process:
1. Sets up all UI component references and instance variables
2. Loads initial target list from the specified folder
3. Initializes dropdown menus with available options
4. Creates atmospheric parameter panels and controls
5. Sets up background process monitoring
6. Configures mathematical function utilities
The panel automatically adapts to available targets and instruments
found in the specified data directory structure.
"""
super().__init__(parent, color, row, column, **kwargs)
# === Core UI component references ===
self.target_information = None
self.dropdown_observatory_coords = None
self.terminator_night = None # Night termination handler
self.dropdown_target = None # Target selection dropdown
self.dropdown_hr_instrument = None # HR instrument dropdown
self.list_hr_instrument = None # Available HR instruments list
self.dropdown_cross_corr = None # Cross-correlation model dropdown
# === Framework and window references ===
self.parent = parent
self.color = color
self.path_folder_targets = frame_input.frame_path.path_folder_targets.get_text()
self.window = window
self.path_default = path_default
self.frame_input = frame_input
self.width_GUI = width_GUI
self.height_frame = height_frame
# === File and path management variables ===
self.folder_retrieval = None # Folder retrieval settings
self.prefix_file = None # File prefix for outputs
self.underscore = None # Underscore formatting option
self.p = None # Processing parameter
self.filepng = None # PNG file path for plots
self.lista_parametri = None # Parameter list storage
self.path_imm = None # Image path reference
self.last_plot_reduced = None # Last reduced plot reference
# === Background process management ===
self.synthetic_process = None # Subprocess handle for generations
self.synthetic_running = False # generation execution flag
# === Mathematical function utilities ===
self.math_functions = MathematicalFunctions()
# === Initialize UI components ===
# Load available targets from data directory
target_list = get_target_list(self.path_folder_targets)
# Set up all UI panels and controls
self._initialize_ui_components(target_list)
self._initialize_atmospheric_parameters()
# Global exception handling flag
self.global_exc = 1
def _initialize_ui_components(self, target_list: list) -> None:
"""
Initialize all primary UI components with their default values and layout.
Creates the main control panels and dropdown menus for target selection,
instrument configuration, and basic generation parameters. This method
establishes the foundation UI elements that other initialization methods
build upon.
Parameters
----------
target_list : list
List of available target names loaded from the data directory
Notes
-----
The initialization order is important as later dropdowns depend on
selections made in earlier ones. The method creates:
1. Main controls panel with light blue background
2. Target selection dropdown with auto-population
3. Instrument selection based on first target
4. Nights text field with available observation nights
5. Radiation transfer mode selection
"""
# Create main panel container with light blue background for visual separation
self.main_controls_panel = MyPanel(self, "#F0F8FF", 0, 1, columnspan=12, rowspan=3)
# Help button for main controls
help_text_main = {
"Target": "Select the exoplanet target for which to generate simulated high-resolution spectra. The target must be previously configured in the database with all orbital and physical parameters.",
"Nights": "Observation dates to simulate, in YYYY-MM-DD format. Multiple nights can be comma-separated (e.g., '2023-05-15,2023-05-16'). Each night generates a separate simulated dataset with time-dependent atmospheric conditions.",
"Rad. Transf. Mode": "Radiative transfer mode: 'Transmission' simulates primary transit observations (planetary atmosphere backlit by star, showing absorption features). 'Emission' simulates secondary eclipse observations (planetary thermal emission and reflected starlight).",
"HR Instrument": "High-resolution spectrograph to simulate. The instrument's resolving power, wavelength coverage, and line spread function will be applied to the synthetic spectra to match real observational characteristics.",
"Model CC": "Atmospheric model for the planetary spectrum to inject. Select from pre-computed petitRADTRANS models in the target's Models directory. Different models represent various atmospheric compositions, temperature profiles, or cloud properties.",
"Order Selection": "Mainly used for internal file structure: Specify which spectral orders to search.",
"Tell rm method": "Mainly used for internal file structure: Telluric removal algorithm."
}
HelpButton(
self.main_controls_panel, 0, 0,
"Main Controls Help",
help_text_main,
columnspan=1
)
# Target selection dropdown
self.dropdown_target = MyDropdown(self.main_controls_panel, 0, 1, target_list, "Target:", color=self.main_controls_panel.cget("bg"), columnspan=2)
# Get initial instrument list and nights
self.list_hr_instrument = get_target_instruments(
self.path_folder_targets, target_list[0], "HR", "Transmission", Path(__file__).name
)
nights = get_target_nights(
self.path_folder_targets,
target_list[0],
"Transmission",
self.list_hr_instrument[0],
0
)
# Nights text field
self.textfield_nights = MyTextField(self.main_controls_panel, 0, 3, nights, "Nights:", color=self.main_controls_panel.cget("bg"), columnspan=4, width=80)
# Radiation transfer mode dropdown
self.dropdown_rad_mode = MyDropdown(
self.main_controls_panel, 0, 7, ConstantVariables.LIST_RAD_MODE, "Rad. Transf. Mode:",
initial_value=0, color=self.main_controls_panel.cget("bg"), columnspan=2
)
# HR instrument dropdown
self.dropdown_hr_instrument = MyDropdown(
self.main_controls_panel, 0, 9, self.list_hr_instrument, "HR Instrument:",
color=self.main_controls_panel.cget("bg"), columnspan=2
)
# Cross-correlation model dropdown
dict_list_cc = get_list_cross_corr_models(self.path_folder_targets, target_list[0], "Transmission")
self.dropdown_cross_corr = MyDropdown(
self.main_controls_panel, 0, 11, list(sorted(dict_list_cc.keys())),
"Model CC", initial_value=0,
color=self.main_controls_panel.cget("bg"), columnspan=2
)
# Order selection text field
self.textfield_order_selection = MyTextField(
self.main_controls_panel, 1, 1, "full", "Order Selection \n(only for shuffle):",
color=self.main_controls_panel.cget("bg"), columnspan=3, width=20
)
# Telluric removal method dropdown
self.dropdown_tell_rm_method = MyDropdown(
self.main_controls_panel, 1, 4, ConstantVariables.LIST_TELL_TABLE,
"Tell rm method \n(only for shuffle):", color=self.main_controls_panel.cget("bg"), columnspan=2
)
# Set up callbacks
self.dropdown_hr_instrument.set_callback(self.update_nights)
self.dropdown_rad_mode.set_callback(self.update_from_target_or_rad_mode)
self.dropdown_target.set_callback(self.update_from_target_or_rad_mode)
def _initialize_atmospheric_parameters(self):
"""
Initialize atmospheric parameter panels and widgets using tab structure.
Creates a tabbed interface for organizing atmospheric and observational
parameters. Sets up PWV parameters panel and creates additional panels
for atmospheric, observational, wavelength, observatory, and coordinates
parameters within the tab structure.
"""
# Create tab manager for organizing atmospheric and observational parameters
self.param_tab_manager = MyTabPanel(
parent=self,
tab_list=ConstantVariables.list_tab_simulated_hr,
row=4,
column=4, width=int(self.width_GUI / 2), rowspan=15,
)
# First tab - Basic Configuration with PWV parameters and generation controls
# Create a panel inside the first tab for PWV parameters
self.pwv_panel = MyPanel(self.param_tab_manager.tab_list[0], "#E8F4FD", 0, 1, columnspan=10, rowspan=3)
# Help button for PWV parameters
help_text_pwv = {
"dt": "Time step in seconds for PWV evolution. Smaller values create more detailed temporal variations. Recommended: 1 second for high-fidelity simulations.",
"pace": "PWV correlation timescale parameter in seconds.",
"s": "Shape parameter for the PWV distribution function. Controls the steepness of PWV variations. Typical values: 0.5-1.0. Default: 0.69598.",
"scale": "cale parameter for the random component, by default 0.69598. Controls the amplitude of PWV variations.",
"tExp": "Exposure time in seconds for each spectral frame. Used to average PWV over the exposure duration, simulating realistic integration effects. Typical: 300-3600 seconds.",
"Seed": "Random number generator seed for reproducible PWV time series. Using the same seed with identical parameters produces identical PWV variations. Set to any integer for reproducibility."
}
HelpButton(
self.pwv_panel, 0, 0,
"PWV Parameters Help",
help_text_pwv,
columnspan=1
)
# Header for PWV parameters
MyLabel(
self.pwv_panel, 0, 1,
color=self.pwv_panel.cget("bg"),
label_text="PWV PARAMETERS", font=ScaleManager.get().font_label_large if ScaleManager.get() else ("Sans", 12, "bold"), columnspan=10
)
# PWV parameter entry widgets arranged in a single row
self.entry_dt = MyEntry(
self.pwv_panel, 1, 1, "1", label_text="dt:",
color=self.pwv_panel.cget("bg"), columnspan=2
)
self.entry_pace = MyEntry(
self.pwv_panel, 1, 3, "6e5", label_text="pace:",
color=self.pwv_panel.cget("bg"), columnspan=2
)
self.entry_s = MyEntry(
self.pwv_panel, 1, 5, "0.69598", label_text="s:",
color=self.pwv_panel.cget("bg"), columnspan=2
)
self.entry_scale = MyEntry(
self.pwv_panel, 1, 7, "3.0862", label_text="scale:",
color=self.pwv_panel.cget("bg"), columnspan=2
)
self.entry_tExp = MyEntry(
self.pwv_panel, 1, 9, "1800", label_text="tExp:",
color=self.pwv_panel.cget("bg"), columnspan=2
)
# Add seed entry parameter for reproducible random number generation
self.entry_seed = MyEntry(
self.pwv_panel, 1, 11, "42", label_text="Seed:",
color=self.pwv_panel.cget("bg"), columnspan=2
)
# Create generation parameters panel under PWV panel in first tab
self.generation_params_panel = MyPanel(self.param_tab_manager.tab_list[0], "#F0F8FF", 3, 1, columnspan=10, rowspan=2)
# Help button for generation parameters
help_text_sim = {
"Gain": "Detector gain factor (electrons/ADU). Multiplies the photon counts to convert to detector units. Typical CCD gains: 1-5.",
"Error Type": "'Error from reduction': Uses real error estimates from actual data reduction. 'Synthetic error': Generates theoretical noise based on photon statistics. Use synthetic for idealized generations.",
"Noise Factor": "Multiplicative factor for error bars. Increases (>1) or decreases (<1) the noise level relative to theoretical/measured values. Use to simulate better/worse observing conditions. Default: 1 (realistic noise).",
"Save Plots": "When enabled, generates and saves diagnostic plots during generation including. Useful for quality assessment but increases processing time.",
"Include 0.1 PWV": "When enabled, adds an additional simulated night with very low PWV (0.1 mm), representing exceptionally dry conditions. This value should not be used due to possible errors from the original skycalc simulator."
}
HelpButton(
self.generation_params_panel, 0, 0,
"Generation Parameters Help",
help_text_sim,
columnspan=1
)
# Header for generation parameters
MyLabel(
self.generation_params_panel, 0, 1,
color=self.generation_params_panel.cget("bg"),
label_text="GENERATIONS PARAMETERS", font=ScaleManager.get().font_label_large if ScaleManager.get() else ("Sans", 12, "bold"), columnspan=10
)
# Gain entry - Controls the gain factor for the generation
self.entry_gain = MyEntry(
self.generation_params_panel, 1, 1, "1.0", "Gain:",
color=self.generation_params_panel.cget("bg"), columnspan=2
)
# Error type dropdown - Controls the type of error generation
self.dropdown_error_type = MyDropdown(
self.generation_params_panel, 1, 3, ["Error from reduction", "Synthetic error"],
"Error Type:", color=self.generation_params_panel.cget("bg"), columnspan=2
)
# Noise factor entry - Controls the amplitude of synthetic noise
self.entry_noise_factor = MyEntry(
self.generation_params_panel, 1, 5, "1", "Noise Factor:",
color=self.generation_params_panel.cget("bg"), columnspan=2
)
# Checkboxes
self.checkbox_save_plots = MyCheckBox(
self.generation_params_panel, 1, 7, "Save Plots:"
)
self.checkbox_include_0_1 = MyCheckBox(
self.generation_params_panel, 1, 9, "Include 0.1 PWV:"
)
# Second tab contains the advanced parameters with a nested tab manager
# Create a nested tab manager inside the "Advanced Parameters" tab
self.advanced_tab_manager = MyTabPanel(
parent=self.param_tab_manager.tab_list[1],
tab_list=ConstantVariables.list_tab_simulated_hr_advanced,
row=0,
column=0,
width=int(self.width_GUI / 2),
rowspan=15,
)
# Create panels within their respective tabs in the advanced tab manager
self.atm_panel = MyPanel(self.advanced_tab_manager.tab_list[0], "#E8F4FD", 0, 0, columnspan=15, rowspan=15)
self.obs_panel = MyPanel(self.advanced_tab_manager.tab_list[1], "#FDF4E8", 0, 0, columnspan=15, rowspan=15)
self.wl_panel = MyPanel(self.advanced_tab_manager.tab_list[2], "#E8FDF4", 0, 0, columnspan=15, rowspan=15)
self.observatory_panel = MyPanel(self.advanced_tab_manager.tab_list[3], "#F4E8FD", 0, 0, columnspan=15, rowspan=15)
# Keep coordinates panel in main frame for now
self.coordinates_panel = MyPanel(self, "#FDE8F4", 4, 5, columnspan=8, rowspan=2)
# Initialize each panel
self._initialize_atmospheric_panel()
self._initialize_observational_panel()
self._initialize_wavelength_panel()
self._initialize_observatory_panel()
self._initialize_coordinates_panel()
# Add generation controls (dropdown and run button) in main frame
self._initialize_generation_controls()
def _initialize_generation_controls(self):
"""
Initialize generation mode dropdown and run button in main frame.
Creates the generation mode selection and run button that need to stay
in the main frame for easy access.
"""
# Create a new panel for generation controls in main frame
self.main_generation_controls_panel = MyPanel(self, "#F0F8FF", 6, 5, columnspan=8, rowspan=1)
# Help button for generation controls
help_text_sim_ctrl = {
"Generation Mode": "Select the generation type:\n\n'Synthetic': Generates fully synthetic data with continuous PWV evolution using the specified PWV function. Creates realistic time-varying telluric absorption.\n\n'Synthetic Discontinuous PWV': Similar to Synthetic but with discontinuous PWV jumps between exposures instead of smooth evolution. Simpler but less realistic.\n\n'Shuffle': Injects planetary signal into existing reduced observational data where each phases is shuffled. Requires real data to already exist for the selected target/night/instrument combination."
}
HelpButton(
self.main_generation_controls_panel, 0, 0,
"Generation Mode Help",
help_text_sim_ctrl,
columnspan=1
)
# Generation mode dropdown
self.dropdown_generation_mode = MyDropdown(
self.main_generation_controls_panel, 0, 1,
["Synthetic", "Synthetic Discontinuous PWV", "Shuffle"],
"Generation Mode:",
color=self.main_generation_controls_panel.cget("bg"), columnspan=3
)
# Run button
self.button_run = MyButton(
self.main_generation_controls_panel, 0, 4, 'RUN', '#4CAF50',
self.run_selected_generation, color_panel=self.main_generation_controls_panel.cget("bg"), columnspan=1, sticky="we"
)
def _initialize_atmospheric_panel(self):
"""
Initialize atmospheric parameters panel.
Creates the atmospheric parameters section with PWV functions,
airmass settings, atmospheric conditions, and PWV image display.
Includes validation information and parameter organization.
"""
# Help button for atmospheric parameters
help_text_atm = {
"Airmass": "Air mass of the observation. -999: retrieved from almanac (computed automatically based on target position and time). 0: calculated by SkyCalc algorithm. Positive values: constant airmass throughout the night. Typical values: 1.0 (zenith) to 2.5 (low altitude).",
"PWV Function": "Mathematical function describing precipitable water vapor (PWV) time evolution. Different functions simulate various atmospheric conditions and temporal variability patterns. PWV affects telluric absorption strength, particularly water vapor lines.",
"Monthly Solar Flux": "Monthly average solar radio flux at 10.7 cm (F10.7 index) in solar flux units (sfu). -999: retrieved from almanac for the observation date. Values typically range 50-300 sfu. Affects upper atmospheric ionization and airglow emission.",
"PWV Mode": "'pwv': Use the selected PWV function for temporal evolution. 'season': Use seasonal average PWV value appropriate for the observatory and observation date. PWV mode determines how atmospheric water vapor is modeled.",
"Season": "Season code for seasonal PWV modeling (0-3). Only relevant if 'PWV Mode' is set to 'season'. Values: 0=Winter, 1=Spring, 2=Summer, 3=Fall. Different seasons have characteristic PWV ranges.",
"Time": "Time offset in hours from midnight for PWV function evaluation. Used to set the starting point of the PWV time series. Typically 0 to start at midnight UTC.",
"Initial X": "Initial phase value for PWV function (in degrees or radians depending on function). Defines the starting point of the PWV evolution curve. Used to offset the PWV time series.",
"Final X": "Final phase value for PWV function (in degrees or radians). Defines the ending point of the PWV evolution. Together with Initial X, sets the range of PWV variation to explore."
}
HelpButton(
self.atm_panel, 0, 0,
"Atmospheric Parameters Help",
help_text_atm,
columnspan=1
)
# Section header
MyLabel(
self.atm_panel, 0, 1, color=self.atm_panel.cget("bg"),
label_text="ATMOSPHERIC PARAMETERS", font=ScaleManager.get().font_label_large if ScaleManager.get() else ("Sans", 12, "bold"), columnspan=10
)
# Airmass info label
MyLabel(
self.atm_panel, 1, 1, color=self.atm_panel.cget("bg"),
label_text="Airmass = -999 means retrieved by almanac, 0 means calculated by algorithm, other values are constant along the night",
font=ScaleManager.get().font_label_italic if ScaleManager.get() else ("Sans", 9, "italic"), columnspan=10
)
# Parameters arranged in a 2-column layout for better readability
# First row of parameters
self.entry_airmass = MyEntry(
self.atm_panel, 2, 1, "-999", label_text="Airmass:",
color=self.atm_panel.cget("bg"), columnspan=3
)
self.dropdown_pwv = MyDropdown(
self.atm_panel, 2, 4, self.math_functions.list_functions(), "PWV Function:",
color=self.atm_panel.cget("bg"), columnspan=3
)
self.dropdown_pwv.set_callback(self.update_pwv_image)
# Second row of parameters
self.entry_msolflux = MyEntry(
self.atm_panel, 3, 1, "-999", label_text="Monthly Solar Flux:",
color=self.atm_panel.cget("bg"), columnspan=3
)
self.dropdown_pwv_mode = MyDropdown(
self.atm_panel, 3, 4, ["pwv", "season"], "PWV Mode:",
color=self.atm_panel.cget("bg"), columnspan=3
)
# Third row of parameters
self.entry_season = MyEntry(
self.atm_panel, 4, 1, "0", label_text="Season:",
color=self.atm_panel.cget("bg"), columnspan=3
)
self.entry_time = MyEntry(
self.atm_panel, 4, 4, "0", label_text="Time:",
color=self.atm_panel.cget("bg"), columnspan=3
)
# Fourth row - new X-axis range parameters
self.entry_initial_x = MyEntry(
self.atm_panel, 5, 1, "-90", label_text="Initial X:",
color=self.atm_panel.cget("bg"), columnspan=3
)
self.entry_final_x = MyEntry(
self.atm_panel, 5, 4, "-75", label_text="Final X:",
color=self.atm_panel.cget("bg"), columnspan=3
)
# PWV Image display positioned below parameters for better layout
default_image_path = Path(
self.path_default, "GUIBRUSHR", "Files", "pwv_functions_plots",
self.math_functions.list_functions()[0] + ".png"
)
self.pwv_image = UpdatableImage(
self.atm_panel, 6, 1, 500, 350, str(default_image_path),
columnspan=6, rowspan=4, label=False
)
def _initialize_observational_panel(self):
"""
Initialize observational parameters panel.
Sets up moon parameters, light source controls, coordinate settings,
atmospheric emission options, and thermal emission parameters.
Provides information about parameter retrieval methods.
"""
# Help button for observational parameters
help_text_obs = {
"Include Moon": "Y: Include scattered moonlight in sky background. N: Exclude moon contribution. Scattered moonlight significantly increases sky background, especially in optical wavelengths.",
"Moon-Sun Sep [deg]": "Angular separation between Moon and Sun in degrees. -999: computed from almanac. Range 0-180°. Determines lunar phase (0°=New Moon, 180°=Full Moon). Full moon has maximum scattered light.",
"Moon-Target Sep [deg]": "Angular separation between Moon and target in degrees. -999: computed from almanac. Closer moon increases scattered light contamination in target spectra.",
"Moon Altitude [deg]": "Moon altitude above horizon in degrees. -999: computed from almanac. Higher moon increases sky brightness. Negative values mean moon is below horizon.",
"Moon-Earth Distance": "Moon-Earth distance in Earth radii or AU. -999: computed from almanac. Affects apparent lunar brightness (closer = brighter).",
"Include Starlight": "Y: Include scattered starlight background. N: Exclude. Integrated starlight contributes to diffuse sky background, particularly important in dark sky conditions.",
"Include Zodiacal": "Y: Include zodiacal light (sunlight scattered by interplanetary dust). N: Exclude. Most prominent at ecliptic, contributes to sky background particularly in infrared.",
"Ecliptic Longitude [deg]": "Ecliptic longitude of target in degrees. -999: computed from target coordinates. Used for zodiacal light calculation. Range 0-360°.",
"Ecliptic Latitude [deg]": "Ecliptic latitude of target in degrees. -999: computed from target coordinates. Zodiacal light peaks at ecliptic plane (latitude = 0°).",
"Lower Atmosphere": "Y: Include lower atmosphere emission (troposphere). N: Exclude. Lower atmospheric emission dominates in mid-infrared, particularly from H2O and CO2.",
"Upper Atmosphere": "Y: Include upper atmosphere/stratosphere emission. N: Exclude. Contributes thermal emission and scattered sunlight at high altitudes.",
"Airglow Continuum": "Y: Include airglow continuum emission. N: Exclude. Airglow from atmospheric chemiluminescence adds diffuse background, particularly in optical/NIR.",
"Thermal Emission": "Y: Include telescope/instrument thermal emission. N: Exclude (default). Thermal emission from warm surfaces dominates background in mid-infrared.",
"Therm T1/T2/T3 [K]": "Temperatures in Kelvin for thermal emission components. Multiple blackbody components model telescope optics, enclosure, and ambient surfaces at different temperatures.",
"Therm E1/E2/E3": "Emissivities (0-1) for each thermal component. Product of emissivity and solid angle subtended. Higher values = stronger thermal contribution."
}
HelpButton(
self.obs_panel, 0, 0,
"Observational Parameters Help",
help_text_obs,
columnspan=1
)
# Section header
MyLabel(
self.obs_panel, 0, 1, color=self.obs_panel.cget("bg"),
label_text="OBSERVATIONAL PARAMETERS", font=ScaleManager.get().font_label_large if ScaleManager.get() else ("Sans", 12, "bold"), columnspan=12
)
# Info label about -999 values
MyLabel(
self.obs_panel, 1, 1, color=self.obs_panel.cget("bg"),
label_text="Parameters with value -999 are retrieved by the almanac. If you want to use a constant value, set it.",
font=ScaleManager.get().font_label_italic if ScaleManager.get() else ("Sans", 9, "italic"), columnspan=12
)
# Moon parameters section (reorganized for better readability)
self.dropdown_incl_moon = MyDropdown(
self.obs_panel, 2, 1, ["Y", "N"], "Include Moon:",
color=self.obs_panel.cget("bg"), columnspan=2
)
self.entry_moon_sun_sep = MyEntry(
self.obs_panel, 2, 3, "-999", label_text="Moon-Sun Sep [deg]:",
color=self.obs_panel.cget("bg"), columnspan=3
)
self.entry_moon_target_sep = MyEntry(
self.obs_panel, 2, 6, "-999", label_text="Moon-Target Sep [deg]:",
color=self.obs_panel.cget("bg"), columnspan=3
)
# Moon parameters continued
self.entry_moon_alt = MyEntry(
self.obs_panel, 3, 1, "-999", label_text="Moon Altitude [deg]:",
color=self.obs_panel.cget("bg"), columnspan=4
)
self.entry_moon_earth_dist = MyEntry(
self.obs_panel, 3, 5, "-999", label_text="Moon-Earth Distance:",
color=self.obs_panel.cget("bg"), columnspan=4
)
# Light sources section
self.dropdown_incl_starlight = MyDropdown(
self.obs_panel, 4, 1, ["Y", "N"], "Include Starlight:",
color=self.obs_panel.cget("bg"), columnspan=2
)
self.dropdown_incl_zodiacal = MyDropdown(
self.obs_panel, 4, 3, ["Y", "N"], "Include Zodiacal:",
color=self.obs_panel.cget("bg"), columnspan=2
)
# Ecliptic coordinates
self.entry_ecl_lon = MyEntry(
self.obs_panel, 5, 1, "-999", label_text="Ecliptic Longitude [deg]:",
color=self.obs_panel.cget("bg"), columnspan=4
)
self.entry_ecl_lat = MyEntry(
self.obs_panel, 5, 5, "-999", label_text="Ecliptic Latitude [deg]:",
color=self.obs_panel.cget("bg"), columnspan=4
)
# Atmospheric emission section
self.dropdown_incl_loweratm = MyDropdown(
self.obs_panel, 6, 1, ["Y", "N"], "Lower Atmosphere:",
color=self.obs_panel.cget("bg"), columnspan=3
)
self.dropdown_incl_upperatm = MyDropdown(
self.obs_panel, 6, 4, ["Y", "N"], "Upper Atmosphere:",
color=self.obs_panel.cget("bg"), columnspan=3
)
self.dropdown_incl_airglow = MyDropdown(
self.obs_panel, 7, 1, ["Y", "N"], "Airglow Continuum:",
color=self.obs_panel.cget("bg"), columnspan=3
)
# Thermal emission section
self.dropdown_incl_therm = MyDropdown(
self.obs_panel, 8, 1, ["Y", "N"], "Thermal Emission:", initial_value=1,
color=self.obs_panel.cget("bg"), columnspan=3
)
# Thermal component 1
self.entry_therm_t1 = MyEntry(
self.obs_panel, 9, 1, "0.0", label_text="Therm T1 [K]:",
color=self.obs_panel.cget("bg"), columnspan=3
)
self.entry_therm_e1 = MyEntry(
self.obs_panel, 9, 4, "0.0", label_text="Therm E1:",
color=self.obs_panel.cget("bg"), columnspan=3
)
# Thermal component 2
self.entry_therm_t2 = MyEntry(
self.obs_panel, 10, 1, "0.0", label_text="Therm T2 [K]:",
color=self.obs_panel.cget("bg"), columnspan=3
)
self.entry_therm_e2 = MyEntry(
self.obs_panel, 10, 4, "0.0", label_text="Therm E2:",
color=self.obs_panel.cget("bg"), columnspan=3
)
# Thermal component 3
self.entry_therm_t3 = MyEntry(
self.obs_panel, 11, 1, "0.0", label_text="Therm T3 [K]:",
color=self.obs_panel.cget("bg"), columnspan=3
)
self.entry_therm_e3 = MyEntry(
self.obs_panel, 11, 4, "0.0", label_text="Therm E3:",
color=self.obs_panel.cget("bg"), columnspan=3
)
def _initialize_wavelength_panel(self):
"""
Initialize wavelength parameters panel.
Configures wavelength coverage settings, resolution parameters,
grid mode selection, and Line Spread Function (LSF) options.
"""
# Help button for wavelength parameters
help_text_wl = {
"Vacuum/Air": "'vac': Wavelengths in vacuum. 'air': Wavelengths in air (corrected for refractive index). Most high-resolution spectrographs work in vacuum, low-resolution often in air wavelengths.",
"Min Wavelength [nm]": "Minimum wavelength for telluric spectrum calculation in nanometers. Defines blue edge of spectral coverage. Example: 300 nm (UV) to 950 nm (optical/NIR boundary).",
"Max Wavelength [nm]": "Maximum wavelength for telluric calculation in nanometers. Defines red edge of spectral coverage. Example: 2500 nm covers optical + NIR up to K-band.",
"Wavelength Resolution Multiplier": "Multiplier for spectral resolution when using 'fixed_spectral_resolution' mode. Higher values = finer wavelength sampling. Typical: 2-10. Increases computation time linearly.",
"Grid Mode": "'fixed_spectral_resolution': Constant R = λ/Δλ (like echelle spectrographs). 'fixed_wavelength_step': Constant Δλ spacing (like grating spectrographs). Choose based on instrument type.",
"Wavelength Delta [nm]": "Wavelength step size in nanometers for 'fixed_wavelength_step' mode. Determines wavelength sampling. Smaller values = higher sampling but longer computation. Typical: 0.01-0.1 nm.",
"LSF Type": "Line Spread Function type. 'none': No instrumental broadening (infinite resolution). 'Gaussian': Symmetric Gaussian profile (most spectrographs). 'Boxcar': Rectangular profile (for some imaging spectrographs).",
"LSF Gauss FWHM": "Full Width at Half Maximum of Gaussian LSF in wavelength units. Defines instrumental resolution broadening. Must match instrument specifications. Only used if LSF Type = 'Gaussian'.",
"LSF Boxcar FWHM": "Full width of boxcar (rectangular) LSF in wavelength units. Simulates pixel-limited resolution. Only used if LSF Type = 'Boxcar'."
}
HelpButton(
self.wl_panel, 0, 0,
"Wavelength Parameters Help",
help_text_wl,
columnspan=1
)
# Section header
MyLabel(
self.wl_panel, 0, 1, color=self.wl_panel.cget("bg"),
label_text="WAVELENGTH PARAMETERS", font=ScaleManager.get().font_label_large if ScaleManager.get() else ("Sans", 12, "bold"), columnspan=10
)
# Grid mode info label
MyLabel(
self.wl_panel, 1, 1, color=self.wl_panel.cget("bg"),
label_text="For 'fixed_spectral_resolution' use 'Wavelength Resolution Multiplier', otherwise use 'Wavelength Delta [nm]'",
font=ScaleManager.get().font_label_italic if ScaleManager.get() else ("Sans", 9, "italic"), columnspan=10
)
# Wavelength coverage section
self.dropdown_vacair = MyDropdown(
self.wl_panel, 2, 1, ["vac", "air"], "Vacuum/Air:",
color=self.wl_panel.cget("bg"), columnspan=2
)
self.entry_wmin = MyEntry(
self.wl_panel, 2, 3, "300.0", label_text="Min Wavelength [nm]:",
color=self.wl_panel.cget("bg"), columnspan=3
)
self.entry_wmax = MyEntry(
self.wl_panel, 2, 6, "2000.0", label_text="Max Wavelength [nm]:",
color=self.wl_panel.cget("bg"), columnspan=3
)
# Grid mode section
self.dropdown_wgrid_mode = MyDropdown(
self.wl_panel, 3, 1, ["fixed_spectral_resolution", "fixed_wavelength_step"],
"Grid Mode:", color=self.wl_panel.cget("bg"), columnspan=4
)
# Resolution parameters
self.entry_wres = MyEntry(
self.wl_panel, 4, 1, "4", label_text="Wavelength Resolution Multiplier:",
color=self.wl_panel.cget("bg"), columnspan=4
)
self.entry_wdelta = MyEntry(
self.wl_panel, 4, 5, "0.1", label_text="Wavelength Delta [nm]:",
color=self.wl_panel.cget("bg"), columnspan=4
)
# Line Spread Function section
self.dropdown_lsf_type = MyDropdown(
self.wl_panel, 5, 1, ["none", "Gaussian", "Boxcar"], "LSF Type:",
color=self.wl_panel.cget("bg"), columnspan=3
)
self.entry_lsf_gauss_fwhm = MyEntry(
self.wl_panel, 6, 1, "5.0", label_text="LSF Gauss FWHM:",
color=self.wl_panel.cget("bg"), columnspan=4
)
self.entry_lsf_boxcar_fwhm = MyEntry(
self.wl_panel, 6, 5, "5.0", label_text="LSF Boxcar FWHM:",
color=self.wl_panel.cget("bg"), columnspan=4
)
def _initialize_observatory_panel(self):
"""
Initialize observatory parameters panel.
Sets up observatory selection and temperature flag configuration.
"""
# Help button for observatory parameters
help_text_observatory = {
"Observatory": "Predefined observatory site for SkyCalc atmospheric modeling. Options: 'lasilla': La Silla Observatory (2400m, Chile), 'paranal': Paranal/VLT (2635m, Chile), '3060m': Generic 3060m altitude site. Different sites have different atmospheric profiles (PWV, pressure, temperature).",
"Temperature Flag": "Temperature flag for atmospheric modeling. 1: Use site-specific temperature profile from almanac/climatology. 0: Use fixed temperature value. Temperature affects atmospheric density profile and refraction."
}
HelpButton(
self.observatory_panel, 0, 0,
"Observatory Parameters Help",
help_text_observatory,
columnspan=1
)
# Section header
MyLabel(
self.observatory_panel, 0, 1, color=self.observatory_panel.cget("bg"),
label_text="OBSERVATORY PARAMETERS", font=ScaleManager.get().font_label_large if ScaleManager.get() else ("Sans", 12, "bold"), columnspan=10
)
# Observatory selection
self.dropdown_observatory = MyDropdown(
self.observatory_panel, 1, 1, ["lasilla", "paranal", "3060m"], "Observatory:",
color=self.observatory_panel.cget("bg"), columnspan=4
)
# Temperature flag
self.entry_temp_flag = MyEntry(
self.observatory_panel, 2, 1, "1", label_text="Temperature Flag:",
color=self.observatory_panel.cget("bg"), columnspan=4
)
def _initialize_coordinates_panel(self):
"""
Initialize coordinates parameters panel with vertical layout.
Creates entry fields for equatorial coordinates (RA, Dec) and
observatory coordinates (latitude, longitude, altitude).
"""
# Help button for coordinates parameters
help_text_coords = {
"RA [deg]": "Right Ascension of target in decimal degrees (J2000 epoch). Range 0-360°. Used for airmass calculation, barycentric corrections, and almanac computations. Automatically populated from target database but can be overridden.",
"Dec [deg]": "Declination of target in decimal degrees (J2000 epoch). Range -90° to +90°. Together with RA defines target position on celestial sphere. Critical for accurate airmass and barycentric velocity calculations.",
"Select Observatory": "Choose observatory from database. Pre-configured observatories have accurate geodetic coordinates. Selecting an observatory automatically populates latitude, longitude, and altitude fields. Required for barycentric corrections and almanac calculations.",
"Observatory Lat [deg]": "Observatory geodetic latitude in decimal degrees. North positive (+), South negative (-). Range -90° to +90°. Critical for Earth rotation velocity corrections and accurate airmass calculations.",
"Observatory Long [deg]": "Observatory geodetic longitude in decimal degrees. East positive (+), West negative (-). Range -180° to +180°. Used for calculating Local Sidereal Time and Earth rotation effects on radial velocities.",
"Observatory Alt [m]": "Observatory altitude above sea level in meters. Affects atmospheric pressure/density profiles, extinction, and atmospheric refraction. Higher altitude = less atmosphere = less extinction and better transparency."
}
HelpButton(
self.coordinates_panel, 0, 0,
"Coordinates Parameters Help",
help_text_coords,
columnspan=1
)
# Section header
MyLabel(
self.coordinates_panel, 0, 1, color=self.coordinates_panel.cget("bg"),
label_text="COORDINATES PARAMETERS", font=ScaleManager.get().font_label_large if ScaleManager.get() else ("Sans", 12, "bold"), columnspan=6
)
self.target_information = get_target_info(self.path_folder_targets, self.dropdown_target.get_value(), self.dropdown_rad_mode.get_value())
# Arrange coordinate parameters vertically (one below the other)
# Equatorial coordinates
self.entry_ra = MyEntry(
self.coordinates_panel, 1, 1, str(self.target_information.ra), label_text="RA [deg]:",
color=self.coordinates_panel.cget("bg"), columnspan=3
)
self.entry_dec = MyEntry(
self.coordinates_panel, 2, 1, str(self.target_information.dec), label_text="Dec [deg]:",
color=self.coordinates_panel.cget("bg"), columnspan=3
)
# Observatory selection dropdown
# Get observatories from database
DB = DBSQLite3(self.path_default)
observatories, lat, long, alt = DB.get_observatories()
DB.close_DB()
self.dropdown_observatory_coords = MyDropdown(
self.coordinates_panel, 3, 1, observatories, "Select Observatory:",
color=self.coordinates_panel.cget("bg"), columnspan=3
)
self.dropdown_observatory_coords.set_callback(self.update_observatory_coordinates)
lat = str(lat[0]) if lat is not None else "0"
long = str(long[0]) if long is not None else "0"
alt = str(alt[0]) if alt is not None else "0"
# Observatory coordinates
self.entry_obs_lat = MyEntry(
self.coordinates_panel, 4, 1, lat, label_text="Observatory Lat [deg]:",
color=self.coordinates_panel.cget("bg"), columnspan=3
)
self.entry_obs_long = MyEntry(
self.coordinates_panel, 5, 1, long, label_text="Observatory Long [deg]:",
color=self.coordinates_panel.cget("bg"), columnspan=3
)
self.entry_obs_alt = MyEntry(
self.coordinates_panel, 6, 1, alt, label_text="Observatory Alt [m]:",
color=self.coordinates_panel.cget("bg"), columnspan=3
)
[docs]
def update_observatory_coordinates(self):
"""
Update observatory coordinates when dropdown selection changes.
Retrieves the selected observatory's coordinates from the database
and updates the latitude, longitude, and altitude entry fields.
"""
selected_observatory = self.dropdown_observatory_coords.get_value()
if selected_observatory and selected_observatory != "None":
DB = DBSQLite3(self.path_default)
observatory_data = DB.get_observatory_by_key(selected_observatory)
DB.close_DB()
if observatory_data:
# Update the entry fields with observatory data
self.entry_obs_lat.set_value(str(observatory_data[1])) # latitude
self.entry_obs_long.set_value(str(observatory_data[2])) # longitude
self.entry_obs_alt.set_value(str(observatory_data[3])) # altitude
print(f"Updated coordinates for observatory: {selected_observatory}")
else:
print(f"No data found for observatory: {selected_observatory}")
else:
print("No observatory selected or 'None' selected")
[docs]
def update_observatory_list(self, observatories):
"""
Update the observatory dropdown with a new list of observatories.
Recreates the observatory dropdown widget with new observatory options
and maintains the callback function.
Parameters
----------
observatories : list
List of observatory names to populate the dropdown
"""
try:
# Clean the existing dropdown widget
self.dropdown_observatory_coords.clean_widget()
# Recreate the dropdown with new observatory list
self.dropdown_observatory_coords = MyDropdown(
self.coordinates_panel, 3, 1, observatories, "Select Observatory:",
color=self.coordinates_panel.cget("bg"), columnspan=3
)
# Restore the callback function
self.dropdown_observatory_coords.set_callback(self.update_observatory_coordinates)
print(f"Updated observatory dropdown with {len(observatories)} observatories")
except Exception as e:
print(f"Error updating observatory dropdown: {e}")
[docs]
def get_atmospheric_parameters(self):
"""
Get all atmospheric parameters as a dictionary.
Collects all atmospheric, observational, wavelength, and observatory
parameters from the GUI widgets. Parameters with value -999 are
excluded from the final dictionary as they indicate values to be
retrieved automatically.
Returns
-------
dict
Dictionary containing all atmospheric parameter values with
-999 values excluded
"""
print("Collecting atmospheric parameters...")
# Collect all parameters first
all_params = {
# Atmospheric parameters
"airmass": float(self.entry_airmass.get_value()),
"pwv_mode": self.dropdown_pwv_mode.get_value(),
"season": int(self.entry_season.get_value()),
"time": int(self.entry_time.get_value()),
"pwv": self.dropdown_pwv.get_value(),
"pwv_func": self.dropdown_pwv.get_value(),
"msolflux": float(self.entry_msolflux.get_value()),
"initial_x": float(self.entry_initial_x.get_value()),
"final_x": float(self.entry_final_x.get_value()),
# Observational parameters
"incl_moon": self.dropdown_incl_moon.get_value(),
"moon_sun_sep": float(self.entry_moon_sun_sep.get_value()),
"moon_target_sep": float(self.entry_moon_target_sep.get_value()),
"moon_alt": float(self.entry_moon_alt.get_value()),
"moon_earth_dist": float(self.entry_moon_earth_dist.get_value()),
"incl_starlight": self.dropdown_incl_starlight.get_value(),
"incl_zodiacal": self.dropdown_incl_zodiacal.get_value(),
"ecl_lon": float(self.entry_ecl_lon.get_value()),
"ecl_lat": float(self.entry_ecl_lat.get_value()),
"incl_loweratm": self.dropdown_incl_loweratm.get_value(),
"incl_upperatm": self.dropdown_incl_upperatm.get_value(),
"incl_airglow": self.dropdown_incl_airglow.get_value(),
"incl_therm": self.dropdown_incl_therm.get_value(),
"therm_t1": float(self.entry_therm_t1.get_value()),
"therm_e1": float(self.entry_therm_e1.get_value()),
"therm_t2": float(self.entry_therm_t2.get_value()),
"therm_e2": float(self.entry_therm_e2.get_value()),
"therm_t3": float(self.entry_therm_t3.get_value()),
"therm_e3": float(self.entry_therm_e3.get_value()),
# Wavelength parameters
"vacair": self.dropdown_vacair.get_value(),
"wmin": float(self.entry_wmin.get_value()),
"wmax": float(self.entry_wmax.get_value()),
"wgrid_mode": self.dropdown_wgrid_mode.get_value(),
"wdelta": float(self.entry_wdelta.get_value()),
"wres_mul": float(self.entry_wres.get_value()),
"lsf_type": self.dropdown_lsf_type.get_value(),
"lsf_gauss_fwhm": float(self.entry_lsf_gauss_fwhm.get_value()),
"lsf_boxcar_fwhm": float(self.entry_lsf_boxcar_fwhm.get_value()),
# Observatory parameters
"observatory": self.dropdown_observatory.get_value(),
"temp_flag": int(self.entry_temp_flag.get_value())
}
# Filter out parameters with value -999
filtered_params = {}
excluded_count = 0
for key, value in all_params.items():
if isinstance(value, (int, float)) and value == -999:
excluded_count += 1
print(f" Excluding parameter '{key}' (value = -999)")
else:
filtered_params[key] = value
print(f" Total parameters: {len(all_params)}")
print(f" Excluded parameters: {excluded_count}")
print(f" Final parameters: {len(filtered_params)}")
return filtered_params
[docs]
def get_almanac_parameters(self):
"""
Get coordinate parameters as an almanac dictionary.
Collects equatorial coordinates and observatory position parameters
needed for astronomical calculations and almanac computations.
Returns
-------
dict
Dictionary containing coordinate parameters including RA, Dec,
observatory latitude, longitude, and altitude
"""
print("Collecting almanac parameters...")
return {
# Equatorial coordinates
"ra": float(self.entry_ra.get_value()),
"dec": float(self.entry_dec.get_value()),
# Observatory coordinates
"obs_lat": float(self.entry_obs_lat.get_value()),
"obs_long": float(self.entry_obs_long.get_value()),
"obs_alt": float(self.entry_obs_alt.get_value())
}
[docs]
def get_pwv_parameters(self):
"""
Get PWV parameters from the first tab entry widgets.
Collects PWV (precipitable water vapor) modeling parameters used
for atmospheric simulation and telluric absorption calculations.
Returns
-------
dict
Dictionary containing PWV parameters including dt, pace, s,
scale, tExp, and seed values
"""
print("Collecting PWV parameters...")
return {
"dt": float(self.entry_dt.get_value()),
"pace": float(self.entry_pace.get_value()),
"s": float(self.entry_s.get_value()),
"scale": float(self.entry_scale.get_value()),
"tExp": float(self.entry_tExp.get_value()),
"seed": int(self.entry_seed.get_value())
}
[docs]
def get_row(self):
"""
Get the currently selected row information from the filter table.
Retrieves target information and array values from the currently
selected row in the filter table interface.
Returns
-------
tuple
(target, array_values, selected_name) or (None, None, None)
if no row is selected or an error occurs
"""
try:
target = self.dropdown_target.get_value()
arr_values = self.frame_input.frame_filter_table.frame_table.table.get_arr_values()
selected_name = arr_values[ConstantVariables.DICT_FILTER_TABLE["ID"]["index"]]
except Exception as e:
messagebox.showerror("Choose a row", str(e))
return None, None, None
return target, arr_values, selected_name
[docs]
def update_from_target_or_rad_mode(self):
"""
Update night information for the current target and instrument configuration.
Called when the target selection or radiation transfer mode changes.
Updates the available nights, instruments, and cross-correlation models
to match the new selection.
"""
if self.global_exc == 0:
return
self.update_nights_instrument()
self.update_CC_model()
self.update_nights()
[docs]
def update_nights(self):
"""
Update HR instrument settings and available nights.
Called when the HR instrument selection changes. Updates the nights
text field with available observation nights for the selected
target and instrument combination.
"""
if self.global_exc == 0:
return
instrumenti = self.dropdown_hr_instrument.get_value()
target_id = self.dropdown_target.get_value()
nights = get_target_nights(
self.path_folder_targets,
target_id,
self.dropdown_rad_mode.get_value(),
instrumenti,
0
)
self.target_information = get_target_info(self.path_folder_targets, target_id, self.dropdown_rad_mode.get_value())
self.entry_ra.set_value(str(self.target_information.ra))
self.entry_dec.set_value(str(self.target_information.dec))
self.textfield_nights.insert_text(nights)
[docs]
def update_nights_instrument(self):
"""
Update the HR instrument dropdown based on current target and mode.
Refreshes the list of available HR instruments for the selected
target and radiation transfer mode. Recreates the dropdown widget
with updated options.
"""
if self.global_exc == 0:
return
target_id = self.dropdown_target.get_value()
# Update HR instruments
self.list_hr_instrument = get_target_instruments(
self.path_folder_targets,
target_id,
"HR",
self.dropdown_rad_mode.get_value(),
Path(__file__).name
)
self.dropdown_hr_instrument.clean_widget()
self.dropdown_hr_instrument = MyDropdown(
self.main_controls_panel, 0, 9, self.list_hr_instrument, "HR Instrument:",
color=self.main_controls_panel.cget("bg"), columnspan=2
)
self.dropdown_hr_instrument.set_callback(self.update_nights)
[docs]
def update_CC_model(self):
"""
Update the cross-correlation model dropdown.
Refreshes the list of available cross-correlation models based on
the current target and radiation transfer mode selection.
"""
target_id = self.dropdown_target.get_value()
# Update cross-correlation models
self.dropdown_cross_corr.clean_widget()
dict_list_cc = get_list_cross_corr_models(
self.path_folder_targets,
target_id,
self.dropdown_rad_mode.get_value()
)
self.dropdown_cross_corr = MyDropdown(
self.main_controls_panel, 0, 11, list(sorted(dict_list_cc.keys())),
"Model CC", initial_value=0,
color=self.main_controls_panel.cget("bg"), columnspan=2
)
[docs]
def run_selected_generation(self):
"""
Run the selected generation based on dropdown choice.
Checks the generation mode dropdown and calls the appropriate
generation function. Supports synthetic generations with continuous
or discontinuous PWV, and shuffle generations.
"""
selected_mode = self.dropdown_generation_mode.get_value()
print(f"Selected generation mode: {selected_mode}")
if selected_mode == "Synthetic":
print("Running Synthetic generation with PWV parameters...")
self.run_synthetic(False)
elif selected_mode == "Synthetic Discontinuous PWV":
print("Running Synthetic Discontinuous PWV generation...")
self.run_synthetic(True)
elif selected_mode == "Shuffle":
print("Running shuffle generation...")
self.run_shuffle()
else:
print(f"Unknown generation mode: {selected_mode}")
[docs]
def modify_target_list(self, target_list):
"""
Update the target dropdown with a new list of targets.
Recreates the target dropdown widget with new target options
and updates the callback function.
Parameters
----------
target_list : list
List of target names to populate the dropdown
"""
self.dropdown_target.clean_widget()
self.dropdown_target = MyDropdown(
self.main_controls_panel, 0, 1, target_list, "Target:",
color=self.main_controls_panel.cget("bg"), columnspan=2
)
self.dropdown_target.set_callback(self.update_from_target_or_rad_mode)
[docs]
def run_shuffle(self):
"""
Run the HR data shuffle with the current configuration.
Collects all current settings and parameters from the GUI and
executes the HR data shuffle using the shuffle_HR function.
This method runs synchronously and blocks until completion.
"""
print("=== Starting HR Data shuffle ===")
self.terminator_night = "sim"
target_obj = Target(
str(Path(self.path_folder_targets, self.dropdown_target.get_value(), "info.yaml")),
self.dropdown_rad_mode.get_value()
)
print(f"Target: {target_obj.name}")
print(f"Radiation Transfer Mode: {self.dropdown_rad_mode.get_value()}")
with DBSQLite3(ConstantVariables.path_default) as DB:
instrument_obj = DB.get_instrument_by_key(self.dropdown_hr_instrument.get_value())
print(f"HR Instrument: {self.dropdown_hr_instrument.get_value()}")
print(f"Nights: {self.textfield_nights.get_text()}")
print(f"Cross-correlation Model: {self.dropdown_cross_corr.get_value()}")
print(f"Order Selection: {self.textfield_order_selection.get_text()}")
print(f"Telluric Removal Method: {self.dropdown_tell_rm_method.get_value()}")
print("Running shuffle...")
shuffle_HR(
target_obj,
self.path_folder_targets,
self.dropdown_rad_mode.get_value(),
instrument_obj,
self.textfield_nights.get_text(),
self.dropdown_cross_corr.get_value(),
self.textfield_order_selection.get_text(),
self.dropdown_tell_rm_method.get_value(),
)
print("=== HR Data shuffle Completed ===\n")
messagebox.showinfo(
"Shuffle HR Generation Complete",
"The shuffle HR generation has completed successfully!"
)
[docs]
def run_synthetic(self, discontinuos=True):
"""
Run the synthetic HR data generation in background with the current configuration.
Handles stellar spectrum download if needed and runs the synthetic
generation in a background subprocess. Manages parameter collection,
stellar spectrum acquisition, and background process monitoring.
Parameters
----------
discontinuos : bool, optional
Whether to use discontinuous PWV modeling, by default True
"""
self.terminator_night = "syn"
print("=== Starting Background Synthetic HR Data Generation ===")
target_obj = Target(
str(Path(self.path_folder_targets, self.dropdown_target.get_value(), "info.yaml")),
self.dropdown_rad_mode.get_value()
)
print(f"Target: {target_obj.name}")
print(f"Radiation Transfer Mode: {self.dropdown_rad_mode.get_value()}")
with DBSQLite3(ConstantVariables.path_default) as DB:
instrument_obj = DB.get_instrument_by_key(self.dropdown_hr_instrument.get_value())
print(f"HR Instrument: {self.dropdown_hr_instrument.get_value()}")
folder_stellar_spc = str(Path(self.path_folder_targets, target_obj.name, "Star_Spectrum"))
path_phoenixf = download_star_spectrum(self, target_obj, folder_stellar_spc)
if path_phoenixf is None:
return
# Get atmospheric parameters
print("Collecting atmospheric and almanac parameters...")
atmospheric_params = self.get_atmospheric_parameters()
almanac_params = self.get_almanac_parameters()
pwv_params = self.get_pwv_parameters()
print(f"Atmospheric parameters collected: {len(atmospheric_params)} parameters")
print(f"Almanac parameters collected: {len(almanac_params)} parameters")
if discontinuos:
print(f"PWV parameters not collected")
else:
print(f"PWV parameters collected: {len(pwv_params)} parameters")
# Save parameters to pickle file
print("Saving parameters to pickle file...")
pickle_path = self._save_synthetic_hr_data(
target_obj, instrument_obj, path_phoenixf, atmospheric_params, almanac_params, pwv_params, discontinuos
)
# Disable buttons and start background process
print("Starting background subprocess...")
self._disable_buttons()
self._start_synthetic_subprocess(pickle_path)
print("=== Background Synthetic HR Generation Started ===")
print("The generation is running in the background. You will be notified when it completes.")
[docs]
def update_pwv_image(self):
"""
Update the PWV image when the dropdown selection changes.
Loads and displays the appropriate PWV function plot based on
the selected function. Falls back to default logo if the
specific function image is not found.
"""
try:
selected_function = self.dropdown_pwv.get_value()
print(f"Updating PWV image for function: {selected_function}")
image_path = Path(self.path_default, "GUIBRUSHR", "Files", "pwv_functions_plots", f"{selected_function}.png")
if image_path.exists():
self.pwv_image.update_image(str(image_path))
print(f"PWV image updated successfully: {selected_function}")
else:
# If image doesn't exist, use default logo
default_image_path = Path(self.path_default, "GUIBRUSHR", "Files", "logo.png")
if default_image_path.exists():
self.pwv_image.update_image(str(default_image_path))
print(f"Using default logo (function image not found): {selected_function}")
else:
print(f"Neither function image nor default logo found. Function: {selected_function}")
print(f"PWV function image not found: {image_path}")
except Exception as e:
print(f"Error updating PWV image: {e}")
def _disable_buttons(self):
"""
Disable buttons during background execution.
Prevents user interaction with generation controls while a
background process is running.
"""
try:
self.button_run.set_status('disabled')
except Exception as _:
pass # Handle cases where buttons might not exist yet
def _enable_buttons(self):
"""
Enable buttons after background execution completes.
Re-enables user interaction with generation controls after
background process completion.
"""
try:
self.button_run.set_status('normal')
except Exception as _:
pass # Handle cases where buttons might not exist yet
def _save_synthetic_hr_data(self, target_obj, instrument_obj, path_phoenixf, atmospheric_params, almanac_params, pwv_params, discontinuos):
"""
Save all parameters needed for synthetic HR generation to pickle file.
Creates a temporary directory and saves all generation parameters
to a pickle file for use by the background subprocess.
Parameters
----------
target_obj : Target
Target object containing target information
instrument_obj : Instrument
Instrument object containing instrument configuration
path_phoenixf : str
Path to stellar spectrum file
atmospheric_params : dict
Atmospheric parameters dictionary
almanac_params : dict
Almanac parameters dictionary
pwv_params : dict
PWV parameters dictionary
discontinuos : bool
Whether to use discontinuous PWV mode
Returns
-------
str
Path to the created pickle file containing all parameters
Notes
-----
All parameters are organized into a single dictionary structure for
improved organization and maintainability. The dictionary contains
core generation parameters, GUI settings, and processing options.
"""
# Create a temp directory for the background process
temp_dir = Path(self.path_folder_targets, target_obj.name, "temp_synthetic")
temp_dir.mkdir(exist_ok=True)
pickle_path = temp_dir / "synthetic_hr_params.pkl"
# Organize all parameters into a single dictionary
generation_params = {
# Core framework parameters
'path_default': self.path_default,
'target_obj': target_obj,
'path_folder_targets': self.path_folder_targets,
'rad_mode': self.dropdown_rad_mode.get_value(),
'instrument_obj': instrument_obj,
# Observation parameters
'nights_text': self.textfield_nights.get_text(),
'cross_corr_model': self.dropdown_cross_corr.get_value(),
'order_selection': self.textfield_order_selection.get_text(),
'tell_rm_method': self.dropdown_tell_rm_method.get_value(),
'path_stellar_spectrum': path_phoenixf,
# Physical model parameters
'atmospheric_params': atmospheric_params,
'almanac_params': almanac_params,
'pwv_params': pwv_params,
# Processing options
'discontinuous': discontinuos,
'save_plots': self.checkbox_save_plots.get_value(),
'noise_factor': float(self.entry_noise_factor.get_value()),
'gain': float(self.entry_gain.get_value()),
'include_0_1': self.checkbox_include_0_1.get_value(),
'error_type': self.dropdown_error_type.get_value(),
# Fixed parameters
'eccentricity': False # Always False for now
}
# Save the complete parameter dictionary
with open(str(pickle_path), "wb") as fo:
pickle.dump(generation_params, fo)
return str(pickle_path)
def _start_synthetic_subprocess(self, pickle_path):
"""
Start the synthetic HR generation subprocess.
Launches the synthetic_HR.py script as a subprocess with the
pickle file containing all parameters. Sets up process monitoring.
Parameters
----------
pickle_path : str
Path to the pickle file containing all generation parameters
"""
script_path = str(Path(
self.path_default, "GUIBRUSHR", "GUI", "Input_Output_Panels", "Input_Panels",
"TabPanels", "FrameGenerationHRData", "synthetic_HR.py"
))
self.synthetic_process = subprocess.Popen(
["python3", script_path, pickle_path, self.path_default],
stderr=subprocess.STDOUT,
universal_newlines=True,
)
self.synthetic_running = True
self.window.after(500, self.check_synthetic_proc)
[docs]
def check_synthetic_proc(self):
"""
Check if the synthetic HR subprocess has completed.
Monitors the subprocess status and handles completion or failure.
Re-enables buttons and shows appropriate messages when the process
finishes. Schedules itself to run again if the process is still active.
"""
if self.synthetic_process.poll() is not None:
# Subprocess completed
self.synthetic_running = False
self._enable_buttons()
# Show completion message
returncode = self.synthetic_process.returncode
path_instrument = create_path_night(
self.path_folder_targets, self.dropdown_target.get_value(), self.dropdown_rad_mode.get_value(), self.dropdown_hr_instrument.get_value()
)
nights = self.textfield_nights.get_text()
night = nights.split(",")
path_night = str(Path(path_instrument, night[-1]))
path_night = path_night + self.terminator_night
path_pdf = str(Path(path_night, "aligned_matrix.pdf"))
if returncode == 0:
messagebox.showinfo(
"Synthetic HR Generation Complete",
"The synthetic HR generation has completed successfully!"
)
MyPDFWindow(path_pdf=path_pdf, title="Synthetic HR Data")
else:
messagebox.showerror(
"Synthetic HR Generation Error",
f"The synthetic HR generation failed with return code {returncode}. Check console for details."
)
else:
# Subprocess still running - check again later
self.window.after(500, self.check_synthetic_proc)