Source code for cosedaUI.indexintegrate_sections.ici

import os
import json
import shlex
import time

from PyQt6.QtCore import Qt, QThread
from PyQt6.QtGui import QTextCursor
from PyQt6.QtWidgets import (
    QCheckBox,
    QDoubleSpinBox,
    QFileDialog,
    QFormLayout,
    QGroupBox,
    QHBoxLayout,
    QLabel,
    QLineEdit,
    QMessageBox,
    QPlainTextEdit,
    QProgressBar,
    QPushButton,
    QComboBox,
    QSpinBox,
    QVBoxLayout,
)

from coseda.ici import orchestrator as orch
from coseda.initialize import write_icisettings
from coseda.logging_utils import log_print
from cosedaUI.indexintegrate_sections.helpers import OrchestratorWorker


[docs] class IciMixin: def _init_ici_settings_tab(self): """Build the ICI settings tab UI (convergence params, flags).""" layout = QVBoxLayout(self.ici_settings_tab) form_layout = QFormLayout() form_layout.setLabelAlignment(Qt.AlignmentFlag.AlignLeft) form_layout.setFormAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop) form_layout.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow) self.ici_radius_spin = QDoubleSpinBox(self.ici_settings_tab) self.ici_radius_spin.setDecimals(3) self.ici_radius_spin.setRange(0.0, 10.0) self.ici_radius_spin.setSingleStep(0.001) self.ici_radius_spin.setValue(float(orch.radius_mm)) form_layout.addRow("Search radius (mm):", self.ici_radius_spin) self.ici_min_spacing_spin = QDoubleSpinBox(self.ici_settings_tab) self.ici_min_spacing_spin.setDecimals(5) self.ici_min_spacing_spin.setRange(0.0, 1.0) self.ici_min_spacing_spin.setSingleStep(1e-4) self.ici_min_spacing_spin.setValue(float(orch.min_spacing_mm)) form_layout.addRow("Minimum shift spacing (mm):", self.ici_min_spacing_spin) self.ici_N_conv_spin = QSpinBox(self.ici_settings_tab) self.ici_N_conv_spin.setMinimum(1) self.ici_N_conv_spin.setMaximum(1000) self.ici_N_conv_spin.setValue(int(orch.N_conv)) form_layout.addRow("Events for convergence:", self.ici_N_conv_spin) self.ici_recurring_tol_spin = QDoubleSpinBox(self.ici_settings_tab) self.ici_recurring_tol_spin.setDecimals(2) self.ici_recurring_tol_spin.setRange(0.0, 1.0) self.ici_recurring_tol_spin.setSingleStep(0.01) self.ici_recurring_tol_spin.setValue(float(orch.recurring_tol)) form_layout.addRow("Recurring shift tolerance:", self.ici_recurring_tol_spin) self.ici_median_rel_tol_spin = QDoubleSpinBox(self.ici_settings_tab) self.ici_median_rel_tol_spin.setDecimals(2) self.ici_median_rel_tol_spin.setRange(0.0, 1.0) self.ici_median_rel_tol_spin.setSingleStep(0.01) self.ici_median_rel_tol_spin.setValue(float(orch.median_rel_tol)) form_layout.addRow("Median change tolerance:", self.ici_median_rel_tol_spin) self.ici_noimprove_N_spin = QSpinBox(self.ici_settings_tab) self.ici_noimprove_N_spin.setMinimum(1) self.ici_noimprove_N_spin.setMaximum(1000) self.ici_noimprove_N_spin.setValue(int(orch.noimprove_N)) form_layout.addRow("No-improvement iterations:", self.ici_noimprove_N_spin) self.ici_noimprove_eps_spin = QDoubleSpinBox(self.ici_settings_tab) self.ici_noimprove_eps_spin.setDecimals(2) self.ici_noimprove_eps_spin.setRange(0.0, 1.0) self.ici_noimprove_eps_spin.setSingleStep(0.01) self.ici_noimprove_eps_spin.setValue(float(orch.noimprove_eps)) form_layout.addRow("Minimum improvement:", self.ici_noimprove_eps_spin) self.ici_stability_N_spin = QSpinBox(self.ici_settings_tab) self.ici_stability_N_spin.setMinimum(1) self.ici_stability_N_spin.setMaximum(1000) self.ici_stability_N_spin.setValue(int(orch.stability_N)) form_layout.addRow("Stability window:", self.ici_stability_N_spin) self.ici_stability_std_spin = QDoubleSpinBox(self.ici_settings_tab) self.ici_stability_std_spin.setDecimals(2) self.ici_stability_std_spin.setRange(0.0, 1.0) self.ici_stability_std_spin.setSingleStep(0.01) self.ici_stability_std_spin.setValue(float(orch.stability_std)) form_layout.addRow("Stability spread limit:", self.ici_stability_std_spin) self.ici_done_streak_succ_spin = QSpinBox(self.ici_settings_tab) self.ici_done_streak_succ_spin.setMinimum(1) self.ici_done_streak_succ_spin.setMaximum(1000) self.ici_done_streak_succ_spin.setValue(int(orch.done_on_streak_successes)) form_layout.addRow("Successes in streak:", self.ici_done_streak_succ_spin) self.ici_done_streak_len_spin = QSpinBox(self.ici_settings_tab) self.ici_done_streak_len_spin.setMinimum(1) self.ici_done_streak_len_spin.setMaximum(1000) self.ici_done_streak_len_spin.setValue(int(orch.done_on_streak_length)) form_layout.addRow("Streak length:", self.ici_done_streak_len_spin) self.ici_lambda_spin = QDoubleSpinBox(self.ici_settings_tab) self.ici_lambda_spin.setDecimals(2) self.ici_lambda_spin.setRange(0.0, 10.0) self.ici_lambda_spin.setSingleStep(0.1) self.ici_lambda_spin.setValue(float(orch.λ)) form_layout.addRow("Damping factor:", self.ici_lambda_spin) self.ici_no_retry_cb = QCheckBox("Disable retry", self.ici_settings_tab) self.ici_no_half_pixel_shift_cb = QCheckBox("Disable half-pixel shift", self.ici_settings_tab) self.ici_no_non_hits_cb = QCheckBox("Omit non-hits from stream", self.ici_settings_tab) self.ici_no_retry_cb.setChecked(True) self.ici_no_half_pixel_shift_cb.setChecked(True) self.ici_no_non_hits_cb.setChecked(True) form_layout.addRow(self.ici_no_retry_cb) form_layout.addRow(self.ici_no_half_pixel_shift_cb) form_layout.addRow(self.ici_no_non_hits_cb) self.ici_flags_edit = QPlainTextEdit(self.ici_settings_tab) self.ici_flags_edit.setToolTip( "Additional indexamajig flags not covered by the structured controls above." ) self.ici_radius_spin.setToolTip( "Search radius in mm for propose-next-shift (default: 0.05 mm)." ) self.ici_min_spacing_spin.setToolTip( "Minimum spacing in mm between proposed shifts (default: 5e-4 mm)." ) self.ici_N_conv_spin.setToolTip( "Minimum number of events required to consider convergence (default: 3)." ) self.ici_recurring_tol_spin.setToolTip( "Tolerance for recurring shifts; 0.1 = 10% relative difference allowed." ) self.ici_median_rel_tol_spin.setToolTip( "Median relative tolerance for convergence; 0.1 = 10% relative change threshold." ) self.ici_noimprove_N_spin.setToolTip( "Number of iterations with no improvement required to trigger 'no-improve' convergence (default: 2)." ) self.ici_noimprove_eps_spin.setToolTip( "Minimum relative improvement considered significant; 0.02 = 2% (below this counts as 'no improvement')." ) self.ici_stability_N_spin.setToolTip( "Number of iterations over which stability is evaluated (default: 3)." ) self.ici_stability_std_spin.setToolTip( "Standard deviation threshold for stability; 0.05 = 5% spread allowed in shifts." ) self.ici_done_streak_succ_spin.setToolTip( "Number of successful iterations within an unindexed streak required to consider the run 'done' (default: 2)." ) self.ici_done_streak_len_spin.setToolTip( "Length of the unindexed streak used together with done_on_streak_successes to mark the run as done (default: 5)." ) self.ici_lambda_spin.setToolTip( "Damping factor λ for refined shift updates (0 = full damping, 1 = no damping; default: 0.8)." ) index_group = QGroupBox("Indexamajig settings", self.ici_settings_tab) index_layout = QFormLayout(index_group) index_layout.setLabelAlignment(Qt.AlignmentFlag.AlignLeft) index_layout.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow) self.ici_peaks_combo = QComboBox(index_group) self.ici_peaks_combo.addItems(["cxi", "peakfinder8", "peakfinder9", "zaef", "hdf5"]) index_layout.addRow("Peaks:", self.ici_peaks_combo) self.ici_indexing_combo = QComboBox(index_group) self.ici_indexing_combo.addItems(["xgandalf", "xgandalf-nolatt-cell", "asdf", "mosflm", "dirax"]) index_layout.addRow("Indexing:", self.ici_indexing_combo) self.ici_integration_combo = QComboBox(index_group) self.ici_integration_combo.addItems(["rings", "prof2d", "none"]) index_layout.addRow("Integration:", self.ici_integration_combo) self.ici_push_res_edit = QLineEdit("0", index_group) index_layout.addRow("Resolution push:", self.ici_push_res_edit) self.ici_min_peaks_spin = QSpinBox(index_group) self.ici_min_peaks_spin.setRange(0, 10000) self.ici_min_peaks_spin.setValue(15) index_layout.addRow("Minimum peaks:", self.ici_min_peaks_spin) self.ici_max_indexer_threads_spin = QSpinBox(index_group) self.ici_max_indexer_threads_spin.setRange(1, 512) self.ici_max_indexer_threads_spin.setValue(1) index_layout.addRow("Max indexer threads:", self.ici_max_indexer_threads_spin) self.ici_tolerance_edit = QLineEdit("5,5,5,5", index_group) index_layout.addRow("Cell tolerance:", self.ici_tolerance_edit) self.ici_int_radius_edit = QLineEdit("4,5,8", index_group) index_layout.addRow("Integration radii:", self.ici_int_radius_edit) self.ici_xgandalf_tolerance_spin = QDoubleSpinBox(index_group) self.ici_xgandalf_tolerance_spin.setDecimals(4) self.ici_xgandalf_tolerance_spin.setRange(0.0, 10.0) self.ici_xgandalf_tolerance_spin.setSingleStep(0.001) self.ici_xgandalf_tolerance_spin.setValue(0.02) index_layout.addRow("xGANDALF tolerance:", self.ici_xgandalf_tolerance_spin) self.ici_sampling_pitch_spin = QSpinBox(index_group) self.ici_sampling_pitch_spin.setRange(1, 1000) self.ici_sampling_pitch_spin.setValue(5) index_layout.addRow("xGANDALF sampling pitch:", self.ici_sampling_pitch_spin) self.ici_min_lat_vec_len_spin = QDoubleSpinBox(index_group) self.ici_min_lat_vec_len_spin.setDecimals(2) self.ici_min_lat_vec_len_spin.setRange(0.0, 10000.0) self.ici_min_lat_vec_len_spin.setValue(40.0) index_layout.addRow("Minimum lattice vector:", self.ici_min_lat_vec_len_spin) self.ici_max_lat_vec_len_spin = QDoubleSpinBox(index_group) self.ici_max_lat_vec_len_spin.setDecimals(2) self.ici_max_lat_vec_len_spin.setRange(0.0, 10000.0) self.ici_max_lat_vec_len_spin.setValue(110.0) index_layout.addRow("Maximum lattice vector:", self.ici_max_lat_vec_len_spin) self.ici_grad_desc_iterations_spin = QSpinBox(index_group) self.ici_grad_desc_iterations_spin.setRange(0, 10000) self.ici_grad_desc_iterations_spin.setValue(1) index_layout.addRow("Gradient descent iterations:", self.ici_grad_desc_iterations_spin) self.ici_no_revalidate_cb = QCheckBox("Skip revalidation", index_group) self.ici_no_refine_cb = QCheckBox("Skip prediction refinement", index_group) self.ici_no_image_data_cb = QCheckBox("Prediction-only stream (no merge intensities)", index_group) self.ici_no_image_data_cb.setToolTip( "Adds --no-image-data to indexamajig. Leave this unchecked when the stream will be merged, " "because the resulting reflection table can contain zero intensities." ) self.ici_no_revalidate_cb.setChecked(True) self.ici_no_refine_cb.setChecked(True) self.ici_no_image_data_cb.setChecked(False) self.ici_no_image_data_cb.setEnabled(False) index_layout.addRow(self.ici_no_revalidate_cb) index_layout.addRow(self.ici_no_refine_cb) index_layout.addRow(self.ici_no_image_data_cb) self.ici_radius_spin.valueChanged.connect(self._ici_update_command_label) self.ici_min_spacing_spin.valueChanged.connect(self._ici_update_command_label) self.ici_N_conv_spin.valueChanged.connect(self._ici_update_command_label) self.ici_recurring_tol_spin.valueChanged.connect(self._ici_update_command_label) self.ici_median_rel_tol_spin.valueChanged.connect(self._ici_update_command_label) self.ici_noimprove_N_spin.valueChanged.connect(self._ici_update_command_label) self.ici_noimprove_eps_spin.valueChanged.connect(self._ici_update_command_label) self.ici_stability_N_spin.valueChanged.connect(self._ici_update_command_label) self.ici_stability_std_spin.valueChanged.connect(self._ici_update_command_label) self.ici_done_streak_succ_spin.valueChanged.connect(self._ici_update_command_label) self.ici_done_streak_len_spin.valueChanged.connect(self._ici_update_command_label) self.ici_lambda_spin.valueChanged.connect(self._ici_update_command_label) self.ici_flags_edit.textChanged.connect(self._ici_on_flags_text_changed) self.ici_no_retry_cb.stateChanged.connect(self._ici_on_flag_checkbox_changed) self.ici_no_half_pixel_shift_cb.stateChanged.connect(self._ici_on_flag_checkbox_changed) self.ici_no_non_hits_cb.stateChanged.connect(self._ici_on_flag_checkbox_changed) for widget in ( self.ici_peaks_combo, self.ici_indexing_combo, self.ici_integration_combo, ): widget.currentTextChanged.connect(self._ici_on_indexamajig_setting_changed) for widget in ( self.ici_push_res_edit, self.ici_tolerance_edit, self.ici_int_radius_edit, ): widget.textChanged.connect(self._ici_on_indexamajig_setting_changed) for widget in ( self.ici_min_peaks_spin, self.ici_max_indexer_threads_spin, self.ici_xgandalf_tolerance_spin, self.ici_sampling_pitch_spin, self.ici_min_lat_vec_len_spin, self.ici_max_lat_vec_len_spin, self.ici_grad_desc_iterations_spin, ): widget.valueChanged.connect(self._ici_on_indexamajig_setting_changed) for widget in ( self.ici_no_revalidate_cb, self.ici_no_refine_cb, self.ici_no_image_data_cb, ): widget.stateChanged.connect(self._ici_on_indexamajig_setting_changed) for sb in ( self.ici_N_conv_spin, self.ici_noimprove_N_spin, self.ici_stability_N_spin, self.ici_done_streak_succ_spin, self.ici_done_streak_len_spin, self.ici_radius_spin, self.ici_min_spacing_spin, self.ici_recurring_tol_spin, self.ici_median_rel_tol_spin, self.ici_noimprove_eps_spin, self.ici_stability_std_spin, self.ici_lambda_spin, self.ici_min_peaks_spin, self.ici_max_indexer_threads_spin, self.ici_xgandalf_tolerance_spin, self.ici_sampling_pitch_spin, self.ici_min_lat_vec_len_spin, self.ici_max_lat_vec_len_spin, self.ici_grad_desc_iterations_spin, ): sb.setAlignment(Qt.AlignmentFlag.AlignLeft) self.ici_flags_edit.clear() self._ici_update_command_label() layout.addLayout(form_layout) layout.addWidget(index_group) advanced_group = QGroupBox("Additional flags", self.ici_settings_tab) advanced_layout = QFormLayout(advanced_group) advanced_layout.setLabelAlignment(Qt.AlignmentFlag.AlignLeft) advanced_layout.setFormAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop) advanced_layout.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow) advanced_layout.addRow("Extra flags:", self.ici_flags_edit) layout.addWidget(advanced_group) layout.addStretch(1) def _init_ici_run_tab(self): """Build the ICI run tab UI (paths, run/stop, progress, log).""" layout = QVBoxLayout(self.ici_run_tab) paths_group = QGroupBox("Paths and basic settings", self.ici_run_tab) paths_layout = QFormLayout(paths_group) paths_layout.setLabelAlignment(Qt.AlignmentFlag.AlignLeft) paths_layout.setFormAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop) paths_layout.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow) self.ici_run_root_edit = QLineEdit(paths_group) self.ici_run_root_edit.setReadOnly(True) self.ici_run_root_edit.setToolTip("Automatically resolved from the selected INI. A timestamped ICI run folder is created when the run starts.") paths_layout.addRow("Run folder:", self.ici_run_root_edit) self.ici_geom_edit = QLineEdit(paths_group) self.ici_geom_edit.setReadOnly(True) self.ici_geom_edit.setToolTip("Generated automatically in the ICI run folder from the Geometry tab/settings.") paths_layout.addRow("Geom (.geom):", self.ici_geom_edit) self.ici_cell_edit = QLineEdit(paths_group) self.ici_cell_edit.setReadOnly(True) self.ici_cell_edit.setToolTip("Generated automatically in the ICI run folder from the Cell tab/workspace cell.") paths_layout.addRow("Cell (.cell):", self.ici_cell_edit) self.ici_h5_edit = QPlainTextEdit(paths_group) self.ici_h5_edit.setReadOnly(True) self.ici_h5_edit.setToolTip("Automatically resolved from the selected INI or workspace file.") paths_layout.addRow("HDF5 files:", self.ici_h5_edit) self.ici_max_iters_spin = QSpinBox(paths_group) self.ici_max_iters_spin.setMinimum(1) self.ici_max_iters_spin.setMaximum(10_000) self.ici_max_iters_spin.setValue(orch.DEFAULT_MAX_ITERS) self.ici_jobs_spin = QSpinBox(paths_group) self.ici_jobs_spin.setMinimum(1) self.ici_jobs_spin.setMaximum(512) self.ici_jobs_spin.setValue(int(orch.DEFAULT_NUM_CPU) if orch.DEFAULT_NUM_CPU is not None else os.cpu_count()) iters_jobs_row = QHBoxLayout() iters_jobs_row.addWidget(QLabel("Max iterations:", paths_group)) iters_jobs_row.addWidget(self.ici_max_iters_spin) iters_jobs_row.addSpacing(20) iters_jobs_row.addWidget(QLabel("Jobs:", paths_group)) iters_jobs_row.addWidget(self.ici_jobs_spin) iters_jobs_row.addStretch(1) paths_layout.addRow(iters_jobs_row) self.ici_max_iters_spin.setToolTip( "Maximum number of iterations of the orchestration loop." ) self.ici_jobs_spin.setToolTip( "Number of parallel jobs (default is os.cpu_count(), i.e. all available CPUs)." ) self.ici_max_iters_spin.setAlignment(Qt.AlignmentFlag.AlignLeft) self.ici_jobs_spin.setAlignment(Qt.AlignmentFlag.AlignLeft) layout.addWidget(paths_group) self.ici_command_edit = QLineEdit(self.ici_run_tab) self.ici_command_edit.setReadOnly(True) command_layout = QFormLayout() command_layout.setLabelAlignment(Qt.AlignmentFlag.AlignLeft) command_layout.setFormAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop) command_layout.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow) command_layout.addRow("Command:", self.ici_command_edit) layout.addLayout(command_layout) button_row = QHBoxLayout() self.ici_run_button = QPushButton("Run orchestration", self.ici_run_tab) self.ici_run_button.clicked.connect(self._ici_on_run_clicked) button_row.addWidget(self.ici_run_button) self.ici_run_all_button = QPushButton("Run all", self.ici_run_tab) self.ici_run_all_button.clicked.connect(self._ici_on_run_all_clicked) button_row.addWidget(self.ici_run_all_button) self.ici_stop_button = QPushButton("Stop", self.ici_run_tab) self.ici_stop_button.clicked.connect(self._ici_on_stop_clicked) self.ici_stop_button.setEnabled(False) button_row.addWidget(self.ici_stop_button) button_row.addStretch(1) layout.addLayout(button_row) self.ici_progress_label = QLabel( "Progress (updates with processed indexing events):", self.ici_run_tab, ) layout.addWidget(self.ici_progress_label) self.ici_progress_bar = QProgressBar(self.ici_run_tab) self.ici_progress_bar.setMinimum(0) self.ici_progress_bar.setMaximum(100) self.ici_progress_bar.setValue(0) self.ici_progress_bar.setFormat("%p%") self.ici_progress_bar.setTextVisible(True) layout.addWidget(self.ici_progress_bar) self.ici_log_edit = QPlainTextEdit(self.ici_run_tab) self.ici_log_edit.setReadOnly(True) self.ici_log_edit.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap) self.ici_log_edit.setMinimumHeight(500) layout.addWidget(self.ici_log_edit, stretch=1) self.ici_run_root_edit.textChanged.connect(self._ici_update_command_label) self.ici_geom_edit.textChanged.connect(self._ici_update_command_label) self.ici_cell_edit.textChanged.connect(self._ici_update_command_label) self.ici_h5_edit.textChanged.connect(self._ici_update_command_label) self.ici_max_iters_spin.valueChanged.connect(self._ici_update_command_label) self.ici_jobs_spin.valueChanged.connect(self._ici_update_command_label) self._ici_update_command_label() def _on_method_changed(self, index: int): if hasattr(self, "settings_stack"): self.settings_stack.setCurrentIndex(index) if hasattr(self, "run_stack"): self.run_stack.setCurrentIndex(index) if index == 1: self._sync_ici_shared_paths() def _sync_ici_shared_paths(self): if not hasattr(self, "ici_run_root_edit"): return try: if self.ini_path: self.ici_run_root_edit.setText( os.path.join(self._resolve_run_root(self.ini_path), "ici_<timestamp>") ) except Exception: pass self.ici_geom_edit.setText("Generated when the run starts") self.ici_cell_edit.setText("Generated when the run starts") if hasattr(self, "ici_h5_edit"): h5_paths = self._ici_resolve_h5_paths() self.ici_h5_edit.setPlainText("\n".join(h5_paths)) self._ici_update_command_label() def _ici_resolve_h5_paths(self): ini_path = getattr(self, "ini_path", None) inferred = self._find_h5_path_from_ini(ini_path) if inferred and os.path.exists(inferred): return [os.path.abspath(inferred)] if ini_path and os.path.exists(ini_path): try: from configparser import ConfigParser cfg = ConfigParser() cfg.read(ini_path) if cfg.has_section("Paths"): h5_rel = cfg.get("Paths", "h5file", fallback="").strip() out_rel = cfg.get("Paths", "outputfolder", fallback="").strip() ini_dir = os.path.dirname(os.path.abspath(ini_path)) candidates = [] if h5_rel: candidates.append(h5_rel) candidates.append(os.path.join(ini_dir, h5_rel)) if out_rel: candidates.append(os.path.join(ini_dir, out_rel, h5_rel)) for candidate in candidates: if candidate and os.path.exists(candidate): return [os.path.abspath(candidate)] except Exception: pass h5 = getattr(self, "h5_path", None) if h5 and os.path.exists(h5): return [os.path.abspath(h5)] try: paths = self._resolve_h5_for_current_run() if paths: return paths except Exception: pass return [] def _ici_normalize_path(self, path: str) -> str: return os.path.abspath(os.path.expanduser(path)) def _ici_browse_run_root(self): path = QFileDialog.getExistingDirectory( self, "Select run root directory", self.ici_run_root_edit.text() or ".", ) if path: self.ici_run_root_edit.setText(path) def _ici_browse_geom(self): path, _ = QFileDialog.getOpenFileName( self, "Select .geom file", self.ici_geom_edit.text() or ".", "Geom files (*.geom);;All files (*)", ) if path: self.ici_geom_edit.setText(path) def _ici_browse_cell(self): path, _ = QFileDialog.getOpenFileName( self, "Select .cell file", self.ici_cell_edit.text() or ".", "Cell files (*.cell);;All files (*)", ) if path: self.ici_cell_edit.setText(path) def _ici_add_h5_files(self): paths, _ = QFileDialog.getOpenFileNames( self, "Select HDF5 files", ".", "HDF5 files (*.h5 *.hdf5);;All files (*)", ) if not paths: return current = self.ici_h5_edit.toPlainText().strip() lines = [line for line in current.splitlines() if line.strip()] lines.extend(paths) self.ici_h5_edit.setPlainText("\n".join(lines)) def _ici_clear_h5_files(self): self.ici_h5_edit.clear() def _ici_append_text(self, text: str): self.ici_log_edit.moveCursor(QTextCursor.MoveOperation.End) self.ici_log_edit.insertPlainText(text) self.ici_log_edit.moveCursor(QTextCursor.MoveOperation.End) def _ici_build_command_preview(self) -> str: if not hasattr(self, "ici_run_root_edit"): return "" run_root = self.ici_run_root_edit.text().strip() geom = self.ici_geom_edit.text().strip() cell = self.ici_cell_edit.text().strip() raw_h5_lines = [line.strip() for line in self.ici_h5_edit.toPlainText().splitlines() if line.strip()] if not raw_h5_lines: raw_h5_lines = self._ici_resolve_h5_paths() if geom.startswith("Generated when "): geom = "" if cell.startswith("Generated when "): cell = "" args = ["python3", "-m", "coseda.ici.orchestrator"] if run_root: args.extend(["--run-root", run_root]) if geom: args.extend(["--geom", geom]) if cell: args.extend(["--cell", cell]) if raw_h5_lines: args.append("--h5") args.extend(raw_h5_lines) args.extend(["--max-iters", str(self.ici_max_iters_spin.value())]) args.extend(["--jobs", str(self.ici_jobs_spin.value())]) args.extend(["--radius-mm", str(self.ici_radius_spin.value())]) args.extend(["--min-spacing-mm", str(self.ici_min_spacing_spin.value())]) args.extend(["--N-conv", str(self.ici_N_conv_spin.value())]) args.extend(["--recurring-tol", str(self.ici_recurring_tol_spin.value())]) args.extend(["--median-rel-tol", str(self.ici_median_rel_tol_spin.value())]) args.extend(["--noimprove-N", str(self.ici_noimprove_N_spin.value())]) args.extend(["--noimprove-eps", str(self.ici_noimprove_eps_spin.value())]) args.extend(["--stability-N", str(self.ici_stability_N_spin.value())]) args.extend(["--stability-std", str(self.ici_stability_std_spin.value())]) args.extend(["--done-on-streak-successes", str(self.ici_done_streak_succ_spin.value())]) args.extend(["--done-on-streak-length", str(self.ici_done_streak_len_spin.value())]) args.extend(["--damping-factor", str(self.ici_lambda_spin.value())]) args.append("--no-session-subdir") flag_list = self._ici_collect_flags(require_valid=False) if flag_list: args.append("--flags") args.extend(flag_list) return " ".join(shlex.quote(part) for part in args) def _ici_update_command_label(self): if hasattr(self, "ici_command_edit"): self.ici_command_edit.setText(self._ici_build_command_preview()) def _ici_load_json_file(self, path: str) -> dict: if not path or not os.path.isfile(path): return {} try: with open(path, "r", encoding="utf-8") as handle: data = json.load(handle) return data if isinstance(data, dict) else {} except Exception: return {} def _ici_apply_existing_run(self, run_root: str): if not run_root or not os.path.isdir(run_root): return if not os.path.basename(os.path.abspath(run_root)).startswith("ici_"): return manifest = self._ici_load_json_file(os.path.join(run_root, "ici_run_manifest.json")) settings = self._ici_load_json_file(os.path.join(run_root, "ici_settings.json")) inputs = manifest.get("inputs", {}) if isinstance(manifest.get("inputs"), dict) else {} files = manifest.get("files", {}) if isinstance(manifest.get("files"), dict) else {} params = manifest.get("settings", {}).get("params", {}) if isinstance(manifest.get("settings"), dict) else {} self.ici_run_root_edit.setText(run_root) geom = settings.get("geom_file") or inputs.get("geometry") or files.get("geometry") or os.path.join(run_root, "geometry.geom") cell = settings.get("cell_file") or inputs.get("cell") or files.get("cell") or os.path.join(run_root, "cellfile.cell") self.ici_geom_edit.setText(str(geom or "")) self.ici_cell_edit.setText(str(cell or "")) h5_sources = settings.get("h5_files") or inputs.get("h5_sources") or [] if isinstance(h5_sources, str): h5_sources = [h5_sources] if h5_sources: self.ici_h5_edit.setPlainText("\n".join(str(path) for path in h5_sources)) def _set_int_from(mapping, key, widget): if key in mapping: try: widget.setValue(int(mapping[key])) except Exception: pass def _set_float_from(mapping, key, widget): if key in mapping: try: widget.setValue(float(mapping[key])) except Exception: pass def _set_text_from(mapping, key, widget): if key in mapping: try: widget.setText(str(mapping[key])) except Exception: pass def _set_bool_from(mapping, key, widget): if key in mapping: try: value = mapping[key] if isinstance(value, str): value = value.lower() in {"1", "yes", "true", "on"} widget.setChecked(bool(value)) except Exception: pass def _set_combo_from(mapping, key, widget): if key in mapping: self._ici_set_combo_text(widget, str(mapping[key])) _set_int_from(settings, "max_iters", self.ici_max_iters_spin) _set_int_from(settings, "jobs", self.ici_jobs_spin) _set_float_from(params, "radius_mm", self.ici_radius_spin) _set_float_from(params, "min_spacing_mm", self.ici_min_spacing_spin) _set_int_from(params, "N_conv", self.ici_N_conv_spin) _set_float_from(params, "recurring_tol", self.ici_recurring_tol_spin) _set_float_from(params, "median_rel_tol", self.ici_median_rel_tol_spin) _set_int_from(params, "noimprove_N", self.ici_noimprove_N_spin) _set_float_from(params, "noimprove_eps", self.ici_noimprove_eps_spin) _set_int_from(params, "stability_N", self.ici_stability_N_spin) _set_float_from(params, "stability_std", self.ici_stability_std_spin) _set_int_from(params, "done_on_streak_successes", self.ici_done_streak_succ_spin) _set_int_from(params, "done_on_streak_length", self.ici_done_streak_len_spin) _set_float_from(params, "damping_factor", self.ici_lambda_spin) _set_combo_from(settings, "peaks", self.ici_peaks_combo) _set_combo_from(settings, "indexing", self.ici_indexing_combo) _set_combo_from(settings, "integration", self.ici_integration_combo) _set_text_from(settings, "push_res", self.ici_push_res_edit) _set_int_from(settings, "min_peaks", self.ici_min_peaks_spin) _set_int_from(settings, "max_indexer_threads", self.ici_max_indexer_threads_spin) _set_text_from(settings, "tolerance", self.ici_tolerance_edit) _set_text_from(settings, "int_radius", self.ici_int_radius_edit) _set_float_from(settings, "xgandalf_tolerance", self.ici_xgandalf_tolerance_spin) _set_int_from(settings, "xgandalf_sampling_pitch", self.ici_sampling_pitch_spin) _set_float_from(settings, "xgandalf_min_lattice_vector_length", self.ici_min_lat_vec_len_spin) _set_float_from(settings, "xgandalf_max_lattice_vector_length", self.ici_max_lat_vec_len_spin) _set_int_from(settings, "xgandalf_grad_desc_iterations", self.ici_grad_desc_iterations_spin) _set_bool_from(settings, "no_retry", self.ici_no_retry_cb) _set_bool_from(settings, "no_half_pixel_shift", self.ici_no_half_pixel_shift_cb) _set_bool_from(settings, "no_non_hits_in_stream", self.ici_no_non_hits_cb) _set_bool_from(settings, "no_revalidate", self.ici_no_revalidate_cb) _set_bool_from(settings, "no_refine", self.ici_no_refine_cb) self.ici_no_image_data_cb.setChecked(False) self.ici_flags_edit.setPlainText(str(settings.get("extra_flags") or "")) self._ici_update_command_label() def _ici_can_resume_run(self, run_root: str) -> bool: if not run_root or not os.path.isdir(run_root): return False if not os.path.isdir(os.path.join(run_root, "run_000")): return False manifest = self._ici_load_json_file(os.path.join(run_root, "ici_run_manifest.json")) status = str(manifest.get("status", "")).lower() if status == "converged": return False if os.path.exists(os.path.join(run_root, "done.stream")): return False return True def _load_ici_defaults_from_ini(self): if not self.ini_path or not os.path.exists(self.ini_path): return if not hasattr(self, "ici_max_iters_spin"): return from configparser import ConfigParser parser = ConfigParser() try: parser.read(self.ini_path) except Exception: return if not parser.has_section("Parameters"): return def _set_int(key, widget): if parser.has_option("Parameters", key): try: widget.setValue(parser.getint("Parameters", key)) except Exception: pass def _set_float(key, widget): if parser.has_option("Parameters", key): try: widget.setValue(parser.getfloat("Parameters", key)) except Exception: pass def _set_text(key, widget): if parser.has_option("Parameters", key): try: widget.setText(parser.get("Parameters", key).strip()) except Exception: pass def _set_bool(key, widget): if parser.has_option("Parameters", key): try: widget.setChecked(parser.getboolean("Parameters", key)) except Exception: pass def _set_combo(key, widget): if parser.has_option("Parameters", key): self._ici_set_combo_text(widget, parser.get("Parameters", key).strip()) _set_int("ici_max_iters", self.ici_max_iters_spin) _set_int("ici_jobs", self.ici_jobs_spin) _set_float("ici_radius_mm", self.ici_radius_spin) _set_float("ici_min_spacing_mm", self.ici_min_spacing_spin) _set_int("ici_n_conv", self.ici_N_conv_spin) _set_float("ici_recurring_tol", self.ici_recurring_tol_spin) _set_float("ici_median_rel_tol", self.ici_median_rel_tol_spin) _set_int("ici_noimprove_n", self.ici_noimprove_N_spin) _set_float("ici_noimprove_eps", self.ici_noimprove_eps_spin) _set_int("ici_stability_n", self.ici_stability_N_spin) _set_float("ici_stability_std", self.ici_stability_std_spin) _set_int("ici_done_on_streak_successes", self.ici_done_streak_succ_spin) _set_int("ici_done_on_streak_length", self.ici_done_streak_len_spin) _set_float("ici_damping_factor", self.ici_lambda_spin) _set_combo("ici_peaks", self.ici_peaks_combo) _set_combo("ici_indexing", self.ici_indexing_combo) _set_combo("ici_integration", self.ici_integration_combo) _set_text("ici_push_res", self.ici_push_res_edit) _set_int("ici_min_peaks", self.ici_min_peaks_spin) _set_int("ici_max_indexer_threads", self.ici_max_indexer_threads_spin) _set_text("ici_tolerance", self.ici_tolerance_edit) _set_text("ici_int_radius", self.ici_int_radius_edit) _set_float("ici_xgandalf_tolerance", self.ici_xgandalf_tolerance_spin) _set_int("ici_xgandalf_sampling_pitch", self.ici_sampling_pitch_spin) _set_float("ici_xgandalf_min_lattice_vector_length", self.ici_min_lat_vec_len_spin) _set_float("ici_xgandalf_max_lattice_vector_length", self.ici_max_lat_vec_len_spin) _set_int("ici_xgandalf_grad_desc_iterations", self.ici_grad_desc_iterations_spin) _set_bool("ici_no_retry", self.ici_no_retry_cb) _set_bool("ici_no_half_pixel_shift", self.ici_no_half_pixel_shift_cb) _set_bool("ici_no_non_hits_in_stream", self.ici_no_non_hits_cb) _set_bool("ici_no_revalidate", self.ici_no_revalidate_cb) _set_bool("ici_no_refine", self.ici_no_refine_cb) self.ici_no_image_data_cb.setChecked(False) if parser.has_option("Parameters", "ici_extra_flags"): self.ici_flags_edit.setPlainText(parser.get("Parameters", "ici_extra_flags").strip()) else: self.ici_flags_edit.clear() self._ici_update_command_label() def _ici_flag_checkbox_pairs(self): return [ ("--no-retry", self.ici_no_retry_cb), ("--no-half-pixel-shift", self.ici_no_half_pixel_shift_cb), ("--no-non-hits-in-stream", self.ici_no_non_hits_cb), ("--no-revalidate", self.ici_no_revalidate_cb), ("--no-refine", self.ici_no_refine_cb), ("--no-image-data", self.ici_no_image_data_cb), ] def _ici_value_flag_pairs(self): def _float_text(widget): return format(widget.value(), "g") return [ ("--peaks", lambda: self.ici_peaks_combo.currentText().strip()), ("--indexing", lambda: self.ici_indexing_combo.currentText().strip()), ("--integration", lambda: self.ici_integration_combo.currentText().strip()), ("--push-res", lambda: self.ici_push_res_edit.text().strip()), ("--min-peaks", lambda: str(self.ici_min_peaks_spin.value())), ("--max-indexer-threads", lambda: str(self.ici_max_indexer_threads_spin.value())), ("--tolerance", lambda: self.ici_tolerance_edit.text().strip()), ("--int-radius", lambda: self.ici_int_radius_edit.text().strip()), ("--xgandalf-tolerance", lambda: _float_text(self.ici_xgandalf_tolerance_spin)), ("--xgandalf-sampling-pitch", lambda: str(self.ici_sampling_pitch_spin.value())), ("--xgandalf-min-lattice-vector-length", lambda: _float_text(self.ici_min_lat_vec_len_spin)), ("--xgandalf-max-lattice-vector-length", lambda: _float_text(self.ici_max_lat_vec_len_spin)), ("--xgandalf-grad-desc-iterations", lambda: str(self.ici_grad_desc_iterations_spin.value())), ] def _ici_managed_value_flag_names(self): return [name for name, _ in self._ici_value_flag_pairs()] def _ici_parse_flag_value(self, flags, name, default=None): for idx, flag in enumerate(flags): if flag == name and idx + 1 < len(flags): return flags[idx + 1] prefix = name + "=" if flag.startswith(prefix): return flag[len(prefix):] return default def _ici_set_combo_text(self, combo, text): if not text: return idx = combo.findText(text) if idx < 0: combo.addItem(text) idx = combo.findText(text) combo.setCurrentIndex(idx) def _ici_apply_flags_to_indexamajig_widgets(self, flags): self._ici_set_combo_text(self.ici_peaks_combo, self._ici_parse_flag_value(flags, "--peaks", self.ici_peaks_combo.currentText())) self._ici_set_combo_text(self.ici_indexing_combo, self._ici_parse_flag_value(flags, "--indexing", self.ici_indexing_combo.currentText())) self._ici_set_combo_text(self.ici_integration_combo, self._ici_parse_flag_value(flags, "--integration", self.ici_integration_combo.currentText())) def _set_line(widget, name): value = self._ici_parse_flag_value(flags, name) if value is not None: widget.setText(str(value)) def _set_int(widget, name): value = self._ici_parse_flag_value(flags, name) if value is not None: try: widget.setValue(int(float(value))) except Exception: pass def _set_float(widget, name): value = self._ici_parse_flag_value(flags, name) if value is not None: try: widget.setValue(float(value)) except Exception: pass _set_line(self.ici_push_res_edit, "--push-res") _set_int(self.ici_min_peaks_spin, "--min-peaks") _set_line(self.ici_tolerance_edit, "--tolerance") _set_line(self.ici_int_radius_edit, "--int-radius") _set_float(self.ici_xgandalf_tolerance_spin, "--xgandalf-tolerance") _set_int(self.ici_sampling_pitch_spin, "--xgandalf-sampling-pitch") _set_float(self.ici_min_lat_vec_len_spin, "--xgandalf-min-lattice-vector-length") _set_float(self.ici_max_lat_vec_len_spin, "--xgandalf-max-lattice-vector-length") _set_int(self.ici_grad_desc_iterations_spin, "--xgandalf-grad-desc-iterations") def _ici_filter_managed_flags(self, flags): managed_values = set(self._ici_managed_value_flag_names()) managed_bare = {flag for flag, _ in self._ici_flag_checkbox_pairs()} filtered = [] idx = 0 while idx < len(flags): flag = flags[idx] name = flag.split("=", 1)[0] if flag.startswith("--") else flag if name in managed_values: if flag == name and idx + 1 < len(flags): idx += 2 else: idx += 1 continue if flag in managed_bare: idx += 1 continue filtered.append(flag) idx += 1 return filtered def _ici_structured_indexamajig_flags(self): flags = [] for name, getter in self._ici_value_flag_pairs(): value = getter() if value: flags.append(f"{name}={value}") for flag, cb in self._ici_flag_checkbox_pairs(): if flag == "--no-image-data": continue if cb.isChecked(): flags.append(flag) return flags def _ici_flags_with_structured_settings(self, flags): merged = self._ici_filter_managed_flags(flags) merged.extend(self._ici_structured_indexamajig_flags()) seen = set() deduped = [] for flag in merged: if flag in seen: continue seen.add(flag) deduped.append(flag) return deduped def _ici_set_flags_text_from_list(self, flags): self._ici_syncing_flags = True try: self.ici_flags_edit.setPlainText(" ".join(flags)) finally: self._ici_syncing_flags = False self._ici_update_command_label() def _ici_on_indexamajig_setting_changed(self): self._ici_update_command_label() def _ici_on_flags_text_changed(self): self._ici_update_command_label() def _ici_on_flag_checkbox_changed(self): self._ici_update_command_label() def _ici_collect_flags(self, require_valid: bool): flags_text = self.ici_flags_edit.toPlainText().strip() if flags_text: try: flag_list = shlex.split(flags_text) except ValueError as e: if require_valid: QMessageBox.critical( self, "Invalid flags", f"Could not parse flags text:\n{e}", ) return None flag_list = [flags_text] else: flag_list = [] return self._ici_flags_with_structured_settings(flag_list) def _ici_build_argv(self): run_root = self._ici_normalize_path(self.ici_run_root_edit.text().strip()) geom = self._ici_normalize_path(self.ici_geom_edit.text().strip()) cell = self._ici_normalize_path(self.ici_cell_edit.text().strip()) if not run_root: QMessageBox.critical(self, "Missing run root", "Please specify a run root directory.") return None if not geom: QMessageBox.critical(self, "Missing geom", "Please specify a .geom file.") return None if not cell: QMessageBox.critical(self, "Missing cell", "Please specify a .cell file.") return None raw_h5_lines = [line.strip() for line in self.ici_h5_edit.toPlainText().splitlines() if line.strip()] if not raw_h5_lines: raw_h5_lines = self._ici_resolve_h5_paths() h5_lines = [self._ici_normalize_path(p) for p in raw_h5_lines] if not h5_lines: QMessageBox.critical( self, "Missing HDF5 input", "Could not resolve an HDF5 file from the selected INI/workspace.", ) return None max_iters = self.ici_max_iters_spin.value() jobs = self.ici_jobs_spin.value() radius_mm = self.ici_radius_spin.value() min_spacing_mm = self.ici_min_spacing_spin.value() N_conv = self.ici_N_conv_spin.value() recurring_tol = self.ici_recurring_tol_spin.value() median_rel_tol = self.ici_median_rel_tol_spin.value() noimprove_N = self.ici_noimprove_N_spin.value() noimprove_eps = self.ici_noimprove_eps_spin.value() stability_N = self.ici_stability_N_spin.value() stability_std = self.ici_stability_std_spin.value() done_streak_succ = self.ici_done_streak_succ_spin.value() done_streak_len = self.ici_done_streak_len_spin.value() lambda_val = self.ici_lambda_spin.value() flag_list = self._ici_collect_flags(require_valid=True) if flag_list is None: return None orch.DEFAULT_FLAGS = flag_list argv = [] argv.extend(["--run-root", run_root]) argv.extend(["--geom", geom]) argv.extend(["--cell", cell]) argv.append("--h5") argv.extend(h5_lines) argv.extend(["--max-iters", str(max_iters)]) argv.extend(["--jobs", str(jobs)]) argv.extend(["--radius-mm", str(radius_mm)]) argv.extend(["--min-spacing-mm", str(min_spacing_mm)]) argv.extend(["--N-conv", str(N_conv)]) argv.extend(["--recurring-tol", str(recurring_tol)]) argv.extend(["--median-rel-tol", str(median_rel_tol)]) argv.extend(["--noimprove-N", str(noimprove_N)]) argv.extend(["--noimprove-eps", str(noimprove_eps)]) argv.extend(["--stability-N", str(stability_N)]) argv.extend(["--stability-std", str(stability_std)]) argv.extend(["--done-on-streak-successes", str(done_streak_succ)]) argv.extend(["--done-on-streak-length", str(done_streak_len)]) argv.extend(["--damping-factor", str(lambda_val)]) argv.append("--no-session-subdir") return argv def _ici_snapshot_settings(self): run_root = self.ici_run_root_edit.text().strip() geom = self.ici_geom_edit.text().strip() cell = self.ici_cell_edit.text().strip() raw_h5_lines = [line.strip() for line in self.ici_h5_edit.toPlainText().splitlines() if line.strip()] if not raw_h5_lines: raw_h5_lines = self._ici_resolve_h5_paths() flags = self._ici_collect_flags(require_valid=False) or [] return { "run_root": run_root, "geom_file": geom, "cell_file": cell, "h5_files": raw_h5_lines, "max_iters": self.ici_max_iters_spin.value(), "jobs": self.ici_jobs_spin.value(), "radius_mm": self.ici_radius_spin.value(), "min_spacing_mm": self.ici_min_spacing_spin.value(), "n_conv": self.ici_N_conv_spin.value(), "recurring_tol": self.ici_recurring_tol_spin.value(), "median_rel_tol": self.ici_median_rel_tol_spin.value(), "noimprove_n": self.ici_noimprove_N_spin.value(), "noimprove_eps": self.ici_noimprove_eps_spin.value(), "stability_n": self.ici_stability_N_spin.value(), "stability_std": self.ici_stability_std_spin.value(), "done_on_streak_successes": self.ici_done_streak_succ_spin.value(), "done_on_streak_length": self.ici_done_streak_len_spin.value(), "damping_factor": self.ici_lambda_spin.value(), "peaks": self.ici_peaks_combo.currentText().strip(), "indexing": self.ici_indexing_combo.currentText().strip(), "integration": self.ici_integration_combo.currentText().strip(), "push_res": self.ici_push_res_edit.text().strip(), "min_peaks": self.ici_min_peaks_spin.value(), "max_indexer_threads": self.ici_max_indexer_threads_spin.value(), "tolerance": self.ici_tolerance_edit.text().strip(), "int_radius": self.ici_int_radius_edit.text().strip(), "xgandalf_tolerance": self.ici_xgandalf_tolerance_spin.value(), "xgandalf_sampling_pitch": self.ici_sampling_pitch_spin.value(), "xgandalf_min_lattice_vector_length": self.ici_min_lat_vec_len_spin.value(), "xgandalf_max_lattice_vector_length": self.ici_max_lat_vec_len_spin.value(), "xgandalf_grad_desc_iterations": self.ici_grad_desc_iterations_spin.value(), "no_retry": self.ici_no_retry_cb.isChecked(), "no_half_pixel_shift": self.ici_no_half_pixel_shift_cb.isChecked(), "no_non_hits_in_stream": self.ici_no_non_hits_cb.isChecked(), "no_revalidate": self.ici_no_revalidate_cb.isChecked(), "no_refine": self.ici_no_refine_cb.isChecked(), "no_image_data": False, "extra_flags": self.ici_flags_edit.toPlainText().strip(), "flags": flags, "command": self._ici_build_command_preview(), } def _ici_write_settings_json(self, run_root: str, argv=None): if not run_root: return try: import json import datetime settings = self._ici_snapshot_settings() settings["timestamp"] = datetime.datetime.now().isoformat(timespec="seconds") if argv is not None: settings["argv"] = list(argv) path = os.path.join(run_root, "ici_settings.json") with open(path, "w", encoding="utf-8") as handle: json.dump(settings, handle, indent=2) except Exception as exc: log_print(f"Warning: failed to write ICI settings JSON: {exc}") def _ici_settings_params(self): return { "max_iters": self.ici_max_iters_spin.value(), "jobs": self.ici_jobs_spin.value(), "radius_mm": self.ici_radius_spin.value(), "min_spacing_mm": self.ici_min_spacing_spin.value(), "n_conv": self.ici_N_conv_spin.value(), "recurring_tol": self.ici_recurring_tol_spin.value(), "median_rel_tol": self.ici_median_rel_tol_spin.value(), "noimprove_n": self.ici_noimprove_N_spin.value(), "noimprove_eps": self.ici_noimprove_eps_spin.value(), "stability_n": self.ici_stability_N_spin.value(), "stability_std": self.ici_stability_std_spin.value(), "done_on_streak_successes": self.ici_done_streak_succ_spin.value(), "done_on_streak_length": self.ici_done_streak_len_spin.value(), "damping_factor": self.ici_lambda_spin.value(), "peaks": self.ici_peaks_combo.currentText().strip(), "indexing": self.ici_indexing_combo.currentText().strip(), "integration": self.ici_integration_combo.currentText().strip(), "push_res": self.ici_push_res_edit.text().strip(), "min_peaks": self.ici_min_peaks_spin.value(), "max_indexer_threads": self.ici_max_indexer_threads_spin.value(), "tolerance": self.ici_tolerance_edit.text().strip(), "int_radius": self.ici_int_radius_edit.text().strip(), "xgandalf_tolerance": self.ici_xgandalf_tolerance_spin.value(), "xgandalf_sampling_pitch": self.ici_sampling_pitch_spin.value(), "xgandalf_min_lattice_vector_length": self.ici_min_lat_vec_len_spin.value(), "xgandalf_max_lattice_vector_length": self.ici_max_lat_vec_len_spin.value(), "xgandalf_grad_desc_iterations": self.ici_grad_desc_iterations_spin.value(), "no_retry": self.ici_no_retry_cb.isChecked(), "no_half_pixel_shift": self.ici_no_half_pixel_shift_cb.isChecked(), "no_non_hits_in_stream": self.ici_no_non_hits_cb.isChecked(), "no_revalidate": self.ici_no_revalidate_cb.isChecked(), "no_refine": self.ici_no_refine_cb.isChecked(), "no_image_data": False, "extra_flags": self.ici_flags_edit.toPlainText().strip(), } def _ici_write_current_settings_to_ini(self, ini_path: str): write_icisettings(ini_path, **self._ici_settings_params()) def _ici_restore_batch_original_context(self): original = getattr(self, "_ici_batch_original_ini", None) if not original or not os.path.exists(original): return self.ini_path = original self.h5_path = self._find_h5_path_from_ini(original) or self.h5_path self._load_ici_defaults_from_ini() self._sync_ici_shared_paths() def _ici_on_run_clicked(self): if self._ici_running: QMessageBox.warning( self, "Orchestrator running", "An orchestration run is already in progress.", ) return run_root = self._ici_normalize_path(self.ici_run_root_edit.text().strip()) if self._ici_can_resume_run(run_root): self._ici_apply_existing_run(run_root) self._ici_append_text(f"[GUI] Resuming ICI orchestration in: {run_root}\n") elif not self._ici_create_fresh_run_for_current_ini(): return argv = self._ici_build_argv() if argv is None: return try: self._ici_write_current_settings_to_ini(self.ini_path) except Exception as exc: self._ici_append_text(f"[GUI] Warning: failed to write ICI settings to INI: {exc}\n") self._ici_write_settings_json(self.ici_run_root_edit.text().strip(), argv) self.ici_log_edit.clear() self._ici_append_text(f"[GUI] Starting orchestration with output in: {argv[1]}\n") self._ici_running = True self._ici_stop_requested = False self.ici_run_button.setEnabled(False) if hasattr(self, "ici_run_all_button"): self.ici_run_all_button.setEnabled(False) self.ici_stop_button.setEnabled(True) self._ici_thread = QThread(self) self._ici_worker = OrchestratorWorker(argv, should_stop=lambda: self._ici_stop_requested) self._ici_worker.moveToThread(self._ici_thread) self._ici_thread.started.connect(self._ici_worker.run) self._ici_worker.text_ready.connect(self._ici_append_text) self._ici_worker.finished.connect(self._ici_on_worker_finished) self._ici_worker.finished.connect(self._ici_thread.quit) self._ici_thread.finished.connect(self._ici_thread.deleteLater) self._ici_worker.progress_init.connect(self._ici_on_progress_init) self._ici_worker.progress_step.connect(self._ici_on_progress_step) self._ici_worker.progress_done.connect(self._ici_on_progress_done) self._ici_thread.start() def _ici_on_run_all_clicked(self): if self._ici_running: QMessageBox.warning( self, "Orchestrator running", "An orchestration run is already in progress.", ) return ini_paths = self._get_workspace_ini_paths() if not ini_paths: QMessageBox.warning(self, "Run All", "No INI files found in the workspace.") return self._ici_batch_original_ini = self.ini_path self._ici_batch_queue = list(ini_paths) self._ici_batch_total = len(self._ici_batch_queue) self._ici_batch_done = 0 self._ici_batch_mode = True from datetime import datetime as _dt self._ici_batch_timestamp = _dt.now().strftime("%Y%m%d_%H%M%S") self._ici_stop_requested = False self.ici_log_edit.clear() self._ici_append_text( f"[GUI] Starting ICI batch for {self._ici_batch_total} file(s) " f"with run name ici_{self._ici_batch_timestamp}.\n" ) settings_errors = 0 for ini in ini_paths: try: self._ici_write_current_settings_to_ini(ini) except Exception as exc: settings_errors += 1 self._ici_append_text(f"[GUI] Warning: failed to write ICI settings to {ini}: {exc}\n") if settings_errors == len(ini_paths): self._ici_batch_mode = False self._ici_batch_timestamp = None QMessageBox.critical(self, "Run All", "Failed to write ICI settings to every INI.") return self.ici_run_button.setEnabled(False) self.ici_run_all_button.setEnabled(False) self.ici_stop_button.setEnabled(True) self._ici_start_next_batch_run() def _ici_start_next_batch_run(self): if not getattr(self, "_ici_batch_mode", False): return if self._ici_stop_requested: remaining = len(getattr(self, "_ici_batch_queue", [])) self._ici_batch_queue = [] self._ici_batch_mode = False self._ici_batch_timestamp = None self._ici_running = False self._ici_restore_batch_original_context() self.ici_run_button.setEnabled(True) self.ici_run_all_button.setEnabled(True) self.ici_stop_button.setEnabled(False) self._ici_append_text(f"[GUI] ICI batch stopped. {remaining} run(s) skipped.\n") return if not self._ici_batch_queue: self._ici_batch_mode = False self._ici_batch_timestamp = None self._ici_running = False self.ici_run_button.setEnabled(True) self.ici_run_all_button.setEnabled(True) self.ici_stop_button.setEnabled(False) self._ici_restore_batch_original_context() self._ici_append_text("[GUI] ICI batch finished.\n") self._refresh_runs_tree() return ini_path = self._ici_batch_queue.pop(0) self._ici_batch_done += 1 self.ini_path = ini_path self.h5_path = self._find_h5_path_from_ini(ini_path) or None self._load_ici_defaults_from_ini() self._sync_ici_shared_paths() label = os.path.basename(ini_path) self._ici_append_text( f"\n[GUI] ICI batch {self._ici_batch_done}/{self._ici_batch_total}: {label}\n" ) if not self._ici_create_fresh_run_for_current_ini( timestamp=getattr(self, "_ici_batch_timestamp", None), show_errors=False, ): self._ici_append_text(f"[GUI] Skipping {label}: failed to prepare run.\n") self._ici_start_next_batch_run() return argv = self._ici_build_argv() if argv is None: self._ici_append_text(f"[GUI] Skipping {label}: failed to build command.\n") self._ici_start_next_batch_run() return self._ici_write_settings_json(self.ici_run_root_edit.text().strip(), argv) self._ici_append_text(f"[GUI] Starting orchestration with output in: {argv[1]}\n") self._ici_running = True self._ici_thread = QThread(self) self._ici_worker = OrchestratorWorker(argv, should_stop=lambda: self._ici_stop_requested) self._ici_worker.moveToThread(self._ici_thread) self._ici_thread.started.connect(self._ici_worker.run) self._ici_worker.text_ready.connect(self._ici_append_text) self._ici_worker.finished.connect(self._ici_on_worker_finished) self._ici_worker.finished.connect(self._ici_thread.quit) self._ici_thread.finished.connect(self._ici_thread.deleteLater) self._ici_worker.progress_init.connect(self._ici_on_progress_init) self._ici_worker.progress_step.connect(self._ici_on_progress_step) self._ici_worker.progress_done.connect(self._ici_on_progress_done) self._ici_thread.start() def _ici_on_stop_clicked(self): if not self._ici_running: if getattr(self, "_ici_batch_mode", False): self._ici_stop_requested = True self._ici_start_next_batch_run() return self._ici_stop_requested = True self.ici_stop_button.setEnabled(False) self._ici_append_text("\n[GUI] Stop requested. Will abort after current step.\n") def _ici_on_worker_finished(self, exit_code: int): self._ici_running = False self.ici_run_button.setEnabled(True) if hasattr(self, "ici_run_all_button"): self.ici_run_all_button.setEnabled(True) self.ici_stop_button.setEnabled(False) self._ici_append_text(f"\n[GUI] Orchestrator finished with exit code {exit_code}.\n") run_root = self.ici_run_root_edit.text().strip() if run_root and os.path.isdir(run_root): self._apply_run_dir(run_root) self._refresh_runs_tree() if getattr(self, "_ici_batch_mode", False): self.ici_run_button.setEnabled(False) self.ici_run_all_button.setEnabled(False) self.ici_stop_button.setEnabled(not self._ici_stop_requested) self._ici_start_next_batch_run() return if exit_code != 0 and not self._ici_stop_requested: QMessageBox.warning( self, "Orchestrator finished with error", f"Orchestrator exited with code {exit_code}. " f"Check the log above and the orchestrator.log file for details.", ) def _ici_format_eta(self, seconds: float) -> str: if seconds is None or seconds <= 0 or seconds == float("inf"): return "estimating..." total = int(seconds) m, s = divmod(total, 60) h, m = divmod(m, 60) if h > 0: return f"{h:d}h {m:02d}m {s:02d}s" return f"{m:02d}m {s:02d}s" def _ici_on_progress_init(self, total: int): self._ici_progress_start_time = time.time() self.ici_progress_bar.setMinimum(0) self.ici_progress_bar.setMaximum(total) self.ici_progress_bar.setValue(0) self.ici_progress_bar.setFormat("%p%") self.ici_progress_label.setText( f"Indexing: 0% (0 / {total} events), ETA estimating..." ) def _ici_on_progress_step(self, step: int): new_val = min(self.ici_progress_bar.value() + step, self.ici_progress_bar.maximum()) self.ici_progress_bar.setValue(new_val) total = self.ici_progress_bar.maximum() done = new_val if total <= 0: return percent = 100.0 * done / total if self._ici_progress_start_time is not None and done > 0: elapsed = time.time() - self._ici_progress_start_time rate = done / elapsed if elapsed > 0 else 0.0 eta = (total - done) / rate if rate > 0 else None eta_str = self._ici_format_eta(eta) else: eta_str = "estimating..." self.ici_progress_label.setText( f"Indexing: {percent:5.1f}% ({done} / {total} events), ETA {eta_str}" ) def _ici_on_progress_done(self): total = self.ici_progress_bar.maximum() if total > 0: self.ici_progress_bar.setValue(total) self.ici_progress_label.setText( f"[run_sh] Indexing finished ({total} / {total} events)" ) else: self.ici_progress_label.setText("Progress (last run_sh.py finished)") self._ici_progress_start_time = None def _ici_write_cell_file(self, path: str, show_errors: bool = True) -> bool: return self._write_cell_file_to_path(path, show_errors=show_errors, label="ICI cell") def _ici_write_geom_file(self, path: str, show_errors: bool = True) -> bool: return self._write_geom_file_to_path(path, show_errors=show_errors, label="ICI geometry") def _ici_create_fresh_run_for_current_ini( self, show_errors: bool = True, timestamp: str | None = None, ): if not self.ini_path or not os.path.exists(self.ini_path): message = "No current INI selected in main window." if show_errors: QMessageBox.warning(self, "Run", message) else: self._ici_append_text(f"[GUI] {message}\n") return None h5_paths = self._ici_resolve_h5_paths() if not h5_paths: message = "Could not resolve an HDF5 file from the selected INI/workspace." if show_errors: QMessageBox.critical(self, "Run", message) else: self._ici_append_text(f"[GUI] {message}\n") return None run_base = self._resolve_run_root(self.ini_path) if timestamp is None: from datetime import datetime as _dt timestamp = _dt.now().strftime("%Y%m%d_%H%M%S") run_root = os.path.join(run_base, f"ici_{timestamp}") try: os.makedirs(run_root, exist_ok=True) except Exception as e: message = f"Failed to create ICI run directory: {e}" if show_errors: QMessageBox.critical(self, "Run", message) else: self._ici_append_text(f"[GUI] {message}\n") return None cell_path = os.path.join(run_root, "cellfile.cell") geom_path = os.path.join(run_root, "geometry.geom") if not self._ici_write_cell_file(cell_path, show_errors=show_errors): return None if not self._ici_write_geom_file(geom_path, show_errors=show_errors): return None self.ici_run_root_edit.setText(run_root) self.ici_cell_edit.setText(cell_path) self.ici_geom_edit.setText(geom_path) self.ici_h5_edit.setPlainText("\n".join(h5_paths)) self._ici_update_command_label() self._refresh_runs_tree() return run_root