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