"""UI for configuring and running CrystFEL indexing/integration from the workspace."""
from coseda.logging_utils import log_print
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton,
QFormLayout, QCheckBox, QSpinBox, QDoubleSpinBox, QFileDialog, QMessageBox, QTabWidget, QComboBox, QTextEdit,
QTreeWidget, QTreeWidgetItem, QStackedWidget, QPlainTextEdit, QGroupBox, QProgressBar
)
from PyQt6.QtGui import QTextCursor
import subprocess
from PyQt6.QtCore import Qt, QTimer, pyqtSignal, QThread, QObject
import os
import signal
import shlex
import sys
import re
import time
import traceback
from coseda.ici import orchestrator as orch
from coseda.initialize import write_icisettings
from coseda.nexus.process import write_nxprocess_indexing
from cosedaUI.indexintegrate_sections import (
CellfileMixin,
GeomfileMixin,
IciMixin,
IndexSettingsMixin,
RunControlMixin,
)
from cosedaUI.indexintegrate_sections.helpers import GuiStream, OrchestratorWorker
[docs]
class IndexingControlWindow(
IndexSettingsMixin,
CellfileMixin,
GeomfileMixin,
IciMixin,
RunControlMixin,
QWidget,
):
"""Tabbed dialog for managing runs, editing params, and launching indexamajig."""
ini_selection_changed = pyqtSignal(str) # absolute INI path selected in this window
[docs]
class ProcessOutputThread(QThread):
"""Stream stdout from a subprocess and emit lines to the UI."""
output_received = pyqtSignal(str)
def __init__(self, process):
super().__init__()
self.process = process
[docs]
def run(self):
"""Read lines until the process finishes and emit each one."""
for line in iter(self.process.stdout.readline, b''):
try:
text = line.decode("utf-8")
except:
text = str(line)
self.output_received.emit(text)
def __init__(self, main_window, ini_directory=None, h5_path=None, parent=None):
"""Wire UI, load latest runs/settings, and start syncing with the main window."""
super().__init__(parent)
self.main_window = main_window
self.ini_directory = ini_directory
# h5_path is passed from main_window; ini_path is retrieved from main_window
self.h5_path = h5_path
self.ini_path = getattr(main_window, 'full_ini_file_path', None)
# Fallback: pick the first INI in the provided directory so the run tree isn't empty
if self.ini_path is None and self.ini_directory and os.path.isdir(self.ini_directory):
for f in sorted(os.listdir(self.ini_directory)):
if f.endswith(".ini") and not f.startswith("."):
self.ini_path = os.path.join(self.ini_directory, f)
break
# Try to locate the workspace file path from the main window
self.workspace_path = (
getattr(main_window, 'workspace_file_path', None)
or getattr(main_window, 'workspace_path', None)
or getattr(main_window, 'workspacefile', None)
)
from configparser import ConfigParser
self.setWindowTitle("Indexing & Integration")
# Read ini file and paths
parser = ConfigParser()
parser.read(self.ini_path)
base_dir = os.path.dirname(self.ini_path)
import glob
run_base = self._resolve_run_root(self.ini_path)
prev_runs = sorted(glob.glob(os.path.join(run_base, "indexingintegration_*")))
# Do not create a run on open; the Run tab creates fresh runs when starting.
self.run_dir = None
self.cell_filepath = None
self.geom_filepath = None
self.list_filepath = None
self.stream_filepath = None
# Initialize tab widget (right side)
self.tabs = QTabWidget()
self.method_combo = QComboBox()
self.method_combo.addItem("Simple Indexamajig")
self.method_combo.addItem("ICI Orchestrator (experimental)")
self.method_combo.setToolTip("Select the indexing method to configure and run.")
self._init_index_integrate_tab()
self._init_cellfile_tab()
self._init_geomfile_tab()
self._init_start_indexing_tab()
self._init_ici_settings_tab()
self._init_ici_run_tab()
# Left panel: workspace tree and run history
left_panel = QWidget()
left_v = QVBoxLayout(left_panel)
left_v.setContentsMargins(0, 0, 8, 0)
left_v.addWidget(QLabel("Workspace Files / Runs"))
self.runs_tree = QTreeWidget()
self.runs_tree.setHeaderLabels(["Name"]) # single column
self.runs_tree.setColumnCount(1)
left_v.addWidget(self.runs_tree, 1)
# Outer layout: left tree + right method selector + tabs
right_panel = QWidget()
right_v = QVBoxLayout(right_panel)
right_v.addWidget(QLabel("Method"))
right_v.addWidget(self.method_combo)
right_v.addWidget(self.tabs, 1)
outer = QHBoxLayout()
outer.addWidget(left_panel, 1)
outer.addWidget(right_panel, 3)
self.setLayout(outer)
# Populate workspace tree
self._refresh_runs_tree()
self._select_tree_for_ini(self.ini_path)
# Connect method selection to tab stacks
self.method_combo.currentIndexChanged.connect(self._on_method_changed)
self._on_method_changed(self.method_combo.currentIndex())
# Connect tree selection change to handler
self.runs_tree.currentItemChanged.connect(self._on_runs_tree_selection_changed)
# Bidirectional selection sync: poll main window for selection changes
self._last_seen_main_ini = self.ini_path
self.sync_timer = QTimer(self)
self.sync_timer.setInterval(700)
self.sync_timer.timeout.connect(self._sync_from_mainwindow_selection)
self.sync_timer.start()
# Load initial data for cell and geom tabs
if getattr(self, 'prev_cell_file', None) and os.path.exists(self.prev_cell_file):
# Prefer the latest existing run's cellfile over workspace defaults
self._load_cell_file()
else:
loaded_from_ws = self._load_cell_from_workspace()
if not loaded_from_ws:
self._load_cell_file()
self._load_geom_file()
self._load_ici_defaults_from_ini()
# On startup, try to load Index & Integrate settings from the most recent run that actually has them
try:
import glob, os as _os
base_dir = os.path.dirname(self.ini_path)
run_base = self._resolve_run_root(self.ini_path)
prev_runs = sorted(
set(glob.glob(os.path.join(run_base, "indexingintegration_*"))) |
set(glob.glob(os.path.join(base_dir, "indexingintegration_*")))
)
latest_settings_run = None
for rd in reversed(prev_runs):
if os.path.exists(os.path.join(rd, 'index_settings.json')):
latest_settings_run = rd
break
if latest_settings_run:
self._load_index_settings_from_run(latest_settings_run)
except Exception:
pass
# Update command label after all tabs/widgets are initialized
self._update_command_label()
# Placeholder for indexing subprocess
self.indexing_process = None
# Session-level cache of last applied/saved Index & Integrate settings
self._last_index_settings = None
# Metrics / progress state
self._total_frames_estimate = None
# Batch run state
self._batch_mode = False
self._batch_queue = [] # list of run_dir paths to process sequentially
self._proc_poll_timer = QTimer(self)
self._proc_poll_timer.setInterval(500)
self._proc_poll_timer.timeout.connect(self._check_process_done)
# ICI orchestration state
self._ici_thread = None
self._ici_worker = None
self._ici_running = False
self._ici_stop_requested = False
self._ici_progress_start_time = None
self._ici_syncing_flags = False
self._ici_batch_mode = False
self._ici_batch_queue = []
self._ici_batch_total = 0
self._ici_batch_done = 0
self._ici_batch_original_ini = None
self._ici_batch_timestamp = None
# --- Index & Integrate Tab ---