"""Dialog for calibration/reference set selection and sequential capture.""" from __future__ import annotations from PyQt6.QtCore import pyqtSignal from PyQt6.QtWidgets import ( QComboBox, QDialog, QFormLayout, QGridLayout, QGroupBox, QHBoxLayout, QLabel, QLineEdit, QPlainTextEdit, QPushButton, QVBoxLayout, ) import pyqtgraph as pg import numpy as np from python_app.models.dataset_model import TraceData class PreprocessDialog(QDialog): """Standalone dialog for preprocessing capture workflows.""" refresh_requested = pyqtSignal() selection_changed = pyqtSignal(str, str) start_sequence_requested = pyqtSignal(str) capture_next_requested = pyqtSignal() abort_sequence_requested = pyqtSignal() def __init__(self, parent=None) -> None: """Create dialog and build all widgets.""" super().__init__(parent) self.setWindowTitle("Preprocessing Setup") self.resize(1040, 760) self._build_ui() def _build_ui(self) -> None: """Build dialog layout, controls, and preview plot.""" layout = QVBoxLayout(self) sets_group = QGroupBox("Calibration / Reference Sets", self) sets_layout = QGridLayout(sets_group) self._set_name_input = QLineEdit("set_001", sets_group) self._calibration_combo = QComboBox(sets_group) self._reference_combo = QComboBox(sets_group) refresh_button = QPushButton("Refresh Sets", sets_group) refresh_button.clicked.connect(self.refresh_requested.emit) self._calibration_combo.currentTextChanged.connect(self._emit_selection_changed) self._reference_combo.currentTextChanged.connect(self._emit_selection_changed) sets_layout.addWidget(QLabel("Set name"), 0, 0) sets_layout.addWidget(self._set_name_input, 0, 1) sets_layout.addWidget(refresh_button, 0, 2) sets_layout.addWidget(QLabel("Calibration set"), 1, 0) sets_layout.addWidget(self._calibration_combo, 1, 1, 1, 2) sets_layout.addWidget(QLabel("Reference set"), 2, 0) sets_layout.addWidget(self._reference_combo, 2, 1, 1, 2) layout.addWidget(sets_group) sequence_group = QGroupBox("Sequential Capture (Fill Full N*M)", self) sequence_layout = QGridLayout(sequence_group) self._active_kind_label = QLabel("", sequence_group) self._progress_label = QLabel("0 / 0", sequence_group) self._combo_label = QLabel("", sequence_group) self._tx_antenna_label_input = QLineEdit(sequence_group) self._rx_antenna_label_input = QLineEdit(sequence_group) self._tx_antenna_label_input.setPlaceholderText("e.g. TX_A") self._rx_antenna_label_input.setPlaceholderText("e.g. RX_B") start_calibration_button = QPushButton("Start Calibration Sequence", sequence_group) start_calibration_button.clicked.connect(lambda: self.start_sequence_requested.emit("calibration")) start_reference_button = QPushButton("Start Reference Sequence", sequence_group) start_reference_button.clicked.connect(lambda: self.start_sequence_requested.emit("reference")) self._capture_next_button = QPushButton("Capture Current Combo", sequence_group) self._capture_next_button.clicked.connect(self.capture_next_requested.emit) self._capture_next_button.setEnabled(False) self._abort_button = QPushButton("Abort Sequence", sequence_group) self._abort_button.clicked.connect(self.abort_sequence_requested.emit) self._abort_button.setEnabled(False) button_row = QHBoxLayout() button_row.addWidget(start_calibration_button) button_row.addWidget(start_reference_button) button_row.addWidget(self._capture_next_button) button_row.addWidget(self._abort_button) sequence_layout.addWidget(QLabel("Active type"), 0, 0) sequence_layout.addWidget(self._active_kind_label, 0, 1) sequence_layout.addWidget(QLabel("Progress"), 1, 0) sequence_layout.addWidget(self._progress_label, 1, 1) sequence_layout.addWidget(QLabel("Current combo"), 2, 0) sequence_layout.addWidget(self._combo_label, 2, 1) sequence_layout.addWidget(QLabel("TX antenna label"), 3, 0) sequence_layout.addWidget(self._tx_antenna_label_input, 3, 1) sequence_layout.addWidget(QLabel("RX antenna label"), 4, 0) sequence_layout.addWidget(self._rx_antenna_label_input, 4, 1) sequence_layout.addLayout(button_row, 5, 0, 1, 2) self._capture_log = QPlainTextEdit(sequence_group) self._capture_log.setReadOnly(True) self._capture_log.setPlaceholderText("Capture history per combo") sequence_layout.addWidget(self._capture_log, 6, 0, 1, 2) layout.addWidget(sequence_group) self._status = QLabel("Ready", self) layout.addWidget(self._status) self._plot = pg.PlotWidget(background="#101418") self._plot.showGrid(x=True, y=True, alpha=0.2) self._plot.setLabel("bottom", "Frequency", units="Hz") self._plot.setLabel("left", "Magnitude", units="dB") layout.addWidget(self._plot, stretch=1) def set_name(self) -> str: """Return requested target set name.""" return self._set_name_input.text().strip() def calibration_set(self) -> str: """Return currently selected calibration set.""" return self._calibration_combo.currentText().strip() def reference_set(self) -> str: """Return currently selected reference set.""" return self._reference_combo.currentText().strip() def antenna_labels(self) -> tuple[str, str]: """Return optional TX/RX user labels used in capture logs.""" return self._tx_antenna_label_input.text().strip(), self._rx_antenna_label_input.text().strip() def clear_capture_log(self) -> None: """Clear capture history text box.""" self._capture_log.clear() def append_capture_log_entry( self, *, kind: str, captured_count: int, total_count: int, input_pos: int, output_pos: int, tx_label: str, rx_label: str, ) -> None: """Append one capture progress row to dialog log.""" tx_info = tx_label or "-" rx_info = rx_label or "-" self._capture_log.appendPlainText( f"{kind}: {captured_count}/{total_count} | " f"input={input_pos} output={output_pos} | " f"TX={tx_info} RX={rx_info}" ) def set_capture_state( self, *, kind: str | None, captured_count: int, total_count: int, next_input: int | None, next_output: int | None, ) -> None: """Update sequence progress/status widgets.""" if kind is None: self._active_kind_label.setText("") self._progress_label.setText("0 / 0") self._combo_label.setText("") self._capture_next_button.setEnabled(False) self._abort_button.setEnabled(False) return self._active_kind_label.setText(kind) self._progress_label.setText(f"{captured_count} / {total_count}") if next_input is None or next_output is None: self._combo_label.setText("") self._capture_next_button.setEnabled(False) self._abort_button.setEnabled(True) else: self._combo_label.setText(f"input={next_input}, output={next_output}") self._capture_next_button.setEnabled(True) self._abort_button.setEnabled(True) def set_calibration_sets(self, names: list[str]) -> None: """Replace calibration set choices while preserving current selection when possible.""" self._set_combo_items(self._calibration_combo, names, self.calibration_set()) def set_reference_sets(self, names: list[str]) -> None: """Replace reference set choices while preserving current selection when possible.""" self._set_combo_items(self._reference_combo, names, self.reference_set()) def set_selected_sets(self, calibration_set: str, reference_set: str) -> None: """Apply selected set names to both comboboxes and emit selection update.""" if calibration_set: index = self._calibration_combo.findText(calibration_set) if index >= 0: self._calibration_combo.setCurrentIndex(index) if reference_set: index = self._reference_combo.findText(reference_set) if index >= 0: self._reference_combo.setCurrentIndex(index) self._emit_selection_changed() def set_status(self, message: str) -> None: """Set short human-readable status line.""" self._status.setText(message) def draw_last_trace(self, trace: TraceData, title: str) -> None: """Draw the latest captured sweep trace in dB scale.""" magnitude_db = 20.0 * np.log10(np.maximum(np.abs(trace.s21), 1e-12)) self._plot.clear() self._plot.plot( trace.frequency_hz, magnitude_db, pen=pg.mkPen("#4cc9f0", width=1.8), ) combo = trace.combo self._status.setText( f"{title}: input={combo.input_pos}, output={combo.output_pos}, points={trace.frequency_hz.size}" ) def _emit_selection_changed(self) -> None: """Emit current calibration/reference selection.""" self.selection_changed.emit(self.calibration_set(), self.reference_set()) @staticmethod def _set_combo_items(combo: QComboBox, names: list[str], current_text: str) -> None: """Replace combo contents and keep previous value when still available.""" combo.clear() combo.addItems(names) if not current_text: return index = combo.findText(current_text) if index >= 0: combo.setCurrentIndex(index)