Source code for GUIBRUSHR.GUIBRUSHR

"""
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()