Source code for cosedaUI.geomfile_window

"""UI to build and validate a CrystFEL geometry file from an INI workspace."""

from PyQt6.QtWidgets import (
    QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QFileDialog, QMessageBox
)
from PyQt6.QtCore import Qt
import configparser
import os
from coseda.geomfile import build_geomfile_from_ini
import math


[docs] def calculate_electron_wavelength(acceleration_voltage_V: float): """Relativistic electron wavelength (m) from acceleration voltage (V).""" E = acceleration_voltage_V wavelength_m = 1.23e-9 / math.sqrt(E * (1 + 9.78e-7 * E)) return wavelength_m
[docs] class GeomfileWindow(QWidget): """Form-driven editor that emits a CrystFEL geometry file and updates the INI.""" def __init__(self, ini_path: str): """Load defaults from INI, derive sensible fallbacks, and build the form.""" super().__init__() self.ini_path = ini_path self.ini_config = configparser.ConfigParser() self.ini_config.read(self.ini_path) self.project_dir = os.path.dirname(self.ini_path) if "Paths" in self.ini_config and "outputfolder" in self.ini_config["Paths"]: self.project_dir = os.path.join(self.project_dir, self.ini_config["Paths"]["outputfolder"]) self.setWindowTitle("Geometry File Editor") self.layout = QVBoxLayout() self.setLayout(self.layout) self.params, self.missing = build_geomfile_from_ini(self.ini_path) # Set default values for missing keys defaults = { "data": "/entry/data/images", "dim0": "%", "dim1": "ss", "dim2": "fs", "peak_list": "/entry/data/", "peak_list_type": "cxi", "detector_shift_x": "/entry/data/det_shift_x_mm mm", "detector_shift_y": "/entry/data/det_shift_y_mm mm", "min_ss": "0", "min_fs": "0", "fs_axis": "x", "ss_axis": "y", "mask": "/mask", "mask_good": "0x01", "mask_bad": "0x00" } for key, value in defaults.items(): self.params.setdefault(key, value) self.params.setdefault("adu_per_photon", "") self.params.setdefault("min_ss", "0") self.params.setdefault("min_fs", "0") self.params["min_ss"] = "0" self.params["min_fs"] = "0" acquisition = self.ini_config["Acquisition"] if "Acquisition" in self.ini_config else {} if 'acceleration_voltage' in acquisition and not self.params.get('wavelength'): voltage_V = float(acquisition['acceleration_voltage']) self.params['wavelength'] = f"{calculate_electron_wavelength(voltage_V):.8e}" self._build_ui() self._update_missing_labels() def _build_ui(self): """Create form rows for geometry parameters and save action.""" # For each parameter, create label, line edit, and missing label self.entries = {} self.missing_labels = {} def add_row(name, label_text): hbox = QHBoxLayout() label = QLabel(label_text) label.setFixedWidth(150) line_edit = QLineEdit() line_edit.setText(str(self.params.get(name) or "")) missing_label = QLabel("") missing_label.setStyleSheet("color: red") missing_label.setFixedWidth(120) hbox.addWidget(label) hbox.addWidget(line_edit) hbox.addWidget(missing_label) self.layout.addLayout(hbox) self.entries[name] = line_edit self.missing_labels[name] = missing_label add_row("wavelength", "Wavelength (m):") add_row("adu_per_photon", "ADU per Photon:") add_row("clen", "Camera Length (m):") add_row("res", "Resolution (m):") add_row("data", "Data Path:") add_row("dim0", "Dimension 0:") add_row("dim1", "Dimension 1:") add_row("dim2", "Dimension 2:") add_row("peak_list", "Peak List Path:") add_row("peak_list_type", "Peak List Type:") add_row("detector_shift_x", "Detector Shift X (m):") add_row("detector_shift_y", "Detector Shift Y (m):") add_row("min_ss", "Min SS:") add_row("max_ss", "Max SS:") add_row("min_fs", "Min FS:") add_row("max_fs", "Max FS:") add_row("corner_x", "Corner X:") add_row("corner_y", "Corner Y:") add_row("fs_axis", "FS Axis:") add_row("ss_axis", "SS Axis:") add_row("mask", "Mask Path:") add_row("mask_file", "Mask File:") add_row("mask_good", "Mask Good:") add_row("mask_bad", "Mask Bad:") # Save button self.save_button = QPushButton("Save Geometry File") self.save_button.clicked.connect(self._save_geom_file) self.layout.addWidget(self.save_button, alignment=Qt.AlignmentFlag.AlignRight) def _update_missing_labels(self): """Highlight fields that are missing or invalid.""" # Check which parameters are missing or invalid for name, line_edit in self.entries.items(): text = line_edit.text().strip() if name in ("wavelength", "clen", "res") and not self._is_float(text): self.missing_labels[name].setText("Missing or invalid") elif name == "adu_per_photon" and text and not self._is_float(text): self.missing_labels[name].setText("Invalid") elif name in ("min_ss", "max_ss", "min_fs", "max_fs", "corner_x", "corner_y") and text and not self._is_float(text): self.missing_labels[name].setText("Invalid") # Skip validation for detector_shift_x and detector_shift_y elif name in ("detector_shift_x", "detector_shift_y"): self.missing_labels[name].setText("") else: self.missing_labels[name].setText("") def _is_float(self, value: str): """Return True if value can be parsed as float.""" try: float(value) return True except ValueError: return False def _save_geom_file(self): """Persist geometry to disk and update the INI Paths.geomfile reference.""" # Update params from UI for name, line_edit in self.entries.items(): self.params[name] = line_edit.text().strip() # Validate all fields are filled (non-empty) missing_fields = [name for name, line_edit in self.entries.items() if not line_edit.text().strip()] if missing_fields: QMessageBox.warning(self, "Missing Fields", f"Please fill in all fields. Missing: {', '.join(missing_fields)}") self._update_missing_labels() return # Validate required parameters (must be valid floats) missing_required = [] for key in ("wavelength", "clen", "res"): if not self._is_float(self.params[key]): missing_required.append(key) if missing_required: QMessageBox.warning(self, "Missing Parameters", f"Please provide valid values for: {', '.join(missing_required)}") self._update_missing_labels() return # Compose geometry file content geom_lines = [ f"wavelength={self.params.get('wavelength', '')}", f"adu_per_photon={self.params.get('adu_per_photon', '')}", f"clen={self.params.get('clen', '')}", f"res={self.params.get('res', '')}", f"data={self.params.get('data', '')}", f"dim0={self.params.get('dim0', '')}", f"dim1={self.params.get('dim1', '')}", f"dim2={self.params.get('dim2', '')}", f"peak_list={self.params.get('peak_list', '')}", f"peak_list_type={self.params.get('peak_list_type', '')}", f"detector_shift_x={self.params.get('detector_shift_x', '0.0')} mm", f"detector_shift_y={self.params.get('detector_shift_y', '0.0')} mm", f"p0/min_ss={self.params.get('min_ss', '')}", f"p0/max_ss={self.params.get('max_ss', '')}", f"p0/min_fs={self.params.get('min_fs', '')}", f"p0/max_fs={self.params.get('max_fs', '')}", f"p0/corner_x={self.params.get('corner_x', '')}", f"p0/corner_y={self.params.get('corner_y', '')}", f"p0/fs_axis={self.params.get('fs_axis', '')}", f"p0/ss_axis={self.params.get('ss_axis', '')}", f"p0/mask={self.params.get('mask', '')}", f"p0/mask_file={self.params.get('mask_file', '')}", f"p0/mask_good={self.params.get('mask_good', '')}", f"p0/mask_bad={self.params.get('mask_bad', '')}" ] geom_content = "\n".join(geom_lines) file_path = os.path.join(self.project_dir, "geometry.geom") try: with open(file_path, "w") as f: f.write(geom_content) # Update ini file with new geomfile path self.ini_config.set("Paths", "geomfile", os.path.basename(file_path)) with open(self.ini_path, "w") as configfile: self.ini_config.write(configfile) QMessageBox.information(self, "Saved", f"Geometry file saved to:\n{file_path}") self.close() except Exception as e: QMessageBox.critical(self, "Error", f"Failed to save file:\n{str(e)}") self._update_missing_labels()