Source code for GUIBRUSHR.GUI.Input_Output_Panels.Input_Panels.TabPanels.FrameDBInteractions.FrameConvertOpacity

"""
Frame for converting high-resolution line-by-line opacities to correlated-k format.
This module handles the conversion of petitRADTRANS line-by-line opacity files
to lower resolution correlated-k format for faster radiative transfer calculations.
"""
import subprocess
import threading
from pathlib import Path
from tkinter import messagebox, filedialog

from GUIBRUSHR.GUI.LAYOUT.MyPanel import MyPanel
from GUIBRUSHR.GUI.WIDGET.MyButton import MyButton
from GUIBRUSHR.GUI.WIDGET.MyTextField import MyTextField
from GUIBRUSHR.General_Constants.Classes.HelpButton import HelpButton
from GUIBRUSHR.General_Constants.FunctionsAndConstants.Constant_Variables import ConstantVariables


[docs] class FrameConvertOpacity(MyPanel): """ A panel for converting line-by-line opacities to correlated-k format. This class provides functionality to: - Select input high-resolution opacity file - Set target resolution for correlated-k - Launch conversion process in background - Monitor conversion progress """
[docs] def __init__(self, parent, color, row, column, path_default, **kwargs): """Initialize the FrameConvertOpacity panel with all necessary components.""" super().__init__(parent, color, row, column, **kwargs) # Store instance variables self.path_default = path_default self.color = color self.conversion_process = None self.is_converting = False # Initialize panels self._init_panels() # Initialize controls self._init_controls()
def _init_panels(self): """Initialize the main panel columns.""" self.main_panel = MyPanel(self, self.color, 0, 0) def _init_controls(self): """Initialize conversion controls.""" # Help text dictionary help_text = { "Opacity HR Path": "Path to the input high-resolution line-by-line opacity file.\n" "Format: petitRADTRANS HDF5 file (e.g., *__*.R1e6_*.xsec.petitRADTRANS.h5)\n" "Click 'Browse' to select a file or paste the path directly.", "Output Resolution": "Target resolving power (R = λ/Δλ) for the correlated-k output.\n" "Typical values: 1000-100000\n" "Higher values = better accuracy but larger file size.\n" "Default: 40000\n" "Note: Must be lower than input resolution (~1e6).", "Conversion": "Launch the opacity conversion process in background.\n" "The conversion may take several minutes depending on file size.\n" "Output will be saved in the 'correlated_k' subdirectory.\n" "Diagnostic plots will be generated automatically." } # Create help button self.help_button = HelpButton( self.main_panel, 0, 0, "Opacity Conversion Help", help_text, columnspan=1 ) self.help_button.button.grid(rowspan=4) # Opacity HR path text field (large, with browse button) self.textfield_opacity_path = MyTextField( self.main_panel, 0, 1, "", # Initial empty text label_text="Opacity HR Path:", color=self.color, width=80, height=1, columnspan=2 ) # Browse button for selecting file self.button_browse = MyButton( self.main_panel, 0, 3, "Browse", bg="#4CAF50", command=self._browse_opacity_file, color_panel=self.color ) # Output resolution text field self.textfield_resolution = MyTextField( self.main_panel, 1, 1, "40000", # Default value label_text="Output Resolution:", color=self.color, width=15, height=1, columnspan=2 ) # Status text field (read-only display) self.textfield_status = MyTextField( self.main_panel, 2, 1, "Ready", label_text="Status:", color=self.color, width=80, height=2, columnspan=2 ) # Make status field read-only by disabling it self.textfield_status.text_widget.config(state='disabled') # Conversion button self.button_convert = MyButton( self.main_panel, 3, 1, "Start Conversion to Correlated-K", bg="#1e4fda", command=self._start_conversion, color_panel=self.color, columnspan=2 ) # Stop button (initially disabled) self.button_stop = MyButton( self.main_panel, 3, 3, "Stop", bg="#f44336", command=self._stop_conversion, color_panel=self.color ) self.button_stop.button.config(state="disabled") def _browse_opacity_file(self): """Open file dialog to browse for opacity file.""" initial_dir = Path(ConstantVariables.path_petitradtrans, "opacities", "lines", "line_by_line") filename = filedialog.askopenfilename( title="Select Line-by-Line Opacity File", initialdir=initial_dir, filetypes=[ ("HDF5 files", "*.h5"), ("All files", "*.*") ] ) if filename: self.textfield_opacity_path.insert_text(filename) def _update_status(self, status_text: str): """Update the status text field.""" self.textfield_status.text_widget.config(state='normal') self.textfield_status.insert_text(status_text) self.textfield_status.text_widget.config(state='disabled') def _get_opacity_path(self) -> str: """Get opacity path from text field.""" return self.textfield_opacity_path.text_widget.get("1.0", "end-1c").strip() def _get_resolution(self) -> str: """Get resolution from text field.""" return self.textfield_resolution.text_widget.get("1.0", "end-1c").strip() def _validate_inputs(self) -> tuple[bool, str, int]: """ Validate user inputs. Returns ------- tuple[bool, str, int] (valid, opacity_path, resolution) """ # Get opacity path opacity_path = self._get_opacity_path() if not opacity_path: messagebox.showerror("Error", "Please provide an opacity file path.") return False, "", 0 # Check if file exists if not Path(opacity_path).exists(): messagebox.showerror("Error", f"Opacity file not found:\n{opacity_path}") return False, "", 0 # Check if it's an HDF5 file if not opacity_path.endswith('.h5'): messagebox.showerror("Error", "Opacity file must be an HDF5 file (.h5)") return False, "", 0 # Get resolution resolution_str = self._get_resolution() if not resolution_str: messagebox.showerror("Error", "Please provide an output resolution.") return False, "", 0 try: resolution = int(resolution_str) if resolution <= 0: raise ValueError("Resolution must be positive") if resolution > 1e6: messagebox.showwarning( "Warning", f"Resolution {resolution} is very high.\n" f"This may result in large file sizes and long computation times." ) except ValueError: messagebox.showerror("Error", "Resolution must be a positive integer.") return False, "", 0 return True, opacity_path, resolution def _start_conversion(self): """Start the opacity conversion process in background.""" if self.is_converting: messagebox.showwarning("Warning", "Conversion already in progress.") return # Validate inputs valid, opacity_path, resolution = self._validate_inputs() if not valid: return # Confirm with user response = messagebox.askyesno( "Start Conversion", f"Start conversion of:\n{opacity_path}\n\n" f"Target resolution: R = {resolution:,}\n\n" f"This process will run in the background and may take several minutes.\n" f"Continue?" ) if not response: return # Update UI self.is_converting = True self.button_convert.button.config(state="disabled") self.button_stop.button.config(state="normal") self._update_status("Converting... (this may take several minutes)") # Launch conversion in separate thread conversion_thread = threading.Thread( target=self._run_conversion, args=(opacity_path, resolution), daemon=True ) conversion_thread.start() def _run_conversion(self, opacity_path: str, resolution: int): """ Run the conversion process in background. Parameters ---------- opacity_path : str Path to input opacity file resolution : int Target resolution """ try: # Path to conversion script script_path = Path(self.path_default, "GUIBRUSHR", "GUI", "Input_Output_Panels", "Input_Panels", "TabPanels", "FrameDBInteractions", "generate_correlated_k_from_lbl.py") if not script_path.exists(): raise FileNotFoundError(f"Conversion script not found: {script_path}") # Build command cmd = [ "python", str(script_path), "--input-file", opacity_path, "--resolution", str(resolution) ] # Run process self.conversion_process = subprocess.Popen( cmd, text=True, cwd=str(Path(self.path_default).parent) ) # Wait for completion stdout, stderr = self.conversion_process.communicate() # Check result if self.conversion_process.returncode == 0: self._conversion_success(stdout) else: self._conversion_error(stderr) except Exception as e: self._conversion_error(str(e)) finally: self.conversion_process = None def _conversion_success(self, stdout: str): """Handle successful conversion.""" self.is_converting = False # Update UI (must be done in main thread) self.after(0, lambda: self.button_convert.button.config(state="normal")) self.after(0, lambda: self.button_stop.button.config(state="disabled")) self.after(0, lambda: self._update_status("Conversion completed successfully!")) # Show success message self.after(0, lambda: messagebox.showinfo( "Success", "Opacity conversion completed successfully!\n\n" "Output file saved in the same directory as input,\n" "in the 'correlated_k' subdirectory." )) def _conversion_error(self, error_msg: str): """Handle conversion error.""" self.is_converting = False # Update UI self.after(0, lambda: self.button_convert.button.config(state="normal")) self.after(0, lambda: self.button_stop.button.config(state="disabled")) self.after(0, lambda: self._update_status("Conversion failed")) # Show error message (truncate if too long) error_display = error_msg if len(error_msg) < 500 else error_msg[:500] + "\n..." self.after(0, lambda: messagebox.showerror( "Conversion Error", f"Opacity conversion failed:\n\n{error_display}" )) def _stop_conversion(self): """Stop the running conversion process.""" if not self.is_converting or self.conversion_process is None: return response = messagebox.askyesno( "Stop Conversion", "Are you sure you want to stop the conversion?\n" "This will terminate the process immediately." ) if response: try: self.conversion_process.terminate() self.conversion_process.wait(timeout=5) except subprocess.TimeoutExpired: self.conversion_process.kill() self.is_converting = False self.conversion_process = None self.button_convert.button.config(state="normal") self.button_stop.button.config(state="disabled") self._update_status("Conversion stopped by user") messagebox.showinfo("Stopped", "Conversion process stopped.")
[docs] def cleanup(self): """Clean up resources when panel is destroyed.""" if self.is_converting and self.conversion_process: self.conversion_process.terminate()