added median sweep and fixed multi device issue

This commit is contained in:
Ayzen
2026-05-20 16:52:06 +03:00
parent 0da8b1283c
commit 1c544aa582
29 changed files with 444 additions and 126 deletions
+80 -26
View File
@@ -17,6 +17,7 @@ from PyQt6.QtWidgets import (
QPlainTextEdit,
QPushButton,
QScrollArea,
QSpinBox,
QVBoxLayout,
QWidget,
)
@@ -38,6 +39,7 @@ class PreprocessDialog(QDialog):
selection_changed = pyqtSignal()
radar_config_dir_changed = pyqtSignal()
multi_radar_option_changed = pyqtSignal()
median_sweep_count_changed = pyqtSignal()
start_sequence_requested = pyqtSignal(str)
capture_next_requested = pyqtSignal()
capture_all_requested = pyqtSignal()
@@ -50,9 +52,10 @@ class PreprocessDialog(QDialog):
"""Initialize window metadata and compose dialog UI."""
super().__init__(parent)
self._set_combos: dict[str, QComboBox] = {}
self._preview_plot: pg.PlotWidget | None = None
self._amplitude_plot: pg.PlotWidget | None = None
self._phase_plot: pg.PlotWidget | None = None
self._preview_placeholder: QLabel | None = None
self._preview_host_layout: QVBoxLayout | None = None
self._preview_host_layout: QHBoxLayout | None = None
self._preview_plot_unavailable = False
self._init_window()
self._build_ui()
@@ -102,11 +105,28 @@ class PreprocessDialog(QDialog):
header_row.addWidget(self._kamil_adc_neutral_sets_button)
header_row.addWidget(refresh_button)
layout.addLayout(header_row)
layout.addLayout(self._build_median_sweep_row(group))
layout.addWidget(self._build_radar_config_group(group))
layout.addWidget(self._build_selector_group("S21", VISIBLE_PREPROCESS_ASSET_KEYS, group))
return group
def _build_median_sweep_row(self, parent: QGroupBox) -> QHBoxLayout:
"""Build per-combo median sweep-count selector for capture sessions."""
row = QHBoxLayout()
self._median_sweep_count_input = QSpinBox(parent)
self._median_sweep_count_input.setRange(1, 99)
self._median_sweep_count_input.setValue(5)
self._median_sweep_count_input.setToolTip(
"How many sweeps to capture for each combo. The per-frequency median of all "
"sweeps is saved, suppressing single-sweep outliers (e.g. random phase glitches)."
)
self._median_sweep_count_input.valueChanged.connect(self.median_sweep_count_changed.emit)
row.addWidget(QLabel("Sweeps per combo (median)"))
row.addWidget(self._median_sweep_count_input)
row.addStretch(1)
return row
def _build_radar_config_group(self, parent: QGroupBox) -> QGroupBox:
"""Build radar-config directory controls for multi-radar preprocessing."""
group = QGroupBox("Radar Config Variants", parent)
@@ -221,11 +241,16 @@ class PreprocessDialog(QDialog):
root_layout.addWidget(self._status_label)
def _build_preview_plot(self, root_layout: QVBoxLayout) -> None:
"""Build lazy preview host used after each successful capture."""
"""Build lazy preview host used after each successful capture.
The host holds a side-by-side amplitude (left) and phase (right) plot
pair, created lazily on first capture and replacing a placeholder
label when the PyQtGraph build supports it.
"""
host = QWidget(self)
layout = QVBoxLayout(host)
layout = QHBoxLayout(host)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
layout.setSpacing(6)
placeholder = QLabel("Preview will appear after the first successful capture.", host)
placeholder.setWordWrap(True)
@@ -260,6 +285,15 @@ class PreprocessDialog(QDialog):
"""Update multi-radar capture checkbox state."""
self._use_all_radar_configs_checkbox.setChecked(bool(enabled))
def median_sweep_count(self) -> int:
"""Return configured number of sweeps to median-combine per combo."""
return max(1, int(self._median_sweep_count_input.value()))
def set_median_sweep_count(self, count: int) -> None:
"""Update per-combo median sweep-count spinbox."""
with QSignalBlocker(self._median_sweep_count_input):
self._median_sweep_count_input.setValue(max(1, int(count)))
def set_radar_config_summary(
self,
*,
@@ -385,40 +419,51 @@ class PreprocessDialog(QDialog):
self._kamil_adc_neutral_sets_button.setEnabled(bool(visible))
def reset_preview(self) -> None:
"""Clear preview surface and restore default empty-state text when possible."""
if self._preview_plot is not None:
self._preview_plot.clear()
self._preview_plot.setTitle("")
"""Clear preview surfaces and restore default empty-state text when possible."""
if self._amplitude_plot is not None:
self._amplitude_plot.clear()
if self._phase_plot is not None:
self._phase_plot.clear()
if self._preview_placeholder is not None:
self._preview_placeholder.setText("Preview will appear after the first successful capture.")
def draw_last_trace(self, trace: TraceData, title: str, *, channel: str) -> None:
"""Draw the latest captured sweep trace for the requested channel in dB scale."""
"""Draw amplitude (dB) and wrapped phase (deg, ±180) panes for the latest capture."""
samples = trace.s11 if channel == "s11" else trace.s21
magnitude_db = 20.0 * np.log10(np.maximum(np.abs(samples), 1e-12))
if self._ensure_preview_plot():
assert self._preview_plot is not None
self._preview_plot.clear()
self._preview_plot.plot(
phase_deg = np.degrees(np.angle(samples))
if self._ensure_preview_plots():
assert self._amplitude_plot is not None
assert self._phase_plot is not None
self._amplitude_plot.clear()
self._amplitude_plot.plot(
trace.frequency_hz,
magnitude_db,
pen=pg.mkPen("#4cc9f0", width=1.8),
)
self._phase_plot.clear()
self._phase_plot.plot(
trace.frequency_hz,
phase_deg,
pen=pg.mkPen("#f48c06", width=1.8),
)
elif self._preview_placeholder is not None:
self._preview_placeholder.setText(
f"{title}\n"
f"input={trace.combo.input_pos}, output={trace.combo.output_pos}, "
f"input={trace.combo.input}, output={trace.combo.output}, "
f"points={trace.frequency_hz.size}\n"
f"Preview plot is unavailable on this PyQtGraph/PyQt6 build."
)
combo = trace.combo
self._status_label.setText(
f"{title}: input={combo.input_pos}, output={combo.output_pos}, points={trace.frequency_hz.size}"
f"{title}: input={combo.input}, output={combo.output}, points={trace.frequency_hz.size}"
)
def _ensure_preview_plot(self) -> bool:
"""Create preview plot lazily and keep a text fallback when unavailable."""
if self._preview_plot is not None:
def _ensure_preview_plots(self) -> bool:
"""Create the amplitude+phase plot pair lazily on first successful capture."""
if self._amplitude_plot is not None and self._phase_plot is not None:
return True
if self._preview_plot_unavailable:
return False
@@ -426,11 +471,8 @@ class PreprocessDialog(QDialog):
return False
try:
plot = pg.PlotWidget(background="#101418", enableMenu=False)
plot.showGrid(x=True, y=True, alpha=0.2)
plot.setLabel("bottom", "Frequency", units="Hz")
plot.setLabel("left", "Magnitude", units="dB")
plot.setMinimumHeight(320)
amplitude_plot = self._build_preview_axis("Magnitude", "dB")
phase_plot = self._build_preview_axis("Phase", "deg")
except Exception:
self._preview_plot_unavailable = True
return False
@@ -440,10 +482,22 @@ class PreprocessDialog(QDialog):
self._preview_placeholder.deleteLater()
self._preview_placeholder = None
self._preview_plot = plot
self._preview_host_layout.addWidget(plot)
self._amplitude_plot = amplitude_plot
self._phase_plot = phase_plot
self._preview_host_layout.addWidget(amplitude_plot, stretch=1)
self._preview_host_layout.addWidget(phase_plot, stretch=1)
return True
@staticmethod
def _build_preview_axis(left_label: str, left_units: str) -> "pg.PlotWidget":
"""Return one PlotWidget configured with the project's preview style."""
plot = pg.PlotWidget(background="#101418", enableMenu=False)
plot.showGrid(x=True, y=True, alpha=0.2)
plot.setLabel("bottom", "Frequency", units="Hz")
plot.setLabel("left", left_label, units=left_units)
plot.setMinimumHeight(320)
return plot
def _emit_selection_changed(self) -> None:
"""Emit current selection snapshot change."""
self.selection_changed.emit()