"""
FrameManualModel module provides a GUI panel for manual model creation and visualization.
This module allows users to:
- Configure atmospheric parameters
- Set up molecular compositions
- Generate and visualize spectral models
- Save model results and plots
"""
# Standard library imports
import os
import pickle
import subprocess
import traceback
from pathlib import Path
# Third-party imports
import matplotlib
matplotlib.rcParams['agg.path.chunksize'] = 10000
if not os.environ.get('SPHINX_BUILD'):
matplotlib.use("TkAgg")
import matplotlib.pyplot as plt
import numpy as np
import tkinter as tk
from tkinter import messagebox
from astropy.io import fits
import yaml
# Local imports
from GUIBRUSHR.GUI.DataInterface.DBSQLite3 import DBSQLite3
from GUIBRUSHR.GUI.Input_Output_Panels.Input_Panels.TabPanels.FrameManualModel.ManualModelObjClass import ManualModelObjClass
from GUIBRUSHR.GUI.Input_Output_Panels.Input_Panels.TabPanels.FrameManualModel.ParamClassForManualPlot import ParamClassForManualPlot
from GUIBRUSHR.Retrieval.ModelCalculation.ModelData import ModelData
from GUIBRUSHR.GUI.WIDGET.MyButton import MyButton
from GUIBRUSHR.GUI.WIDGET.MyDropDown import MyDropdown
from GUIBRUSHR.GUI.WIDGET.MyTextField import MyTextField
from GUIBRUSHR.GUI.WIDGET.MyCheckBox import MyCheckBox
from GUIBRUSHR.GUI.WIDGET.MyEntry import MyEntry
from GUIBRUSHR.GUI.LAYOUT.MyPanel import MyPanel
from GUIBRUSHR.GUI.LAYOUT.MyTabPanel import MyTabPanel
from GUIBRUSHR.GUI.WIDGET.MyLabel import MyLabel
from GUIBRUSHR.GUI.WIDGET.MyFigure import MyFigure
from GUIBRUSHR.GUI.DataInterface.DataInterface import get_target_info, create_path_night, get_target_list
from GUIBRUSHR.General_Constants.FunctionsAndConstants.Constant_Variables import ConstantVariables
from GUIBRUSHR.General_Constants.FunctionsAndConstants.general_functions import get_line_lists, get_condensed_line_list, plot_tp_profile, plot_vmr, download_star_spectrum, get_depth_filename
from GUIBRUSHR.General_Constants.Classes.ValueErrorTP import ValueErrorTP
from GUIBRUSHR.General_Constants.Classes.HelpButton import HelpButton
# noinspection PyTypeChecker,PyUnresolvedReferences
def _create_tab_help_button_manual(panel_idx, parent_panel, has_vmr_button=False, has_hce_button=False,
has_molecule_button=False, has_condensed_button=False, has_hydrogen_button=False):
"""
Create tab-specific help button with parameter descriptions.
Args:
panel_idx: Index of the panel (0-7)
parent_panel: Parent panel widget
has_vmr_button: Whether this panel has VMR button
has_hce_button: Whether this panel has Hybrid Chemical Equilibrium button
has_molecule_button: Whether this panel has ADD MOLECULE button
has_condensed_button: Whether this panel has ADD CONDENSED MOLECULE button
has_hydrogen_button: Whether this panel has UPDATE H-, H, e- button
Returns:
HelpButton instance
"""
# Tab descriptions
tab_descriptions = [
"Planet + Data: Parameters related to planetary properties, orbital characteristics, data scaling, and instrumental offsets.",
"Clouds/Haze: Parameters controlling cloud deck properties, haze opacity, and aerosol scattering in the atmosphere.",
"Temperatures: Temperature-pressure profile parameters for primary parameterizations (isothermal, Madhu, Guillot).",
"Temperatures2: Additional temperature profile parameters for alternative parameterizations (personalized profiles, nodal points).",
"VMR: Volume Mixing Ratio profile parameters for species with altitude-dependent abundances (Gaussian peaks).",
"Equilibrium: Chemical equilibrium parameters including metallicity, C/O ratio, and elemental abundances.",
"Molecules: Molecular species and their volume mixing ratios. Includes major atmospheric constituents and trace species.",
"Condensed elements: Condensed species (cloud particles) parameters including particle properties and sedimentation efficiency."
]
# Collect parameters for this panel
help_text = {"Tab Description": tab_descriptions[panel_idx]}
for param_name, param_data in ConstantVariables.PARAMS_DICT.items():
if param_data["panel"] == panel_idx and param_data["row_manual"] >= 0:
# Get description from params dict, with fallback
description = param_data.get("description", f"Parameter {param_name} (description pending)")
label = param_data.get("label", param_name)
# Format the help entry
help_text[f"{param_name} ({label})"] = description
# Add button-specific help if applicable
if has_vmr_button:
help_text["UPDATE VMR PARAMETERS button"] = "Automatically populate VMR peak, pressure, and width parameters for all molecules with present=1 and VMR=0. Creates altitude-dependent Gaussian abundance profiles."
if has_hce_button:
help_text["ADD ELEMENT TO HYBRID CHEMICAL EQUILIBRIUM button"] = "Adds the selected element for hybrid chemical equilibrium modeling. Combines chemical equilibrium (for major species) with free retrieval (for selected elements)."
if has_hydrogen_button:
help_text["UPDATE H-, H, e- button"] = "Updates the abundances of H- (hydrogen minus), H (atomic hydrogen), and e- (free electrons) based on the H2 and He abundances. Calculates derived abundances for consistency in hot atmosphere models."
if has_molecule_button:
help_text["ADD MOLECULE button"] = "Adds the selected molecule with its isotope, sub-isotope, and abundance to the atmospheric composition. Allows manual specification of molecular abundances."
if has_condensed_button:
help_text["ADD CONDENSED MOLECULE button"] = "Adds cloud/aerosol species (e.g., Al2O3, MgSiO3, Fe) to the model. Controls cloud particle composition and properties."
return HelpButton(
parent_panel, 0, 100,
f"Tab Parameters Help",
help_text,
columnspan=2
)
# noinspection PyUnresolvedReferences
[docs]
class FrameManualModel(MyPanel):
"""
A GUI panel for manual model creation and visualization.
This class provides a comprehensive interface for:
- Configuring atmospheric parameters and molecular compositions
- Generating spectral models with different radiative transfer codes
- Visualizing and comparing model results
- Saving model outputs and plots
The panel includes controls for:
- Target selection and basic parameters
- Temperature-pressure profile configuration
- Molecular composition setup
- Model calculation and visualization
- Results saving and export
"""
[docs]
def __init__(self, parent, row, column, color, path_default, frame_input, width, height, window, **kwargs):
"""
Initialize the manual model frame.
Args:
parent: Parent widget
row: Row position in parent
column: Column position in parent
color: Background color
path_default: Default path for file operations
frame_input: Input frame containing target information
width: Frame width
height: Frame height
window: Main window
**kwargs: Additional keyword arguments
"""
# Initialize instance variables
self.dict_rayleigh_base = {
"H2":None, "He":None, "H":None, "e-":None, "H-":None
}
self.past_string = None
self.last_rad_mode = None
self.list_condensed_last_plot = None
self.dropdown_target = None
self.current_plot_for_saving = None
self.current_plot = None
self.target_information = None
self.last_max_wl = None
self.last_min_wl = None
self.last_min_pressure = None
self.last_max_pressure = None
self.last_pressure_layers = None
self.last_continuum_opacity = None
self.model_obj = None
self.path_folder_targets = frame_input.frame_path.path_folder_targets.get_text()
self.dict_params_plot = None
self.color = color
self.wl = None
self.model = None
self.auto_image = None
self.auto_string = None
self.stop_blink_var = None
self.current_string = None
self.p = None
self.counter_blinks = None
self.current_image = None
self.global_exc = 0
self.first_enter = 1
self.frame_input = frame_input
self.path_default = path_default
self.window = window
# Initialize parent class
super().__init__(parent, color, row, column, **kwargs)
# Setup paths and directories
self._setup_paths()
# Initialize UI components
self._init_ui_components(width, height)
# Setup event handlers
self._setup_event_handlers()
# Initialize state variables
self._init_state_variables()
def _setup_paths(self):
"""Setup necessary paths and directories."""
username = os.environ.get("USER")
self.folder_plot = str(Path(self.path_default, "GUIBRUSHR", "Files", "Temp_Files_GUI", username, "Folder_plot_temp"))
self.path_results = str(Path(self.folder_plot, ConstantVariables.RESULTS_FOLDER))
os.system("mkdir -p " + self.path_results)
self.current_plot_for_saving = None
# Connect to database
target_list = get_target_list(self.path_folder_targets)
self.target_information = get_target_info(self.path_folder_targets, target_list[0])
DB = DBSQLite3(self.path_default)
DB.close_DB()
def _init_ui_components(self, width, height):
"""Initialize UI components and layout."""
# Create main frames
self.frame_manual_model = tk.Frame(
self.window, highlightbackground="black", highlightthickness=1, padx=5, pady=2
)
self.frame_info_params = tk.Frame(
self.frame_manual_model,
highlightbackground="black",
highlightthickness=1,
padx=5,
pady=2,
)
# Create panel rows
self._create_panel_rows()
# Create parameter controls
self._create_parameter_controls()
# Create plot area
self._create_plot_area()
def _create_panel_rows(self):
"""Create panel rows for organizing UI elements."""
self.row1 = MyPanel(self, self.color, 0, 1)
self.row2 = MyPanel(self, self.color, 1, 1)
self.row_instruments = MyPanel(self.row2, self.color, 0, 2, rowspan=2)
self.row3 = MyPanel(self, self.color, 2, 1)
self.row4 = MyPanel(self, self.color, 3, 1)
self.row5 = MyPanel(self, self.color, 4, 1)
# Create row for plot area (replaces row_params[-1])
self.row_plot = MyPanel(self, self.color, 7, 1)
# Create rowinvisible for hidden parameters
self.rowinvisible = MyPanel(self, self.color, 99, 1, visible=False)
def _create_parameter_controls(self):
"""Create parameter control widgets organized by panel (from parameters.yaml)."""
# Target selection
self.dropdown_target = MyDropdown(
self.row1, 0, 1, get_target_list(self.path_folder_targets),
"Target:", color=self.color, columnspan=2
)
# Stellar spectrum
self.dropdown_stellar_spectrum = MyDropdown(
self.row1, 0, 3, ["Planck", "Phoenix"],
"Stellar Spectrum:", initial_value=0,
color=self.color, columnspan=2
)
# Resolution selection
self.dropdown_resolution = MyDropdown(
self.row1, 0, 5, ["High"], "Resolution:",
color=self.color, columnspan=2, initial_value=0
)
# Radiation mode
self.dropdown_rad_mode = MyDropdown(
self.row1, 0, 7, ConstantVariables.LIST_RAD_MODE,
"Rad. Transf. Mode:", initial_value=0,
color=self.color, columnspan=2
)
# Chemistry
self.dropdown_chemistry = MyDropdown(
self.row3, 0, 3, ConstantVariables.LIST_CHEMISTRY_TABLE, "Chemistry:", initial_value=1,
color=self.color, columnspan=2
)
# T-P profile
self.dropdown_t_p_profile = MyDropdown(
self.row3, 0, 5, ConstantVariables.LIST_PT_PROFILE_TABLE, "T-P profile:", color=self.color,
columnspan=2
)
# Scattering
self.check_scattering = MyCheckBox(
self.row3, 0, 7, "Scattering power law"
)
# continuum opacity
self.textfield_continuum_opacity = MyTextField(
self.row3, 0, 8, "H2-H2,H2-He", "Continuum:", color=self.color, columnspan=2, width=20
)
# include h-
self.checkbox_include_h_m = MyCheckBox(
self.row3, 0, 10, "Include H-"
)
# Temp_min
self.entry_temp_min = MyEntry(
self.row3, 0, 12, "300", "Temp Min [K]:", color=self.color, columnspan=2
)
# Temp_max
self.entry_temp_max = MyEntry(
self.row3, 0, 14, "3000", "Temp Max [K]:", color=self.color, columnspan=2
)
# Temp_step
self.entry_temp_step = MyEntry(
self.row3, 0, 16, "150", "Temp Step [K]:", color=self.color, columnspan=2
)
# Radiative transfer code
self.dropdown_retrieval_mode = MyDropdown(
self.row4, 0, 1, ConstantVariables.LIST_RETRIEVAL_MODE,
"Radiative transfer code:", color=self.color, columnspan=2
)
# min_pressure
self.entry_min_pressure = MyEntry(
self.row4, 0, 3, "-9", "Min pressure (Eq, 10^):", color=self.color, columnspan=2
)
# max_pressure
self.entry_max_pressure = MyEntry(
self.row4, 0, 6, "2", "Max pressure (Eq, 10^):", color=self.color, columnspan=2
)
# Pressure Layers
self.entry_pressure_layers = MyEntry(
self.row4, 0, 8, "100", "# Pressure Layers:", color=self.color, columnspan=2
)
# lbl sampling
self.entry_lbl_sampling = MyEntry(
self.row4, 0, 10, "4", "LBL Sampling:", color=self.color, columnspan=2
)
# Nights
self.textfield_model_name = MyTextField(
self.row5, 0, 7, "h2o", "Name model:", color=self.color, columnspan=3, width=20
)
# min_wl_hr
self.entry_min_wl_hr = MyEntry(
self.row5, 0, 10, "0.9", "Min wl HR[um]:", color=self.color, columnspan=2
)
# max_wl_hr
self.entry_max_wl_hr = MyEntry(
self.row5, 0, 12, "2.6", "Max wl HR[um]:", color=self.color, columnspan=2
)
# Help button
self._create_help_button()
self.panel_parameters = MyPanel(self, self.color, 5, 1)
# Global help button for column descriptions
self._create_global_help_button()
# Initialize parameter structures
self.yes_1 = 1
self.no_0 = 0
self.normal = "normal"
self.disabled = "disabled"
self.params_list = ConstantVariables.params_list
self.param_array = [None for _ in range(len(self.params_list))]
self.list_multiple_param = ConstantVariables.LIST_MULTIPLE_PARAM
self.start_molec = self.get_param_index("H2")
# Create parameters organized by panel from parameters.yaml
self._create_parameters_by_panel()
def _create_parameters_by_panel(self):
"""
Create parameters organized by their panel grouping from parameters.yaml using tabs.
Creates 8 tabs (one for each panel 0-7) and displays parameters in table format.
Parameters with row_manual < 0 are hidden in rowinvisible panel.
"""
# Create tab panel structure
self.tab_manager = MyTabPanel(
parent=self.panel_parameters,
tab_list=ConstantVariables.list_tab_frame_parameter,
row=0,
column=2
)
# Create panel frames for each tab with table headers
color_params = ConstantVariables.COLOR_SUB_NOTEBOOK_2
self.list_panel = []
for panel_idx, panel in enumerate(self.tab_manager.tab_list):
# Create panel frame for this tab
panel_frame = MyPanel(panel, color_params, 0, 0)
self.list_panel.append(panel_frame)
# Track row and column position within each panel (start at 1 since row 0 is headers)
NUM_PARAMETER_PANELS = 8 # Number of parameter tabs (Planet+Data, Clouds, Temperatures, etc.)
MAX_ROWS_PER_COLUMN = 4 # Maximum rows before moving to next column
COLUMNS_PER_PARAMETER_TABLE = 9 # Total columns occupied by one parameter table (7 + 1 spacing)
panel_rows = {i: 1 for i in range(NUM_PARAMETER_PANELS)}
panel_columns = {i: 0 for i in range(NUM_PARAMETER_PANELS)} # Track current column offset
panel_headers_created = {i: [0] for i in range(NUM_PARAMETER_PANELS)} # Track which columns have headers
# Track which panels have special buttons for help text
panel_has_vmr_button = [False] * NUM_PARAMETER_PANELS
panel_has_hce_button = [False] * NUM_PARAMETER_PANELS
panel_has_molecule_button = [False] * NUM_PARAMETER_PANELS
panel_has_condensed_button = [False] * NUM_PARAMETER_PANELS
panel_has_hydrogen_button = [False] * NUM_PARAMETER_PANELS
# Create initial headers for all panels (first column)
labels = ["Parameter", "Present", "Value", "Type", "Rayleigh"]
columnspan_values = [3, 1, 2, 1, 1]
column_positions = [1, 4, 5, 7, 8]
for panel_idx in range(NUM_PARAMETER_PANELS):
for label, colpos, colsp in zip(labels, column_positions, columnspan_values):
MyLabel(
self.list_panel[panel_idx], 0, colpos,
label_text=label,
columnspan=colsp,
color=color_params
)
# Iterate through params and organize by panel
for param_name in ConstantVariables.PARAMS_DICT.keys():
temp_param = ConstantVariables.PARAMS_DICT[param_name]
# Extract parameter properties
name = temp_param["name"]
panel_idx = temp_param["panel"]
row_manual = temp_param["row_manual"]
present = temp_param["present"]
state = temp_param["state"]
mass = temp_param["mass"]
if mass == -999:
mass = None
rayleigh = temp_param["rayleigh"]
type_params = temp_param["type"]
# Get parameter value (handle special cases)
value = temp_param["value"]
if value in ["target_kp", "h_ecc", "k_ecc", "radius", "temperature", "omega"]:
if self.target_information.kp is None:
value = -99999
else:
if value == "target_kp":
value = self.target_information.kp
elif value == "h_ecc":
value = np.sqrt(self.target_information.eccentricity) * np.sin(
self.target_information.argument_of_periastron)
elif value == "k_ecc":
value = np.sqrt(self.target_information.eccentricity) * np.cos(
self.target_information.argument_of_periastron)
elif value == "radius":
value = self.target_information.radius
elif value == "temperature":
value = self.target_information.equilibrium_temperature
elif value == "omega":
value = 2 * np.pi / self.target_information.period
elif value == "nothing":
value = ""
else:
value = float(value)
# Check if parameter should be visible (row_manual >= 0)
is_visible = row_manual >= 0
if is_visible:
# Get target panel
current_tab_parameter = self.list_panel[panel_idx]
# Check if we need to move to next column (more than 4 rows)
if panel_rows[panel_idx] > MAX_ROWS_PER_COLUMN:
# Reset row to 1 and move to next column
panel_rows[panel_idx] = 1
panel_columns[panel_idx] += 1
# Create headers for the new column if not already created
if panel_columns[panel_idx] not in panel_headers_created[panel_idx]:
column_offset = panel_columns[panel_idx] * COLUMNS_PER_PARAMETER_TABLE
# Add vertical separator between columns
separator = tk.Frame(
current_tab_parameter,
width=2,
bg='gray',
highlightbackground='gray',
highlightthickness=1
)
separator.grid(
row=0,
column=column_offset,
rowspan=MAX_ROWS_PER_COLUMN + 2,
sticky='ns',
padx=10,
pady=5
)
for label, colpos, colsp in zip(labels, column_positions, columnspan_values):
MyLabel(
current_tab_parameter, 0, colpos + column_offset,
label_text=label,
columnspan=colsp,
color=color_params
)
panel_headers_created[panel_idx].append(panel_columns[panel_idx])
# Get row and calculate column offset
row = panel_rows[panel_idx]
column_offset = panel_columns[panel_idx] * COLUMNS_PER_PARAMETER_TABLE
column = 1 + column_offset
# Create parameter widget
self.param_array[self.get_param_index(name)] = ParamClassForManualPlot(
current_tab_parameter, row, column,
name, value, present, state,
color=self.color, mass_molec=mass, rayleigh=rayleigh, type_params=type_params
)
# Special handling for f_rot parameter - add kernel sampling after the parameter
if name == "f_rot":
# Place kernel sampling to the right of the parameter (after column 6)
self.entry_rv_sampling = MyEntry(
current_tab_parameter, row, 10 + column_offset, "0.1",
"Kernel sampling (km/s):", color=self.color, columnspan=3, entry_width=5
)
# Special handling for width_peak parameter (VMR)
if name == "width_peak":
row += 1
panel_rows[panel_idx] += 1
panel_has_vmr_button[panel_idx] = True
# Create separate panel for VMR widgets
self.panel_vmr = MyPanel(current_tab_parameter, color_params, row, 1, columnspan=15)
# VMR widgets in separate panel
self.textfield_molec_selected = MyTextField(
self.panel_vmr, 0, 1, "", "Current molecules:",
color=self.color, columnspan=5, width=40
)
self.button_update_params_vmr = MyButton(
self.panel_vmr, 0, 6, 'UPDATE VMR PARAMETERS', "#00AA00",
command=self.update_params_vmr_func, columnspan=5, width=25
)
row += 1
panel_rows[panel_idx] += 1
# Special handling for sio_ratio_linear parameter (equilibrium chemistry elements)
elif name == "sio_ratio_linear":
row += 1
panel_rows[panel_idx] += 1
panel_has_hce_button[panel_idx] = True
# Element selector in separate panel (row 0)
elems = ConstantVariables.LIST_ELEMENT_FOR_HYBRID
self.elems_list = ParamClassForManualPlot(
current_tab_parameter,
row,
1,
"",
-5,
self.no_0,
self.normal,
molec_line=self.yes_1,
mass_molec=0,
color=self.color,
molecs=elems,
isotopes=None,
opacity_line_lists=None,
isotope_masses={elem: 0 for elem in elems},
type_params="Log10",
single_dropdown=True
)
row += 1
# Create separate panel for element selection widgets
self.panel_elements = MyPanel(current_tab_parameter, color_params, row, 1, columnspan=15)
# Button on row 1 in separate panel
self.button_hce = MyButton(
self.panel_elements, 1, 1, 'ADD ELEMENT TO HYBRID CHEMICAL EQUILIBRIUM', "#00AA00",
columnspan=15, command=self.update_elem_textfield
)
# Textfield on row 2 in separate panel
self.textfield_hce = MyTextField(
self.panel_elements, 2, 1, "", label_text="Current elements:",
columnspan=15, color=color_params, height=5, width=110
)
row += 1
panel_rows[panel_idx] += 1
# Special handling for e- parameter (molecules)
elif name == "e-":
row += 1
panel_rows[panel_idx] += 1
panel_has_hydrogen_button[panel_idx] = True
panel_has_molecule_button[panel_idx] = True
# Create panel for UPDATE H-, H, e- button
self.panel_hydrogen_minus = MyPanel(current_tab_parameter, color_params, row + 1, 10, columnspan=15)
self.button_update_hydrogen = MyButton(
self.panel_hydrogen_minus, 0, 1, 'UPDATE H-, H, e-',
"#00AA00", command=self.update_hydrogen_menus_btn
)
row = 5
panel_rows[panel_idx] += 1
# Molecule selector in separate panel (row 0)
molecs, isotopes, opacity_line_lists, _, isotope_masses = get_line_lists()
self.molecs_list = ParamClassForManualPlot(
current_tab_parameter,
row,
1,
"",
-5,
self.yes_1,
self.normal,
molec_line=self.yes_1,
mass_molec=0,
color=self.color,
molecs=molecs, isotopes=isotopes, opacity_line_lists=opacity_line_lists,
isotope_masses=isotope_masses, type_params="Log10"
)
row += 1
# Create separate panel for molecule selection widgets
self.panel_molecules = MyPanel(current_tab_parameter, color_params, row, 1, columnspan=15)
# Button on row 1 in separate panel
self.button_molecs = MyButton(
self.panel_molecules, 1, 1, 'ADD MOLECULE', "#00AA00",
columnspan=15, command=self.update_molec_textfield
)
# Textfield on row 2 in separate panel
self.textfield_molecs = MyTextField(
self.panel_molecules, 2, 1, "", label_text="Current molecules:",
columnspan=15, color=color_params, height=5, width=110
)
row += 1
panel_rows[panel_idx] += 1
# Special handling for eddy_diff_coeff parameter (condensed molecules)
elif name == "eddy_diff_coeff":
row += 1
panel_rows[panel_idx] += 1
panel_has_condensed_button[panel_idx] = True
# Get condensed molecule data
condensed_molecs_list, condensed_names_list, condensed_masses, condensed_visualization_name = get_condensed_line_list()
# Condensed molecule selector using ParamClassForManualPlot (row 0 in separate panel)
# Use single_dropdown=True for condensed molecules (only one dropdown, not three)
self.condensed_molecs_list = ParamClassForManualPlot(
current_tab_parameter,
row,
1,
"",
-7,
self.no_0,
self.normal,
molec_line=self.yes_1,
mass_molec=0,
color=self.color,
molecs=condensed_visualization_name,
isotopes=None,
opacity_line_lists=None,
isotope_masses={condensed_visualization_name[i]: condensed_masses[i] for i in range(len(condensed_visualization_name))},
type_params="Log10",
single_dropdown=True
)
row += 1
# Create separate panel for condensed molecule selection widgets
self.panel_condensed = MyPanel(current_tab_parameter, color_params, row, 1, columnspan=15)
# Store mapping for retrieval
self.condensed_molecs_list_data = condensed_molecs_list
self.condensed_visualization_name_data = condensed_visualization_name
# Button on row 0 in separate panel
self.button_condensed_molecs = MyButton(
self.panel_condensed, 0, 1, 'ADD CONDENSED MOLECULE', "#00AA00",
columnspan=7, command=self.update_condensed_molec_textfield
)
# Textfield on row 1 in separate panel
self.textfield_condensed_molecs = MyTextField(
self.panel_condensed, 1, 1, "",
label_text="Current condensed molecules:",
columnspan=7, color=color_params, height=5, width=110
)
row += 1
panel_rows[panel_idx] += 1
# Increment row counter for this panel
panel_rows[panel_idx] += 1
else:
# Parameter is hidden (row_manual < 0)
self.param_array[self.get_param_index(name)] = ParamClassForManualPlot(
self.rowinvisible, 0, 1, name, value, present, state,
color=self.color, mass_molec=mass, rayleigh=rayleigh, type_params=type_params
)
# Pack all panel frames
for panel in self.list_panel:
panel.pack(padx=3, pady=3)
# Create help buttons for each panel
for panel_idx in range(NUM_PARAMETER_PANELS):
_create_tab_help_button_manual(
panel_idx,
self.list_panel[panel_idx],
has_vmr_button=panel_has_vmr_button[panel_idx],
has_hce_button=panel_has_hce_button[panel_idx],
has_molecule_button=panel_has_molecule_button[panel_idx],
has_condensed_button=panel_has_condensed_button[panel_idx],
has_hydrogen_button=panel_has_hydrogen_button[panel_idx]
)
def _create_plot_area(self):
"""Create controls and text area (plot opens in standalone window)."""
# Control buttons
self.button_plot = MyButton(
self.row_plot, 0, 1, 'PLOT', '#FF0000',
self.plot, color_panel=self.color
)
self.button_save = MyButton(
self.row_plot, 1, 1, 'SAVE PLOT + DATA', '#FFFFFF',
self.save_model, color_panel=self.color
)
# Medians text field
self.textfield_medians_errors = MyTextField(
self.row_plot, 0, 2, "", 'Values',
color=self.color, columnspan=3, rowspan=15, height=15
)
def _setup_event_handlers(self):
"""Setup event handlers for UI components."""
self.dropdown_t_p_profile.chosen_var.trace("w", lambda *args: self.update_atmo())
self.dropdown_chemistry.chosen_var.trace("w", lambda *args: self.update_frame())
self.dropdown_target.chosen_var.trace("w", lambda *args: self.update_target())
self.check_scattering.var.trace("w", lambda *args: self.scattering_check())
self.checkbox_include_h_m.var.trace("w", lambda *args: self.include_h_m())
def _create_help_button(self):
"""Create help button with documentation for Frame Manual Model."""
help_text = {
"Manual Model Overview": "This panel allows you to manually create atmospheric models for exoplanets by configuring radiative transfer parameters, molecular compositions, and temperature-pressure profiles.",
"Target": "Select the exoplanet to analyze. Target list is populated from folders in the target directory specified in configuration. Target properties (mass, radius, temperature) are automatically loaded.",
"Stellar Spectrum": "Model for the stellar spectrum. 'Planck' uses a blackbody approximation. 'Phoenix' uses detailed PHOENIX stellar atmosphere models.",
"Resolution": "Spectral resolution mode for the model. 'HR' = high-resolution only in this manual model interface.",
"Rad. Transf. Mode": "Radiative transfer mode. 'Transmission' for transmission spectroscopy (transiting planets), 'Emission' for emission spectroscopy (secondary eclipses or direct imaging).",
"Chemistry": "Atmospheric chemistry model. 'Equilibrium' uses chemical equilibrium calculations. 'Free' allows independent molecular abundances. 'Hybrid' combines both approaches by varying the x/H ratio (where x could be any atom) manually.",
"T-P profile": "Temperature-pressure profile parameterization. There are several options: 'isot' (which stands for isothermal), 'Guillot' (Guillot (2010) temperature profile based on the three-channel Eddington approximation, as described Line et al. (2013)), 'madhu' (temperature profile from Madhusudhan & Seager (2009)), 'personalized' (that is a simpler representation for inverted profiles), 'FourNodeSpline' (that uses four pressure-temperature anchor points connected by cubic splines. Balances flexibility with parameter efficiency by interpolating between pressure levels), and the possibility (in testing) to include other PT profiles.",
"Scattering power law": "Enable to include scattering opacity as a power law. Adds scattering parameters (amplitude and slope) to the model.",
"Continuum": "Continuum opacity sources to include. Default is 'H2-H2,H2-He' for collision-induced absorption. Comma-separated list.",
"Include H-": "Include H- (hydrogen anion) bound-free and free-free opacity. Important for hot stellar atmospheres and irradiated planets.",
"UPDATE H-, H, e- button": "Updates the abundances of H- (hydrogen minus), H (atomic hydrogen), and e- (free electrons) based on the H2 and He abundances. Calculates derived abundances for consistency in hot atmosphere models.",
"Temp Min [K]": "Minimum temperature in Kelvin (required by PyratBay, ongoing).",
"Temp Max [K]": "Maximum temperature in Kelvin (required by PyratBay, ongoing).",
"Temp Step [K]": "Temperature step size in Kelvin (required by PyratBay, ongoing).",
"Radiative transfer code": "Determines which radiative transfer code to use for the model. 'petitRADTRANS' is the default, PyratBay is in development.",
"Min pressure": "Minimum pressure (log10 bar) for pressure grid. Typically -8 corresponds to ~1e-8 bar at high altitudes.",
"Max pressure": "Maximum pressure (log10 bar) for pressure grid. Typically 1 corresponds to ~10 bar at deep layers.",
"Pressure Layers": "Number of atmospheric layers for calculations. More layers increase accuracy but require more computation time.",
"LBL Sampling": "Line-by-line opacity sampling factor for petitRADTRANS. Higher values reduce computation time by sampling fewer wavelength points.",
"Name model": "Output name for saving the model results.",
"Min/Max wl HR": "Wavelength range in micrometers for high-resolution spectrum calculation.",
"Parameter Tabs": "Eight tabs organize model parameters: Stellar, Orbital, Opacity, Temperature-Pressure, Chemistry, Rotation, VMR profiles, and Additional.",
"PLOT": "Calculate and display the model spectrum with current parameters. Also generates T-P and VMR profiles.",
"BLINK": "Toggle between two calculated models for visual comparison. Requires two plots to be generated first.",
"SAVE PLOT + DATA": "Save the current model spectrum and parameters to FITS and YAML files in the target directory."
}
# Create help button in row5 at an available column position
HelpButton(
self.row5, 0, 14,
"Manual Model Help",
help_text,
columnspan=2
)
def _create_global_help_button(self):
"""Create global help button explaining parameter table column headers."""
help_text = {
"Parameter": "Parameter name. This identifier is used to reference the specific atmospheric or instrumental parameter.",
"Present": "Presence flag. Determines if this parameter is included in the current model configuration. Checkbox indicates active status.",
"Value": "Current value for the parameter. This is the value used for calculating the model spectrum. Can be numeric values representing physical quantities.",
"Type": "Parameter type for calculations. 'Linear' for parameters used in linear space. 'Log10' for parameters in log10 space (e.g., pressures, abundances, opacities)."
}
self.global_help_button = HelpButton(
self.panel_parameters, 0, 1,
"Parameter Table Columns Help",
help_text,
columnspan=1
)
def _init_state_variables(self):
"""Initialize state variables."""
self.global_exc = 1
self.current_plot = 1
self.last_auto_img = None
self.last_auto_string = None
self.curr_auto_imm = None
self.curr_auto_string = None
self.last_png_auto = None
self.last_pkl_auto = None
self.model_obj = None
[docs]
def update_molec_textfield(self):
"""
Update the molecule text field with the currently selected molecule information.
Adds a new line with molecule formula, isotope, sub-isotope, presence flag,
mass, value, VMR flag, and formula.
"""
if self.molecs_list.checkbox_presence.get_value():
last_text = self.textfield_molecs.get_text()
if last_text == "":
prefix = ""
else:
prefix = "\n"
new_line = (
f"{prefix}"
f"{self.molecs_list.dropdown_molec.get_value()},"
f"{self.molecs_list.dropdown_isotope.get_value()},"
f"{self.molecs_list.dropdown_opacity_line_list_HR.get_value()},"
f"{self.molecs_list.checkbox_presence.get_value()},"
f"{self.molecs_list.mass_molec},"
f"{self.molecs_list.entry_value.get_value()},"
f"{int(self.molecs_list.VMR)},"
f"{self.molecs_list.molec_formula},"
f"{self.molecs_list.get_rayleigh_value()}\n"
)
self.textfield_molecs.insert_text(last_text + new_line)
[docs]
def update_elem_textfield(self):
"""
Update the elements text field with currently selected element information.
Adds element for hybrid chemical equilibrium.
"""
if hasattr(self, 'elems_list') and self.elems_list.checkbox_presence.get_value():
last_text = self.textfield_hce.get_text()
prefix = "" if last_text == "" else "\n"
elem_name = self.elems_list.dropdown_molec.get_value()
new_line = (
f"{prefix}{elem_name},"
f"{self.elems_list.checkbox_presence.get_value()},"
f"{self.elems_list.entry_value.get_value()}\n"
)
self.textfield_hce.insert_text(last_text + new_line)
[docs]
def update_condensed_molec_textfield(self):
"""
Update the condensed molecules text field with currently selected molecule information.
Adds condensed molecule for cloud/aerosol modeling.
"""
if hasattr(self, 'condensed_molecs_list'):
last_text = self.textfield_condensed_molecs.get_text()
prefix = "" if last_text == "" else "\n"
# Get selected condensed molecule (visualization name from dropdown)
selected_condsensate_name = self.condensed_molecs_list.dropdown_molec.get_value()
# Find index in visualization names (convert to list if numpy array)
condensate_name_list = list(self.condensed_visualization_name_data)
selected_index = condensate_name_list.index(selected_condsensate_name)
# Get actual molecule name (internal name)
molec_name = self.condensed_molecs_list_data[selected_index]
identifier = ConstantVariables.ALL_CONDENSED_MOLECS_DICT[selected_condsensate_name]["name"]
new_line = (
f"{prefix}{identifier},"
f"{self.condensed_molecs_list.checkbox_presence.get_value()},"
f"{self.condensed_molecs_list.mass_molec},"
f"{self.condensed_molecs_list.entry_value.get_value()},"
f"{molec_name}\n"
)
self.textfield_condensed_molecs.insert_text(last_text + new_line)
[docs]
def get_param_index(self, param_name):
"""
Get the index of a parameter in the parameters list.
Args:
param_name: Name of the parameter to find
Returns:
int: Index of the parameter in the list
"""
return int(self.params_list.index(param_name))
[docs]
def update_params_vmr_func(self):
"""
Update VMR (Volume Mixing Ratio) parameters based on selected molecules.
Collects information about molecules and updates the corresponding parameter fields.
"""
counter_0 = ""
counter_1 = ""
counter_2 = ""
str_molec = ""
for elem in self.param_array[self.start_molec:]:
if elem is not None and elem.is_considered() and not elem.VMR:
counter_0 += "-3,"
counter_1 += "-5,"
counter_2 += "2,"
_, name_retrieval, _ = elem.get_name()
str_molec += name_retrieval + ","
if len(counter_0) > 0:
counter_0 = counter_0[:-1]
counter_1 = counter_1[:-1]
counter_2 = counter_2[:-1]
str_molec = str_molec[:-1]
self.param_array[self.get_param_index("vmr_peak")].entry_value.set_value(counter_0)
self.param_array[self.get_param_index("pressure_peak")].entry_value.set_value(counter_1)
self.param_array[self.get_param_index("width_peak")].entry_value.set_value(counter_2)
self.textfield_molec_selected.insert_text(str_molec)
[docs]
def update_LR(self):
"""
Update low resolution parameters based on resolution selection.
Enables/disables low resolution offset parameter based on resolution mode.
"""
if self.global_exc == 0:
return
if self.dropdown_resolution.get_value() == ConstantVariables.LIST_RESOLUTION_TABLE[1]: # High+Low
self.param_array[self.get_param_index("offsetLR")].parameter_status("normal")
else:
self.param_array[self.get_param_index("offsetLR")].parameter_status("disabled")
[docs]
def update_frame(self):
"""
Update the frame based on chemistry selection.
Enables/disables appropriate parameters based on the selected chemistry mode.
"""
if self.global_exc == 0:
return
self.param_array[self.get_param_index("kp")].parameter_status("normal")
self.param_array[self.get_param_index("rv")].parameter_status("normal")
self.param_array[self.get_param_index("sf")].parameter_status("normal")
if self.dropdown_chemistry.get_value() == ConstantVariables.LIST_CHEMISTRY_TABLE[0]: # chemcat
self.param_array[self.get_param_index("Pc")].parameter_status("normal")
self.param_array[self.get_param_index("gamma")].parameter_status("disabled")
self.param_array[self.get_param_index("H2")].parameter_status("normal")
self.param_array[self.get_param_index("He")].parameter_status("normal")
self.param_array[self.get_param_index("met")].parameter_status("normal")
self.param_array[self.get_param_index("met")].checkbox_presence.set_value(1)
self.param_array[self.get_param_index("co_ratio")].parameter_status("normal")
self.param_array[self.get_param_index("co_ratio")].checkbox_presence.set_value(1)
self.param_array[self.get_param_index("co_ratio_linear")].parameter_status("normal")
self.param_array[self.get_param_index("co_ratio_linear")].checkbox_presence.set_value(1)
self.param_array[self.get_param_index("sio_ratio_linear")].parameter_status("normal")
self.param_array[self.get_param_index("sio_ratio_linear")].checkbox_presence.set_value(1)
self.molecs_list.parameter_status("disabled")
self.molecs_list.disabled = 0
else: # Manual
self.param_array[self.get_param_index("Pc")].parameter_status("normal")
self.param_array[self.get_param_index("gamma")].parameter_status("disabled")
self.param_array[self.get_param_index("H2")].parameter_status("normal")
self.param_array[self.get_param_index("He")].parameter_status("normal")
self.param_array[self.get_param_index("met")].parameter_status("disabled")
self.param_array[self.get_param_index("co_ratio")].parameter_status("disabled")
self.param_array[self.get_param_index("co_ratio_linear")].parameter_status("disabled")
self.param_array[self.get_param_index("sio_ratio_linear")].parameter_status("disabled")
self.molecs_list.parameter_status("normal")
self.update_atmo()
self.scattering_check()
[docs]
def update_target(self):
"""
Update target-specific parameters based on selected target.
Updates parameters like Kp, temperature, and radius based on target information.
"""
if self.global_exc == 0:
return
target_id = self.dropdown_target.get_value()
self.target_information = get_target_info(self.path_folder_targets, target_id, self.dropdown_rad_mode.get_value())
self.param_array[self.get_param_index("kp")].entry_value.set_value(self.target_information.kp)
self.param_array[self.get_param_index("T0")].entry_value.set_value(self.target_information.equilibrium_temperature)
self.param_array[self.get_param_index("T_high")].entry_value.set_value(self.target_information.equilibrium_temperature)
self.param_array[self.get_param_index("T_low")].entry_value.set_value(self.target_information.equilibrium_temperature)
self.param_array[self.get_param_index("rp")].entry_value.set_value(self.target_information.radius)
self.param_array[self.get_param_index("omega")].entry_value.set_value(2 * np.pi / self.target_information.period)
[docs]
def update_atmo(self):
"""
Update atmospheric parameters based on T-P profile selection.
Enables/disables appropriate temperature and pressure parameters.
"""
if self.global_exc == 0:
return
# Disable all T-P parameters by default
self.param_array[self.get_param_index("T0")].parameter_status("disabled")
self.param_array[self.get_param_index("p1")].parameter_status("disabled")
self.param_array[self.get_param_index("p2")].parameter_status("disabled")
self.param_array[self.get_param_index("p3")].parameter_status("disabled")
self.param_array[self.get_param_index("alpha1")].parameter_status("disabled")
self.param_array[self.get_param_index("alpha2")].parameter_status("disabled")
self.param_array[self.get_param_index("gamma_g")].parameter_status("disabled")
self.param_array[self.get_param_index("kappa_IR")].parameter_status("disabled")
self.param_array[self.get_param_index("T_low")].parameter_status("disabled")
self.param_array[self.get_param_index("T_high")].parameter_status("disabled")
self.param_array[self.get_param_index("P_low")].parameter_status("disabled")
self.param_array[self.get_param_index("P_high")].parameter_status("disabled")
self.param_array[self.get_param_index("T_int")].parameter_status("disabled")
self.param_array[self.get_param_index("T0_node")].parameter_status("disabled")
self.param_array[self.get_param_index("T1_node")].parameter_status("disabled")
self.param_array[self.get_param_index("T2_node")].parameter_status("disabled")
self.param_array[self.get_param_index("T3_node")].parameter_status("disabled")
self.param_array[self.get_param_index("P1_node")].parameter_status("disabled")
self.param_array[self.get_param_index("P2_node")].parameter_status("disabled")
# Enable parameters based on selected T-P profile
if self.dropdown_t_p_profile.get_value() == ConstantVariables.LIST_PT_PROFILE_TABLE[0]:
self.param_array[self.get_param_index("T0")].parameter_status("normal")
elif self.dropdown_t_p_profile.get_value() == ConstantVariables.LIST_PT_PROFILE_TABLE[1]:
self.param_array[self.get_param_index("T0")].parameter_status("normal")
self.param_array[self.get_param_index("p1")].parameter_status("normal")
self.param_array[self.get_param_index("p2")].parameter_status("normal")
self.param_array[self.get_param_index("p3")].parameter_status("normal")
self.param_array[self.get_param_index("alpha1")].parameter_status("normal")
self.param_array[self.get_param_index("alpha2")].parameter_status("normal")
elif self.dropdown_t_p_profile.get_value() == ConstantVariables.LIST_PT_PROFILE_TABLE[2]:
self.param_array[self.get_param_index("T0")].parameter_status("normal")
self.param_array[self.get_param_index("kappa_IR")].parameter_status("normal")
self.param_array[self.get_param_index("gamma_g")].parameter_status("normal")
self.param_array[self.get_param_index("T_int")].parameter_status("normal")
elif self.dropdown_t_p_profile.get_value() == ConstantVariables.LIST_PT_PROFILE_TABLE[3]:
self.param_array[self.get_param_index("T_low")].parameter_status("normal")
self.param_array[self.get_param_index("T_high")].parameter_status("normal")
self.param_array[self.get_param_index("P_low")].parameter_status("normal")
self.param_array[self.get_param_index("P_high")].parameter_status("normal")
elif self.dropdown_t_p_profile.get_value() == ConstantVariables.LIST_PT_PROFILE_TABLE[4]:
self.param_array[self.get_param_index("T0_node")].parameter_status("normal")
self.param_array[self.get_param_index("T1_node")].parameter_status("normal")
self.param_array[self.get_param_index("T2_node")].parameter_status("normal")
self.param_array[self.get_param_index("T3_node")].parameter_status("normal")
self.param_array[self.get_param_index("P1_node")].parameter_status("normal")
self.param_array[self.get_param_index("P2_node")].parameter_status("normal")
[docs]
def scattering_check(self):
"""
Update scattering parameters based on scattering checkbox.
Enables/disables scattering-related parameters.
"""
if self.check_scattering.get_value():
self.param_array[self.get_param_index("k0")].parameter_status("normal")
self.param_array[self.get_param_index("gamma")].parameter_status("normal")
else:
self.param_array[self.get_param_index("k0")].parameter_status("disabled")
self.param_array[self.get_param_index("gamma")].parameter_status("disabled")
[docs]
def create_plot(self, nome_filef, vmr_mean):
"""
Create and save a plot of the model spectrum.
Args:
nome_filef: Base filename for saving plot
vmr_mean: Volume mixing ratio information to include in plot
Returns:
tuple: (png_file_path, pickle_file_path, string_values)
"""
fig, ax = plt.subplots(1, 1, figsize=(10, 6))
ax.plot(self.wl, self.model, "b", alpha=0.5)
ax.ticklabel_format(useOffset=False, axis='y')
ax.set_xlabel("Wavelength (nm)") # NB: avevi "sey_xlabel", typo
if self.dropdown_rad_mode.get_value() == "Transmission":
ylabel = r"1 - Transit depth ($\rm 1 - (R_{P}/R_{S})^2$)"
else:
ylabel = r"1 + Emission lines ($\rm 1 + (R_{P}/R_{S})^2$)"
ax.set_ylabel(ylabel)
ax.set_title(self.dropdown_rad_mode.get_value() + " spectrum")
filepng = nome_filef + "_temp.png"
file_pickl = nome_filef + "_temp.pkl"
string_val = ""
# Write parameters (excluding molecules)
for elem in self.dict_params_plot.keys():
if elem.replace("_", "") not in ConstantVariables.ALL_MOLEC:
string_val += elem + ": " + str(self.dict_params_plot[elem]) + "\n"
# Add VMR information
string_val += vmr_mean
string_val += (
"\nSt.Dev(model): "
+ str(np.std(self.model))
+ "\nSum(model): "
+ str(np.sum(1 - self.model))
)
# Save plot and data
with open(file_pickl, "wb") as fo:
pickle.dump(string_val, fo)
plt.savefig(fname=filepng, bbox_inches="tight", pad_inches=0.1)
plt.gca().annotate(
filepng,
xy=(1.6, 1.0),
xycoords="figure fraction",
ha="right",
va="top",
color="black",
fontsize=15,
)
plt.close(fig)
return filepng, file_pickl, string_val
[docs]
def plot(self):
"""
Generate and display a plot of the model spectrum.
Handles model calculation, plot creation, and display updates.
"""
try:
self.button_plot.button.configure(state="disabled")
# self.button_blink.button.configure(state="disabled")
# Calculate model
not_retrieval_obj, dict_params_plot = self.calc_model()
# Update plot counter
if self.current_plot == 2:
self.current_plot = 1
else:
self.current_plot = 2
# Create plot
nome_file = str(Path(self.folder_plot, "plot" + str(self.current_plot)))
self.model = not_retrieval_obj.depth_full_resolution_HR
self.wl = not_retrieval_obj.wl_full_resolution_HR
self.dict_params_plot = dict_params_plot
filepng, filepkl, string_val = self.create_plot(nome_file, not_retrieval_obj.vmr_mean)
# Update display
self.current_plot_for_saving = filepng
MyFigure(title="Manual Model", type_data="Image", image_path=filepng)
self.textfield_medians_errors.insert_text(string_val)
# Plot T-P profile
parameters_tp_profile = {
"T0": self.return_value_tp("T0"),
"kappa_IR": self.return_value_tp("kappa_IR"),
"gamma_g": self.return_value_tp("gamma_g"),
"T_int": self.return_value_tp("T_int"),
"T_low": self.return_value_tp("T_low"),
"T_high": self.return_value_tp("T_high"),
"P_low": self.return_value_tp("P_low"),
"P_high": self.return_value_tp("P_high"),
"p1": self.return_value_tp("p1"),
"p2": self.return_value_tp("p2"),
"p3": self.return_value_tp("p3"),
"alpha1": self.return_value_tp("alpha1"),
"alpha2": self.return_value_tp("alpha2"),
"T0_node": self.return_value_tp("T0_node"),
"T1_node": self.return_value_tp("T1_node"),
"T2_node": self.return_value_tp("T2_node"),
"T3_node": self.return_value_tp("T3_node"),
"P1_node": self.return_value_tp("P1_node"),
"P2_node": self.return_value_tp("P2_node"),
}
plot_tp_profile(
int(self.entry_min_pressure.get_value()),
int(self.entry_max_pressure.get_value()),
int(self.entry_pressure_layers.get_value()),
parameters_tp_profile,
self.target_information.gravity,
self.dropdown_t_p_profile.get_value(),
Path(nome_file).parent,
False,
None
)
plot_vmr(
self.model_obj.atmosphere.pressure_data.pressures,
not_retrieval_obj.vmr_dict,
Path(nome_file).parent
)
except Exception as e:
MyFigure(title="Error in manual plot", message=str(e) + "\n" + traceback.format_exc())
print(str(e) + "\n" + str(traceback.format_exc()))
finally:
self.button_plot.button.configure(state="normal")
# self.button_blink.button.configure(state="normal")
[docs]
def return_value_tp(self, name):
"""
Get temperature-pressure parameter value with error handling.
Args:
name: Name of the parameter
Returns:
ValueErrorTP: Parameter value with error handling
"""
a = self.param_array[self.get_param_index(name)].entry_value.get_value()
if a is not None:
a = float(a)
if name in ["kappa_IR", "gamma_g"]:
a = 10 ** a
return ValueErrorTP(a, 0)
[docs]
def blink(self):
"""Toggle between two plots for comparison (currently disabled)."""
pass
[docs]
def stop_blink(self, event):
"""Stop the plot blinking animation."""
self.stop_blink_var = True
[docs]
def display_imm_blink(self):
"""Display blinking animation between plots (currently disabled)."""
pass
[docs]
def display_imm(self):
"""Display plot switching animation (currently disabled)."""
pass
[docs]
def divide_instrument(self, inst_str, resolution):
"""
Process instrument string based on resolution.
Args:
inst_str: Instrument string to process
resolution: Resolution mode (HR/LR)
Returns:
str: Processed instrument string
"""
inst_str = inst_str.replace(",", "&")
inst_str = inst_str.replace(";", "|")
instr_list = inst_str.split("|")
for elem in instr_list:
instr_parts = elem.split("&")
if resolution == "HR":
path_sub = (instr_parts[0] + "&" +
create_path_night(self.path_folder_targets, self.target_information.name,
self.dropdown_rad_mode.get_value(), instr_parts[0])
if "H" in self.dropdown_resolution.get_value() else "")
else:
path_sub = (instr_parts[0] +
"&" +
str(Path(
self.path_folder_targets, self.target_information.name, "LR_Instruments",
self.dropdown_rad_mode.get_value(), instr_parts[0]))
) if "L" in self.dropdown_resolution.get_value() else ""
inst_str = inst_str.replace(instr_parts[0], path_sub)
return inst_str
[docs]
def calc_model(self):
"""
Calculate the model spectrum based on current parameters.
Returns:
tuple: (not_retrieval_obj, dict_params)
"""
minwlen_hr = float(self.entry_min_wl_hr.get_value())
maxwlen_hr = float(self.entry_max_wl_hr.get_value())
range_min_pressure = int(self.entry_min_pressure.get_value())
range_max_pressure = int(self.entry_max_pressure.get_value())
pressure_layers = int(self.entry_pressure_layers.get_value())
rad_mode = self.dropdown_rad_mode.get_value()
if rad_mode == "Emission":
folder_stellar_spc = str(Path(self.path_folder_targets, self.target_information.name, "Star_Spectrum"))
path_pheonix = download_star_spectrum(self, self.target_information, folder_stellar_spc)
if path_pheonix is None:
return None, None
# Initialize parameter dictionaries
dict_params = dict()
current_string_molec = self.textfield_molecs.get_text()
list_condensed_current_plot = []
best_fit_parameters = []
mass_vector = []
additional_opac = False
# Process parameters
for elem in self.param_array:
if elem is not None and elem.is_considered():
name, _, _ = elem.get_name()
if name in ["k_opac", "k_cond", "lambda0_micron", "omega_scale_micron", "xi"]:
additional_opac = True
dict_params[name] = elem.get_value()
best_fit_parameters.append(elem.get_value())
if elem.mass_molec is not None:
mass_vector.append(elem.mass_molec)
# Setup paths
table_output_file = None
id_process = None
username = os.environ.get("USER")
tar_folder = str(Path(self.path_default, "GUIBRUSHR", "Files", "Temp_Files_GUI", username, "tar_unpack"))
os.system("rm -rf " + tar_folder)
os.system("mkdir -p " + tar_folder)
# Setup model composition
manual_model_composition = dict()
manual_model_composition["H2"] = float(self.param_array[self.get_param_index("H2")].entry_value.get_value())
manual_model_composition["He"] = float(self.param_array[self.get_param_index("He")].entry_value.get_value())
# Handle H- inclusion
if self.checkbox_include_h_m.get_value():
manual_model_composition["H-"] = float(self.param_array[self.get_param_index("H-")].entry_value.get_value())
manual_model_composition["H"] = float(self.param_array[self.get_param_index("H")].entry_value.get_value())
manual_model_composition["e-"] = float(self.param_array[self.get_param_index("e-")].entry_value.get_value())
# Process molecule list
molec_list = self.textfield_molecs.get_text()
# Process condensate list
condensed_list = self.textfield_condensed_molecs.get_text()
# Process hybrid list
hybrid_list = self.textfield_hce.get_text()
if molec_list == "":
messagebox.showerror("NO MOLEC SELECTED", "NO MOLEC SELECTED\nPlease select at least one molecule.\n")
return None, None
for molec in molec_list.split("\n"):
string_comp = molec.split(",")
molec_formula = string_comp[0]
name = string_comp[2]
mass = float(string_comp[4])
value_during_retrieval = float(string_comp[5])
mass_vector.append(mass)
dict_params[molec_formula] = value_during_retrieval
best_fit_parameters.append(value_during_retrieval)
manual_model_composition[molec_formula] = value_during_retrieval
if condensed_list != "":
for condensed in condensed_list.split("\n"):
string_cond = condensed.split(",")
name = string_cond[0]
mass = float(string_cond[2])
mass_vector.append(mass)
value_during_retrieval = float(string_cond[3])
dict_params[name] = value_during_retrieval
best_fit_parameters.append(value_during_retrieval)
list_condensed_current_plot.append(name)
manual_model_composition[name] = value_during_retrieval
if hybrid_list != "":
for hybrid in hybrid_list.split("\n"):
string_hybrid = hybrid.split(",")
name = string_hybrid[0]
value_during_retrieval = float(string_hybrid[2])
dict_params[name] = value_during_retrieval
best_fit_parameters.append(value_during_retrieval)
# Create manual model object
manual_model_obj = ManualModelObjClass(
self.param_array,
molec_list,
condensed_list,
hybrid_list,
self.dropdown_resolution.get_value(),
self.textfield_continuum_opacity.get_text(),
self.target_information,
self.dropdown_chemistry.get_value(),
False,
self.dropdown_t_p_profile.get_value(),
table_output_file,
self.path_results,
id_process,
self.path_results,
mass_vector,
float(self.entry_temp_min.get_value()),
float(self.entry_temp_max.get_value()),
float(self.entry_temp_step.get_value()),
rad_mode,
self.dropdown_retrieval_mode.get_value(),
manual_model_composition,
float(self.entry_rv_sampling.get_value()),
stellar_spectrum=self.dropdown_stellar_spectrum.get_value(),
path_targets=Path(self.path_folder_targets, self.target_information.name),
additional_opac=additional_opac,
)
# Create or update model data
if self.model_obj is None:
load_new_opacities = True
self.model_obj = ModelData(
id_process=None,
table_output_file=None,
minwlen_hr=minwlen_hr,
maxwlen_hr=maxwlen_hr,
model_type="Manual",
lbl_sampling_hr=int(self.entry_lbl_sampling.get_value()),
lbl_sampling_lr=1,
range_min=range_min_pressure,
range_max=range_max_pressure,
nlayers=pressure_layers,
manual_model_obj=manual_model_obj,
load_new_opacities=load_new_opacities,
path_default=self.path_default
)
else:
self.model_obj.initial_param_array = [None for _ in self.model_obj.params_list]
if self.past_string is None:
cond1 = True
else:
past_lines = {line.split(',')[0]: line for line in self.past_string.strip().split('\n') if line.strip()}
curr_lines = {line.split(',')[0]: line for line in current_string_molec.strip().split('\n') if line.strip()}
cond1 = False
if past_lines.keys() != curr_lines.keys():
cond1 = True
else:
for key in past_lines:
p = past_lines[key].split(',')
c = curr_lines[key].split(',')
if p[:3] != c[:3] or p[-1] != c[-1]:
cond1 = True
break
cond2 = (
"Low" in self.dropdown_resolution.get_value()
and not self.model_obj.atmosphere.resolution_obj.low_resolution()
)
cond3 = (
"High" in self.dropdown_resolution.get_value()
and not self.model_obj.atmosphere.resolution_obj.high_resolution()
)
cond4 = self.last_min_wl != minwlen_hr or self.last_max_wl != maxwlen_hr
cond5 = (self.last_min_pressure != range_min_pressure or
self.last_max_pressure != range_max_pressure or
self.last_pressure_layers != pressure_layers)
cond6 = list_condensed_current_plot != self.list_condensed_last_plot
cond7 = rad_mode != self.last_rad_mode
cond9 = self.textfield_continuum_opacity.get_text() != self.last_continuum_opacity
cond8 = False
for molec_x in ["H2", "He", "H-", "H", "e-"]:
current_rayleigh = self.param_array[self.get_param_index(molec_x)].get_rayleigh_value()
last_rayleigh = self.dict_rayleigh_base[molec_x]
cond8 = cond8 or (current_rayleigh != last_rayleigh)
self.dict_rayleigh_base[molec_x] = current_rayleigh
if cond1 or cond2 or cond3 or cond4 or cond5 or cond6 or cond7 or cond9:
print("Generation of new atmosphere obj")
load_new_opacities = True
manual_model_obj.rad_mode = rad_mode
manual_model_obj.path_targets = Path(self.path_folder_targets, self.target_information.name)
else:
load_new_opacities = False
self.model_obj.populate_from_manual_model(
manual_model_obj, load_new_opacities,
None, None, minwlen_hr, maxwlen_hr,
range_min_pressure, range_max_pressure, pressure_layers
)
self.last_rad_mode = rad_mode
self.past_string = current_string_molec
self.list_condensed_last_plot = list_condensed_current_plot
self.last_continuum_opacity = self.textfield_continuum_opacity.get_text()
param_full = self.model_obj.create_param_full(best_fit_parameters)
_, _, not_retrieval_obj = self.model_obj.lh_function_gib(param_full)
self.last_min_wl = minwlen_hr
self.last_max_wl = maxwlen_hr
self.last_min_pressure = range_min_pressure
self.last_max_pressure = range_max_pressure
self.last_pressure_layers = pressure_layers
return not_retrieval_obj, dict_params
[docs]
def save_model(self):
"""
Save the current model results and plots.
Creates necessary directories and saves data in various formats.
"""
if self.current_plot_for_saving is None:
MyFigure(title="Error in saving model", message="No model to save")
return
try:
# Create destination directory
destination = str(Path(
self.path_folder_targets, self.dropdown_target.get_value(), "Models",
self.dropdown_rad_mode.get_value(), self.textfield_model_name.get_text()
))
os.system("mkdir -p " + destination)
# Copy plot
os.system(
"cp "
+ self.current_plot_for_saving
+ " "
+ str((Path(destination, "plot.png"))),
)
# Create YAML dictionary with model parameters
yaml_dict = {
'name': self.target_information.name,
'radius_jup': float(self.target_information.radius),
'mass_jup': float(self.target_information.mass),
'gravity_cm_s2': float(self.target_information.gravity),
't_eq_K': float(self.target_information.equilibrium_temperature),
'stellar_radius_sun': float(self.target_information.stellar_radius),
'stellar_mass_sun': float(self.target_information.stellar_mass),
'stellar_teff_K': float(self.target_information.stellar_effective_temperature),
'p0_log10_bar': float(self.target_information.pressure_at_layer_0),
'hjd0_days': float(self.target_information.hjd0),
'period_days': float(self.target_information.period),
'kp_km_s': float(self.target_information.kp),
'v_system_km_s': float(self.target_information.systemic_velocity),
'Limit_Phase_t14': float(self.target_information.limit_phase_t14),
'Limit_Phase_t12': float(self.target_information.limit_phase_t12),
'Limit_Phase_t23': float(self.target_information.limit_phase_t23),
'ecc': float(self.target_information.eccentricity),
'periastron_argument': float(self.target_information.argument_of_periastron),
"resolution": self.dropdown_resolution.get_value(),
"rad_mode": self.dropdown_rad_mode.get_value(),
"chemistry": self.dropdown_chemistry.get_value(),
"t_p_profile": self.dropdown_t_p_profile.get_value(),
"scattering": int(self.check_scattering.get_value()),
"continuum_op": self.textfield_continuum_opacity.get_text(),
"temp_min_pyrat": float(self.entry_temp_min.get_value()),
"temp_max_pyrat": float(self.entry_temp_max.get_value()),
"temp_step": float(self.entry_temp_step.get_value()),
"retrieval_mode": self.dropdown_retrieval_mode.get_value(),
"min_pressure": float(self.entry_min_pressure.get_value()),
"max_pressure": float(self.entry_max_pressure.get_value()),
"nlayers": int(self.entry_pressure_layers.get_value()),
"lbl": int(self.entry_lbl_sampling.get_value()),
"min_wl_hr": float(self.entry_min_wl_hr.get_value()),
"max_wl_hr": float(self.entry_max_wl_hr.get_value())
}
# Add plot parameters
yaml_dict = yaml_dict | self.dict_params_plot
# Save data
manual_model_data_dictionary = {
"wl": self.wl,
"model": self.model,
"yaml_dict": yaml_dict
}
with open(Path(destination, "data.pkl"), "wb") as fo:
pickle.dump(manual_model_data_dictionary, fo)
# Save wavelength data
hdu = fits.PrimaryHDU(self.wl)
hdul = fits.HDUList([hdu])
hdul.writeto(str(Path(destination, 'wavelength.fits')), overwrite=True)
# Save transit depth data
hdu = fits.PrimaryHDU(self.model)
hdul = fits.HDUList([hdu])
hdul.writeto(str(Path(destination, get_depth_filename(self.dropdown_rad_mode.get_value()))), overwrite=True)
# Save YAML info
with open(str(Path(destination, "info.yaml")), 'w') as f:
yaml.dump(yaml_dict, f, sort_keys=False)
MyFigure(title="Model saved", message="Model saved in " + destination)
except Exception as e:
MyFigure(title="Error in saving model", message=str(e) + "\n" + str(traceback.format_exc()))
print(str(e) + "\n" + str(traceback.format_exc()))
[docs]
def modify_target_list(self, target_list):
"""
Update the target dropdown list.
Args:
target_list: List of available targets
"""
self.dropdown_target.clean_widget()
self.dropdown_target = MyDropdown(
self.row1, 0, 0, target_list, "Target:",
color=self.color, columnspan=2
)
self.dropdown_target.chosen_var.trace("w", lambda *args: self.update_target())
[docs]
def include_h_m(self):
"""
Handle H- inclusion in the model.
Updates continuum opacity and parameter states based on H- checkbox.
"""
if self.checkbox_include_h_m.get_value():
self.textfield_continuum_opacity.insert_text("H2-H2,H2-He,H-")
self.param_array[self.get_param_index("H-")].parameter_status("normal")
self.param_array[self.get_param_index("H")].parameter_status("normal")
self.param_array[self.get_param_index("e-")].parameter_status("normal")
else:
self.textfield_continuum_opacity.insert_text("H2-H2,H2-He")
self.param_array[self.get_param_index("H-")].parameter_status("disabled")
self.param_array[self.get_param_index("H")].parameter_status("disabled")
self.param_array[self.get_param_index("e-")].parameter_status("disabled")