added median sweep and fixed multi device issue
This commit is contained in:
@@ -127,6 +127,9 @@ class AppWindow(
|
||||
self._preprocess_set_name = str(self._gui_defaults.preprocess_dialog.set_name)
|
||||
self._preprocess_radar_config_dir = str(self._gui_defaults.preprocess_dialog.radar_config_dir)
|
||||
self._preprocess_use_all_radar_configs = bool(self._gui_defaults.preprocess_dialog.use_all_radar_configs)
|
||||
self._preprocess_median_sweep_count = max(
|
||||
1, int(self._gui_defaults.preprocess_dialog.median_sweep_count)
|
||||
)
|
||||
self._preprocess_radar_variants: list[RadarConfigVariant] = []
|
||||
self._preprocess_radar_scan_summary = RadarConfigScanSummary(
|
||||
directory_path=self._preprocess_radar_config_dir,
|
||||
|
||||
@@ -470,6 +470,7 @@ class AppWindowConfigProfileIOMixin:
|
||||
self._preprocess_set_name = str(gui_state.preprocess_dialog.set_name)
|
||||
self._preprocess_radar_config_dir = str(gui_state.preprocess_dialog.radar_config_dir)
|
||||
self._preprocess_use_all_radar_configs = bool(gui_state.preprocess_dialog.use_all_radar_configs)
|
||||
self._preprocess_median_sweep_count = max(1, int(gui_state.preprocess_dialog.median_sweep_count))
|
||||
self._apply_history_limit_from_config(config)
|
||||
self._gpr_geometry_signature = None
|
||||
self._gpr_selected_geometry = None
|
||||
@@ -487,6 +488,7 @@ class AppWindowConfigProfileIOMixin:
|
||||
self._preprocess_dialog.set_set_name(self._preprocess_set_name)
|
||||
self._preprocess_dialog.set_radar_config_dir(self._preprocess_radar_config_dir)
|
||||
self._preprocess_dialog.set_use_all_radar_configs(self._preprocess_use_all_radar_configs)
|
||||
self._preprocess_dialog.set_median_sweep_count(self._preprocess_median_sweep_count)
|
||||
self._preprocess_dialog.set_selected_sets(self._selected_preprocess_sets, emit_signal=False)
|
||||
|
||||
self._apply_initial_radar_limits()
|
||||
|
||||
@@ -273,6 +273,12 @@ class AppWindowConfigStateBuildersMixin:
|
||||
self._preprocess_use_all_radar_configs = self._preprocess_dialog.use_all_radar_configs()
|
||||
return bool(self._preprocess_use_all_radar_configs)
|
||||
|
||||
def _current_preprocess_median_sweep_count(self) -> int:
|
||||
"""Return current per-combo median sweep count from the dialog."""
|
||||
if self._preprocess_dialog is not None:
|
||||
self._preprocess_median_sweep_count = self._preprocess_dialog.median_sweep_count()
|
||||
return max(1, int(self._preprocess_median_sweep_count))
|
||||
|
||||
def _build_gui_state(self) -> GuiStateModel:
|
||||
"""Build GUI-only persistent state from current widget values."""
|
||||
return GuiStateModel(
|
||||
@@ -367,6 +373,7 @@ class AppWindowConfigStateBuildersMixin:
|
||||
set_name=self._current_preprocess_set_name(),
|
||||
radar_config_dir=self._current_preprocess_radar_config_dir(),
|
||||
use_all_radar_configs=self._current_preprocess_use_all_radar_configs(),
|
||||
median_sweep_count=self._current_preprocess_median_sweep_count(),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ class AppWindowPipelineMixin:
|
||||
+ ", ".join(missing_assets)
|
||||
)
|
||||
|
||||
combo_keys = [ComboKey(input_pos=combo.input, output_pos=combo.output) for combo in config.combos]
|
||||
combo_keys = [ComboKey(input=combo.input, output=combo.output) for combo in config.combos]
|
||||
active_preprocess_keys = runtime_preprocess_asset_keys(config)
|
||||
preprocess_summary = "; ".join(
|
||||
f"{PREPROCESS_ASSET_SPECS[key].display_name}={preprocess_asset_model(config, key).set_name}"
|
||||
|
||||
@@ -95,7 +95,7 @@ def rebuild_bscan_history_from_results(
|
||||
|
||||
for collection in result_tail:
|
||||
for block in collection.blocks:
|
||||
key = (block.combo.input_pos, block.combo.output_pos)
|
||||
key = (block.combo.input, block.combo.output)
|
||||
for payload in block.payloads:
|
||||
if payload.kind != 1 or payload.processing_name != "bscan":
|
||||
continue
|
||||
|
||||
@@ -152,7 +152,7 @@ class AppWindowTracePlotMixin:
|
||||
x_min = np.inf
|
||||
x_max = -np.inf
|
||||
for block in collection.blocks:
|
||||
combo_key = (int(block.combo.input_pos), int(block.combo.output_pos))
|
||||
combo_key = (int(block.combo.input), int(block.combo.output))
|
||||
if combo_filter is not None and combo_key not in combo_filter:
|
||||
continue
|
||||
if combo_key not in combo_colors:
|
||||
@@ -384,7 +384,7 @@ class AppWindowTracePlotMixin:
|
||||
)
|
||||
magnitude_plot.addItem(magnitude_curve)
|
||||
self._trace_magnitude_curves[
|
||||
(int(trace.combo.input_pos), int(trace.combo.output_pos), 0, "__single_trace__")
|
||||
(int(trace.combo.input), int(trace.combo.output), 0, "__single_trace__")
|
||||
] = magnitude_curve
|
||||
|
||||
if show_phase:
|
||||
@@ -396,7 +396,7 @@ class AppWindowTracePlotMixin:
|
||||
)
|
||||
phase_plot.addItem(phase_curve)
|
||||
self._trace_phase_curves[
|
||||
(int(trace.combo.input_pos), int(trace.combo.output_pos), 0, "__single_trace__")
|
||||
(int(trace.combo.input), int(trace.combo.output), 0, "__single_trace__")
|
||||
] = phase_curve
|
||||
phase_plot.setYRange(-180.0, 180.0, padding=0.02)
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ class AppWindowPreprocessMixin:
|
||||
for index, trace in enumerate(session.captured_traces(), start=1):
|
||||
entries.append(
|
||||
f"{display_name}: {index}/{total_count} | "
|
||||
f"input={trace.combo.input_pos} output={trace.combo.output_pos}"
|
||||
f"input={trace.combo.input} output={trace.combo.output}"
|
||||
)
|
||||
return entries
|
||||
|
||||
@@ -134,7 +134,12 @@ class AppWindowPreprocessMixin:
|
||||
f"set={TMP_REFERENCE_SET_NAME}, radar_key={radar_key}"
|
||||
)
|
||||
|
||||
radar_key, collection = capture_reference_set(config, TMP_REFERENCE_SET_NAME, self._store)
|
||||
radar_key, collection = capture_reference_set(
|
||||
config,
|
||||
TMP_REFERENCE_SET_NAME,
|
||||
self._store,
|
||||
median_sweep_count=self._preprocess_median_sweep_count,
|
||||
)
|
||||
self._selected_preprocess_sets["s21_reference"] = TMP_REFERENCE_SET_NAME
|
||||
self._selected_preprocess_radar_key = radar_key
|
||||
self._processor_run_signature = None
|
||||
@@ -176,11 +181,13 @@ class AppWindowPreprocessMixin:
|
||||
dialog.set_set_name(self._preprocess_set_name)
|
||||
dialog.set_radar_config_dir(self._preprocess_radar_config_dir)
|
||||
dialog.set_use_all_radar_configs(self._preprocess_use_all_radar_configs)
|
||||
dialog.set_median_sweep_count(self._preprocess_median_sweep_count)
|
||||
dialog.set_selected_sets(self._selected_preprocess_sets, emit_signal=False)
|
||||
dialog.refresh_requested.connect(self._refresh_sets)
|
||||
dialog.selection_changed.connect(self._on_preprocess_selection_changed)
|
||||
dialog.radar_config_dir_changed.connect(self._on_preprocess_radar_config_inputs_changed)
|
||||
dialog.multi_radar_option_changed.connect(self._on_preprocess_radar_config_inputs_changed)
|
||||
dialog.median_sweep_count_changed.connect(self._on_preprocess_median_sweep_count_changed)
|
||||
dialog.start_sequence_requested.connect(self._start_capture_sequence)
|
||||
dialog.capture_next_requested.connect(self._capture_next_combo)
|
||||
dialog.capture_all_requested.connect(self._capture_all_remaining)
|
||||
@@ -206,6 +213,11 @@ class AppWindowPreprocessMixin:
|
||||
self._preprocess_use_all_radar_configs = dialog.use_all_radar_configs()
|
||||
self._refresh_sets()
|
||||
|
||||
def _on_preprocess_median_sweep_count_changed(self) -> None:
|
||||
"""Persist updated per-combo median sweep count from the preprocessing dialog."""
|
||||
dialog = self._ensure_preprocess_dialog()
|
||||
self._preprocess_median_sweep_count = dialog.median_sweep_count()
|
||||
|
||||
def _on_preprocess_selection_changed(self) -> None:
|
||||
"""Persist selected preprocessing set names from dialog."""
|
||||
dialog = self._ensure_preprocess_dialog()
|
||||
@@ -314,17 +326,31 @@ class AppWindowPreprocessMixin:
|
||||
try:
|
||||
config = self._build_config()
|
||||
display_name = preprocess_asset_display_name(kind)
|
||||
median_sweep_count = dialog.median_sweep_count()
|
||||
self._preprocess_median_sweep_count = median_sweep_count
|
||||
if self._preprocess_use_all_radar_configs:
|
||||
session = self._build_multi_radar_capture_session(config=config, kind=kind, set_name=set_name)
|
||||
session = self._build_multi_radar_capture_session(
|
||||
config=config,
|
||||
kind=kind,
|
||||
set_name=set_name,
|
||||
median_sweep_count=median_sweep_count,
|
||||
)
|
||||
radar_summary = (
|
||||
f"radar_variants={session.radar_variant_count()}, "
|
||||
f"combos={session.state().total_count}"
|
||||
f"combos={session.state().total_count}, "
|
||||
f"median_sweep_count={median_sweep_count}"
|
||||
)
|
||||
else:
|
||||
session = self._build_single_radar_capture_session(config=config, kind=kind, set_name=set_name)
|
||||
session = self._build_single_radar_capture_session(
|
||||
config=config,
|
||||
kind=kind,
|
||||
set_name=set_name,
|
||||
median_sweep_count=median_sweep_count,
|
||||
)
|
||||
radar_summary = (
|
||||
f"radar_key={self._radar_key(config)}, "
|
||||
f"combos={session.state().total_count}"
|
||||
f"combos={session.state().total_count}, "
|
||||
f"median_sweep_count={median_sweep_count}"
|
||||
)
|
||||
|
||||
session.open()
|
||||
@@ -431,6 +457,7 @@ class AppWindowPreprocessMixin:
|
||||
config,
|
||||
kind: str,
|
||||
set_name: str,
|
||||
median_sweep_count: int,
|
||||
) -> SequentialCaptureSession:
|
||||
"""Validate and create the existing single-radar capture session."""
|
||||
radar_key = self._radar_key(config)
|
||||
@@ -438,7 +465,12 @@ class AppWindowPreprocessMixin:
|
||||
display_name = preprocess_asset_display_name(kind)
|
||||
if set_name in existing_sets:
|
||||
raise RuntimeError(f"Set '{set_name}' already exists for {display_name} and cannot be overwritten")
|
||||
return SequentialCaptureSession(config=config, kind=kind, set_name=set_name)
|
||||
return SequentialCaptureSession(
|
||||
config=config,
|
||||
kind=kind,
|
||||
set_name=set_name,
|
||||
median_sweep_count=median_sweep_count,
|
||||
)
|
||||
|
||||
def _build_multi_radar_capture_session(
|
||||
self,
|
||||
@@ -446,6 +478,7 @@ class AppWindowPreprocessMixin:
|
||||
config,
|
||||
kind: str,
|
||||
set_name: str,
|
||||
median_sweep_count: int,
|
||||
) -> MultiRadarSequentialCaptureSession:
|
||||
"""Validate and create the multi-radar capture session."""
|
||||
self._refresh_preprocess_radar_variants()
|
||||
@@ -469,6 +502,7 @@ class AppWindowPreprocessMixin:
|
||||
kind=kind,
|
||||
set_name=set_name,
|
||||
radar_variants=self._preprocess_radar_variants,
|
||||
median_sweep_count=median_sweep_count,
|
||||
)
|
||||
|
||||
def _capture_next_combo(self) -> None:
|
||||
@@ -554,8 +588,8 @@ class AppWindowPreprocessMixin:
|
||||
extra_details = f" | radar_configs={len(capture_result.traces)}"
|
||||
else:
|
||||
trace = capture_result
|
||||
input_pos = trace.combo.input_pos
|
||||
output_pos = trace.combo.output_pos
|
||||
input_pos = trace.combo.input
|
||||
output_pos = trace.combo.output
|
||||
extra_details = ""
|
||||
|
||||
dialog.append_capture_log_entry(
|
||||
@@ -600,8 +634,8 @@ class AppWindowPreprocessMixin:
|
||||
removed_output = removed_capture.combo.output
|
||||
extra_details = f" | radar_configs={len(removed_capture.traces)}"
|
||||
else:
|
||||
removed_input = removed_capture.combo.input_pos
|
||||
removed_output = removed_capture.combo.output_pos
|
||||
removed_input = removed_capture.combo.input
|
||||
removed_output = removed_capture.combo.output
|
||||
extra_details = ""
|
||||
|
||||
dialog.set_capture_log_entries(self._capture_log_entries_for_session(session))
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user