"""
GUIBRUSHR - GUI for Bayesian Retrieval Using Spectroscopy at High Resolution.
This module contains the main application class for the GUIBRUSHR graphical user interface.
The application provides a comprehensive interface for atmospheric retrieval analysis
using spectroscopic data at both high and low resolution.
Classes:
WindowConfig: Configuration dataclass for window dimensions and scaling
GUIBRUSHR: Main application class managing the GUI and its components
Functions:
main: Entry point for the GUIBRUSHR application
"""
import os
os.environ['LANG'] = 'en_US.UTF-8'
os.environ['LC_ALL'] = 'en_US.UTF-8'
import platform
import signal
import socket
import time
import tkinter as tk
import tkinter.messagebox as messagebox
from dataclasses import dataclass
from pathlib import Path
from typing import Tuple
from screeninfo import get_monitors
# Thread configuration for numerical computations
# These settings help prevent thread contention in numerical libraries
# by limiting the number of threads used by various linear algebra libraries
THREAD_CONFIG = {
"OMP_NUM_THREADS": "1",
"OPENBLAS_NUM_THREADS": "1",
"MKL_NUM_THREADS": "1",
"VECLIB_MAXIMUM_THREADS": "1",
"NUMEXPR_NUM_THREADS": "1"
}
# Apply thread configuration to environment
for env_var, value in THREAD_CONFIG.items():
os.environ[env_var] = value
def _show_config_setup_dialog(configuration_path: Path) -> bool:
"""
Show a first-run setup dialog to create the configuration CSV file.
Args:
configuration_path: Path where configuration.csv should be created
Returns:
True if the file was created successfully, False if the user cancelled
"""
root = tk.Tk()
root.withdraw()
dialog = tk.Toplevel(root)
dialog.title("GUIBRUSHR - First Setup")
dialog.resizable(False, False)
created = [False]
tk.Label(
dialog,
text="Configuration file not found.\nPlease enter the required paths to create it:",
justify="left",
pady=10
).grid(row=0, column=0, columnspan=2, padx=20, sticky="w")
tk.Label(dialog, text="petitRADTRANS input_data path:").grid(row=1, column=0, sticky="w", padx=20, pady=5)
prt_var = tk.StringVar()
tk.Entry(dialog, textvariable=prt_var, width=55).grid(row=1, column=1, padx=10, pady=5)
tk.Label(dialog, text="Target folders path:").grid(row=2, column=0, sticky="w", padx=20, pady=5)
target_var = tk.StringVar()
tk.Entry(dialog, textvariable=target_var, width=55).grid(row=2, column=1, padx=10, pady=5)
def create_config():
prt_path = prt_var.get().strip()
target_path = target_var.get().strip()
if not prt_path or not target_path:
messagebox.showerror("Error", "Both paths are required.", parent=dialog)
return
configuration_path.parent.mkdir(parents=True, exist_ok=True)
with open(configuration_path, 'w') as f:
f.write("Element,Path\n")
f.write(f"petitRadTrans_path,{prt_path}\n")
f.write(f"path_target_folders,{target_path}\n")
created[0] = True
dialog.destroy()
root.destroy()
tk.Button(dialog, text="Create Configuration File", command=create_config).grid(
row=3, column=0, columnspan=2, pady=15
)
dialog.protocol("WM_DELETE_WINDOW", root.destroy)
dialog.grab_set()
root.mainloop()
return created[0]
[docs]
@dataclass
class WindowConfig:
"""
Configuration class for window management in GUIBRUSHR.
This class manages window dimensions and scaling factors for the main
application window. It provides a centralized way to handle window sizing
and positioning across different screen resolutions and platforms.
"""
# All values read from graphics.yaml via GraphicsConfig
ASPECT_RATIO: float = 1.0
MIN_WIDTH: int = 800
MIN_HEIGHT: int = 600
MAX_WIDTH: int = 1920
MAX_HEIGHT: int = 1080
DEFAULT_WIDTH: int = 1200
DEFAULT_HEIGHT: int = 900
SCREEN_SCALE_FACTOR: float = 0.8
MACOS_SCALE_FACTOR: float = 1.0
PANEL_MARGIN: int = 20
PANEL_VERTICAL_SPACING: int = 40
INPUT_PANEL_RATIO: float = 0.7
[docs]
@classmethod
def get_default_dimensions(cls) -> Tuple[int, int]:
"""
Get the default fallback window dimensions.
Returns:
Tuple of default width and height in pixels
"""
return cls.DEFAULT_WIDTH, cls.DEFAULT_HEIGHT
[docs]
@classmethod
def get_min_dimensions(cls) -> Tuple[int, int]:
"""
Get the minimum allowed window dimensions.
Returns:
Tuple of minimum width and height in pixels
"""
return cls.MIN_WIDTH, cls.MIN_HEIGHT
[docs]
@classmethod
def get_max_dimensions(cls) -> Tuple[int, int]:
"""
Get the maximum allowed window dimensions.
Returns:
Tuple of maximum width and height in pixels
"""
return cls.MAX_WIDTH, cls.MAX_HEIGHT
[docs]
@classmethod
def clamp_dimensions(cls, width: int, height: int) -> Tuple[int, int]:
"""
Clamp window dimensions to min/max bounds while enforcing the fixed aspect ratio.
The more constraining dimension (width or height) is used, then the
other is derived from ASPECT_RATIO so the ratio is always maintained.
Args:
width: Proposed window width in pixels
height: Proposed window height in pixels
Returns:
Tuple of clamped (width, height) with ASPECT_RATIO preserved
"""
# Pick the most constraining dimension and derive the other
width_from_height = int(height * cls.ASPECT_RATIO)
if width_from_height <= width:
width = width_from_height
else:
height = int(width / cls.ASPECT_RATIO)
# Clamp to bounds (ratio is already baked into MIN/MAX/DEFAULT constants)
if width < cls.MIN_WIDTH:
width, height = cls.MIN_WIDTH, cls.MIN_HEIGHT
elif width > cls.MAX_WIDTH:
width, height = cls.MAX_WIDTH, cls.MAX_HEIGHT
return width, height
[docs]
@classmethod
def calculate_centered_position(cls, window_width: int, window_height: int,
screen_width: int, screen_height: int) -> Tuple[int, int]:
"""
Calculate window position to center it on the screen.
Args:
window_width: Width of the window in pixels
window_height: Height of the window in pixels
screen_width: Width of the screen in pixels
screen_height: Height of the screen in pixels
Returns:
Tuple of x and y displacement in pixels to center the window
"""
x_offset = max(0, (screen_width - window_width) // 2)
y_offset = max(0, (screen_height - window_height) // 2)
return x_offset, y_offset
[docs]
class GUIBRUSHR:
"""
Main GUI application class for GUIBRUSHR.
This class manages the main application window and its components,
including input/output panels and process management. It handles
window initialization, panel creation, and proper cleanup on exit.
Attributes:
global_start: Flag indicating if the application is fully initialized
width: Window width in pixels
height: Window height in pixels
displace_x: Horizontal window displacement from screen edge
displace_y: Vertical window displacement from screen edge
path_default: Default application path for resources
dialog: Optional dialog window reference
window: Main application window (tkinter.Tk)
frame_input_master: Main input panel containing parameter controls
frame_output_table: Main output panel for displaying results
"""
[docs]
def __init__(self, path_default: Path) -> None:
"""
Initialize the GUIBRUSHR application.
This method sets up the main application window, initializes
dimensions based on screen size, creates input/output panels,
and prepares the application for user interaction.
Args:
path_default: Base path for the application resources
"""
# 2. IMPORT DINAMICO: Solo ora che il percorso è validato
from GUIBRUSHR.GUI.LAYOUT.GraphicsConfig import GraphicsConfig as GC
# 3. INIEZIONE DEI VALORI in WindowConfig
WindowConfig.ASPECT_RATIO = GC.ASPECT_RATIO
WindowConfig.MIN_WIDTH = GC.MIN_WIDTH
WindowConfig.MIN_HEIGHT = GC.MIN_HEIGHT
WindowConfig.MAX_WIDTH = GC.MAX_WIDTH
WindowConfig.MAX_HEIGHT = GC.MAX_HEIGHT
WindowConfig.DEFAULT_WIDTH = GC.DEFAULT_WIDTH
WindowConfig.DEFAULT_HEIGHT = GC.DEFAULT_HEIGHT
WindowConfig.SCREEN_SCALE_FACTOR = GC.SCREEN_SCALE_FACTOR
WindowConfig.MACOS_SCALE_FACTOR = GC.MACOS_SCALE_FACTOR
WindowConfig.PANEL_MARGIN = GC.PANEL_MARGIN
WindowConfig.PANEL_VERTICAL_SPACING = GC.PANEL_VERTICAL_SPACING
WindowConfig.INPUT_PANEL_RATIO = GC.INPUT_PANEL_RATIO
# Initialize window properties
self.width = None
self.height = None
self.displace_x = None
self.displace_y = None
self.global_start = False # Application not fully initialized yet
# Initialize window dimensions based on screen size
self.initialize_window()
# Store application paths
self.path_default = path_default
self.dialog = None
# Setup main window and create panels
self._setup_main_window()
self._create_panels()
self.initialize_DB()
# Mark application as fully initialized
self.global_start = True
def _setup_main_window(self) -> None:
"""
Set up the main application window with its properties and layout.
This method creates the main tkinter window, configures its properties
(title, icon, geometry), sets up grid layout, and applies window
constraints.
"""
# Create main window
self.window = tk.Tk()
# Initialize font scaling manager (must happen right after Tk())
from GUIBRUSHR.GUI.LAYOUT.ScaleManager import ScaleManager
ScaleManager.init(self.window)
# Configure grid layout - make window contents expandable
self.window.grid_rowconfigure(0, weight=1)
self.window.grid_columnconfigure(0, weight=1)
# Set window icon from application resources
icon_path = self.path_default / "GUIBRUSHR" / "Files" / "logo.png"
photo = tk.PhotoImage(file=str(icon_path))
self.window.iconphoto(True, photo)
# Set window properties
self.window.client(socket.gethostname())
geometry_string = f"{self.width}x{self.height}+{self.displace_x}+{self.displace_y}"
self.window.geometry(geometry_string)
self.window.title("GUIBRUSH®: GUI for Bayesian Retrieval Using Spectroscopy at High Resolution")
self.window.resizable(True, True)
# Set minimum window size to prevent UI from becoming unusable
min_width, min_height = WindowConfig.get_min_dimensions()
self.window.minsize(min_width, min_height)
def _create_panels(self) -> None:
"""
Create and configure the main input and output panels.
This method calculates appropriate panel dimensions based on the
window size and creates the InputPanel and OutputPanel objects
that form the main interface components.
"""
from GUIBRUSHR.GUI.Input_Output_Panels.Input_Panels.InputPanel import InputPanel
from GUIBRUSHR.GUI.Input_Output_Panels.Output_Panels.OutputPanel import OutputPanel
from GUIBRUSHR.General_Constants.FunctionsAndConstants.Constant_Variables import ConstantVariables
# Calculate panel dimensions based on window size and configuration constants
panel_width = self.width - WindowConfig.PANEL_MARGIN
available_height = self.height - WindowConfig.PANEL_VERTICAL_SPACING
input_panel_height = int(available_height * WindowConfig.INPUT_PANEL_RATIO)
output_panel_height = int(available_height * (1.0 - WindowConfig.INPUT_PANEL_RATIO))
# Create input panel (main parameter interface)
self.frame_input_master = InputPanel(
parent=self,
row=0,
column=0,
color=ConstantVariables.LIST_TAB_MACRO[0][1],
path_default=self.path_default,
width_GUI=panel_width,
height_frame=input_panel_height,
window=self.window
)
# Create output panel (results display)
self.frame_output_table = OutputPanel(
parent=self,
row=1,
column=0,
color=ConstantVariables.LIST_TAB_MACRO[0][1],
path_default=self.path_default,
width_GUI=panel_width,
height_frame=output_panel_height,
window=self.window
)
# Configure panel layout weights
self.window.rowconfigure(0, weight=5) # Input panel gets more space
self.window.columnconfigure(0, weight=1)
self.window.rowconfigure(1, weight=1) # Output panel gets less space
[docs]
def initialize_window(self) -> None:
"""
Initialize window dimensions based on screen size and configuration.
This method detects the primary monitor dimensions and calculates
appropriate window size using scaling factors. The window is automatically
centered on the screen and dimensions are clamped to min/max bounds.
"""
try:
# Get primary monitor information
monitors = get_monitors()
primary_monitor = monitors[0]
screen_width = primary_monitor.width
screen_height = primary_monitor.height
# Adjust scaling factor for macOS
scale = WindowConfig.SCREEN_SCALE_FACTOR
if platform.system() == 'Darwin':
scale *= WindowConfig.MACOS_SCALE_FACTOR
# Fit within scaled screen bounds while preserving aspect ratio
max_w = int(screen_width * scale)
max_h = int(screen_height * scale)
# Choose the constraining dimension
calculated_width = min(max_w, int(max_h * WindowConfig.ASPECT_RATIO))
calculated_height = int(calculated_width / WindowConfig.ASPECT_RATIO)
# Apply min/max constraints
self.width, self.height = WindowConfig.clamp_dimensions(calculated_width, calculated_height)
# Center window on screen
self.displace_x, self.displace_y = WindowConfig.calculate_centered_position(
self.width, self.height, screen_width, screen_height
)
except Exception as error:
# Fallback to default dimensions if monitor detection fails
print(f"Failed to get monitor info: {error}")
self.width, self.height = WindowConfig.get_default_dimensions()
# Position at top-left if we can't detect screen size
self.displace_x = 0
self.displace_y = 0
[docs]
def on_closing(self) -> None:
"""
Handle window closing event with user confirmation.
This method is called when the user attempts to close the window.
It shows a confirmation dialog and ensures proper cleanup of child
processes before terminating the application.
"""
# Don't allow closing before full initialization
if not self.global_start:
return
# Ask user for confirmation before closing
if messagebox.askokcancel("Quit", "Do you want to quit?"):
self._cleanup_processes()
self.window.destroy()
def _cleanup_processes(self) -> None:
"""
Gracefully terminate all child processes before application exit.
This method attempts to terminate child processes in two phases:
1. First tries graceful termination using SIGTERM signal
2. If process is still running after timeout, forces termination with SIGKILL
This ensures that no background processes are left running when the
application exits.
"""
# Check if frame_input_master and its components exist
if not hasattr(self, 'frame_input_master'):
return
if not hasattr(self.frame_input_master, 'frame_parameter'):
return
if not hasattr(self.frame_input_master.frame_parameter, 'panel_start'):
return
# Iterate through all tracked process IDs
process_list = self.frame_input_master.frame_parameter.panel_start.list_pid
for process_id in process_list.values():
try:
# Try graceful termination first
os.kill(process_id.pid, signal.SIGTERM)
time.sleep(0.5) # Give process time to terminate gracefully
# Check if process is still running and force kill if necessary
try:
os.kill(process_id.pid, 0) # Test if process exists
# Process still exists, force termination
os.kill(process_id.pid, signal.SIGKILL)
except ProcessLookupError:
# Process already terminated, which is what we want
pass
except Exception as error:
# Log termination errors but continue with other processes
print(f"Failed to terminate process {process_id.pid}: {error}")
[docs]
def initialize_DB(self) -> None:
"""
Initialize the SQLite database for the application.
Creates the database file and tables if they do not already exist.
"""
from GUIBRUSHR.GUI.DataInterface.DBSQLite3 import DBSQLite3
# Initialize database
database = DBSQLite3(self.path_default)
database.create_db()
database.close_DB()
[docs]
def main() -> None:
"""
Main entry point for the GUIBRUSHR application.
This function performs the following initialization steps:
1. Determines the default application path
2. Prints paths of user-editable files
3. Checks for configuration file, showing a setup dialog if missing
4. Creates the main GUI application (which initializes the database internally)
5. Sets up the window close event handler
6. Starts the main event loop
The function handles the complete lifecycle of the application from
startup to shutdown.
"""
# Determine application base path (parent of GUIBRUSHR directory)
path_default = Path(__file__).parent.parent.resolve()
# Print paths of user-editable files
yaml_dir = path_default / "GUIBRUSHR" / "Files" / "Configuration_Yaml"
db_path = path_default / "GUIBRUSHR" / "Files" / "DB" / "atmo.db"
tp_profile = path_default / "GUIBRUSHR" / "General_Constants" / "Classes" / "UserTemperatureProfile.py"
config_path = path_default / "GUIBRUSHR" / "Files" / "Configuration_Path" / "configuration.csv"
print("\n" + "=" * 65)
print("GUIBRUSHR - User-editable files:")
print(f" YAML configs : {yaml_dir}")
print(f" Database : {db_path}")
print(f" T-P profile : {tp_profile}")
print(f" Configuration : {config_path}")
print("=" * 65 + "\n")
# Check configuration file; show setup dialog if missing
if not config_path.exists():
created = _show_config_setup_dialog(config_path)
if not created:
return
# Create and configure main GUI application
gui_application = GUIBRUSHR(path_default)
gui_application.window.protocol("WM_DELETE_WINDOW", gui_application.on_closing)
# Start main event loop (this blocks until window is closed)
gui_application.window.mainloop()
if __name__ == '__main__':
main()