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