added s11!
This commit is contained in:
@@ -24,6 +24,7 @@ from python_app.models.dataset_model import ResultCollection, SweepCollection
|
||||
from python_app.models.run_config_model import RunConfigModel
|
||||
from python_app.orchestration.config_writer import ConfigWriter
|
||||
from python_app.orchestration.live_processing_config import ProcessingLiveConfigWriter
|
||||
from python_app.orchestration.preprocess_assets import PREPROCESS_ASSET_KEYS, preprocess_asset_model
|
||||
from python_app.orchestration.process_supervisor import ProcessSupervisor
|
||||
from python_app.orchestration.shm_reader import ShmRingReader
|
||||
from python_app.storage.npz_store import NpzStore
|
||||
@@ -78,8 +79,10 @@ class AppWindow(
|
||||
def _init_preprocess_state(self) -> None:
|
||||
"""Initialize preprocessing dialog and selected set names."""
|
||||
self._preprocess_dialog: PreprocessDialog | None = None
|
||||
self._selected_s21_calibration_set = str(self._defaults_config.preprocess.s21_calibration_set)
|
||||
self._selected_s21_reference_set = str(self._defaults_config.preprocess.s21_reference_set)
|
||||
self._selected_preprocess_sets = {
|
||||
key: str(preprocess_asset_model(self._defaults_config, key).set_name)
|
||||
for key in PREPROCESS_ASSET_KEYS
|
||||
}
|
||||
|
||||
def _init_capture_state(self) -> None:
|
||||
"""Initialize one-shot capture and sequence-control flags."""
|
||||
|
||||
@@ -7,6 +7,7 @@ from python_app.models.run_config_model import ComboModel, GprRxGeometryModel, G
|
||||
from python_app.models.run_config_validation import validate_gpr_model
|
||||
from python_app.orchestration.config_writer import parse_combos_from_text
|
||||
from python_app.orchestration.live_processing_config import ProcessingLiveConfig
|
||||
from python_app.orchestration.preprocess_assets import PREPROCESS_ASSET_KEYS, preprocess_asset_model
|
||||
from python_app.storage.npz_store import radar_key_from_config
|
||||
|
||||
|
||||
@@ -118,8 +119,10 @@ class AppWindowConfigMixin:
|
||||
if self._switches_are_effectively_static(config):
|
||||
config.combos = [ComboModel(input=0, output=0)]
|
||||
|
||||
config.preprocess.s21_calibration_set = self._selected_s21_calibration_set
|
||||
config.preprocess.s21_reference_set = self._selected_s21_reference_set
|
||||
for key in PREPROCESS_ASSET_KEYS:
|
||||
asset = preprocess_asset_model(config, key)
|
||||
asset.set_name = self._selected_preprocess_sets[key]
|
||||
asset.bundle_path = ""
|
||||
config.gpr.mode = self._gpr_config_mode.currentText()
|
||||
config.gpr.relative_permittivity = float(self._gpr_relative_permittivity.value())
|
||||
config.gpr.tx_geometry = self._parse_gpr_tx_geometry_text(self._gpr_tx_geometry_input.toPlainText())
|
||||
@@ -132,7 +135,7 @@ class AppWindowConfigMixin:
|
||||
return config
|
||||
|
||||
def _radar_key(self, config: RunConfigModel) -> str:
|
||||
"""Build radar key used by calibration/reference storage lookup."""
|
||||
"""Build radar key used by preprocess-set storage lookup."""
|
||||
return radar_key_from_config(
|
||||
model_name=config.radar.model,
|
||||
serial=config.radar.serial,
|
||||
@@ -158,6 +161,7 @@ class AppWindowConfigMixin:
|
||||
pass_through_y_min_db=min(y_min_db, y_max_db),
|
||||
pass_through_y_max_db=max(y_min_db, y_max_db),
|
||||
bscan_axis=self._bscan_axis.currentText(),
|
||||
bscan_channel=self._bscan_channel.currentText(),
|
||||
bscan_cut_m=float(self._bscan_cut_m.value()),
|
||||
bscan_max_depth_m=float(self._bscan_max_depth_m.value()),
|
||||
bscan_gain=float(self._bscan_gain.value()),
|
||||
|
||||
@@ -9,6 +9,7 @@ from python_app.gui.runtime.history import build_run_history_signature, record_r
|
||||
from python_app.hardware_full.librevna_service import LibreVnaService
|
||||
from python_app.models.dataset_model import ComboKey, ResultCollection, SweepCollection
|
||||
from python_app.models.run_config_model import RunConfigModel
|
||||
from python_app.orchestration.preprocess_assets import PREPROCESS_ASSET_KEYS, PREPROCESS_ASSET_SPECS, preprocess_asset_model
|
||||
from python_app.orchestration.shm_reader import ShmRingReader
|
||||
|
||||
|
||||
@@ -38,28 +39,26 @@ class AppWindowPipelineMixin:
|
||||
run_signature = self._build_run_history_signature(config)
|
||||
radar_key = self._radar_key(config)
|
||||
|
||||
if not config.preprocess.s21_calibration_set or not config.preprocess.s21_reference_set:
|
||||
raise RuntimeError("Select calibration and reference sets in Preprocessing Panel before Start")
|
||||
missing_assets = [
|
||||
PREPROCESS_ASSET_SPECS[key].display_name
|
||||
for key in PREPROCESS_ASSET_KEYS
|
||||
if not preprocess_asset_model(config, key).set_name
|
||||
]
|
||||
if missing_assets:
|
||||
raise RuntimeError(
|
||||
"Select all required preprocess sets in Preprocessing Panel before Start: "
|
||||
+ ", ".join(missing_assets)
|
||||
)
|
||||
|
||||
combo_keys = [ComboKey(input_pos=combo.input, output_pos=combo.output) for combo in config.combos]
|
||||
|
||||
if not self._store.has_combo_coverage(
|
||||
"calibration", radar_key, config.preprocess.s21_calibration_set, combo_keys
|
||||
):
|
||||
raise RuntimeError("Selected calibration set does not cover requested run combos")
|
||||
if not self._store.has_combo_coverage(
|
||||
"reference", radar_key, config.preprocess.s21_reference_set, combo_keys
|
||||
):
|
||||
raise RuntimeError("Selected reference set does not cover requested run combos")
|
||||
for key in PREPROCESS_ASSET_KEYS:
|
||||
spec = PREPROCESS_ASSET_SPECS[key]
|
||||
asset = preprocess_asset_model(config, key)
|
||||
if not self._store.has_combo_coverage(spec.set_kind, radar_key, asset.set_name, combo_keys):
|
||||
raise RuntimeError(f"Selected {spec.display_name} set does not cover requested run combos")
|
||||
|
||||
calibration_bundle, reference_bundle = self._config_writer.prepare_s21_bundles(
|
||||
self._store,
|
||||
radar_key,
|
||||
config.preprocess.s21_calibration_set,
|
||||
config.preprocess.s21_reference_set,
|
||||
)
|
||||
config.preprocess.s21_calibration_bundle_path = str(calibration_bundle)
|
||||
config.preprocess.s21_reference_bundle_path = str(reference_bundle)
|
||||
self._config_writer.prepare_preprocess_bundles(self._store, radar_key, config)
|
||||
config.runtime.continuous = not single_capture
|
||||
|
||||
if not single_capture:
|
||||
|
||||
@@ -389,7 +389,10 @@ class AppWindowPlotMixin:
|
||||
self._bscan_plot.addItem(image_item)
|
||||
self._bscan_plot.setXRange(x_min, x_max, padding=0.02)
|
||||
self._bscan_plot.setYRange(depth_min, depth_max, padding=0.02)
|
||||
self._bscan_plot.setTitle(f"B-scan in{display_key[0]}/out{display_key[1]} | sweeps={sweep_count}")
|
||||
bscan_channel = self._bscan_channel.currentText().upper()
|
||||
self._bscan_plot.setTitle(
|
||||
f"B-scan {bscan_channel} in{display_key[0]}/out{display_key[1]} | sweeps={sweep_count}"
|
||||
)
|
||||
return True
|
||||
|
||||
def _sync_bscan_history_from_results(self) -> None:
|
||||
@@ -793,12 +796,13 @@ class AppWindowPlotMixin:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _draw_single_trace(self, trace: TraceData, title: str) -> None:
|
||||
def _draw_single_trace(self, trace: TraceData, title: str, *, channel: str = "s21") -> None:
|
||||
"""Draw one trace on stacked magnitude/phase plots."""
|
||||
show_magnitude = self._show_magnitude_curves()
|
||||
show_phase = self._show_phase_curves()
|
||||
magnitude_plot = self._trace_magnitude_plot
|
||||
phase_plot = self._trace_phase_plot
|
||||
samples = trace.s11 if channel == "s11" else trace.s21
|
||||
|
||||
magnitude_plot.setVisible(show_magnitude)
|
||||
phase_plot.setVisible(show_phase)
|
||||
@@ -822,7 +826,7 @@ class AppWindowPlotMixin:
|
||||
phase_plot.setTitle(title)
|
||||
|
||||
if show_magnitude:
|
||||
magnitude_db = 20.0 * np.log10(np.maximum(np.abs(trace.s21), 1e-12))
|
||||
magnitude_db = 20.0 * np.log10(np.maximum(np.abs(samples), 1e-12))
|
||||
magnitude_curve = pg.PlotCurveItem(
|
||||
trace.frequency_hz,
|
||||
magnitude_db,
|
||||
@@ -834,7 +838,7 @@ class AppWindowPlotMixin:
|
||||
] = magnitude_curve
|
||||
|
||||
if show_phase:
|
||||
phase_deg = np.degrees(np.angle(trace.s21))
|
||||
phase_deg = np.degrees(np.angle(samples))
|
||||
phase_curve = pg.PlotCurveItem(
|
||||
trace.frequency_hz,
|
||||
phase_deg,
|
||||
|
||||
@@ -3,11 +3,17 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from python_app.gui.preprocess_dialog import PreprocessDialog
|
||||
from python_app.orchestration.preprocess_assets import (
|
||||
PREPROCESS_ASSET_KEYS,
|
||||
PREPROCESS_ASSET_SPECS,
|
||||
preprocess_asset_channel,
|
||||
preprocess_asset_display_name,
|
||||
)
|
||||
from python_app.workflows.sequential_capture_workflow import SequentialCaptureSession
|
||||
|
||||
|
||||
class AppWindowPreprocessMixin:
|
||||
"""Handles calibration/reference set management and capture workflow."""
|
||||
"""Handles preprocess set management and sequential capture workflows."""
|
||||
|
||||
def _open_preprocess_panel(self) -> None:
|
||||
"""Open preprocessing dialog and refresh available sets."""
|
||||
@@ -38,39 +44,39 @@ class AppWindowPreprocessMixin:
|
||||
self._update_capture_dialog_state()
|
||||
return dialog
|
||||
|
||||
def _on_preprocess_selection_changed(self, calibration_set: str, reference_set: str) -> None:
|
||||
def _on_preprocess_selection_changed(self) -> None:
|
||||
"""Persist selected preprocessing set names from dialog."""
|
||||
self._selected_s21_calibration_set = calibration_set.strip()
|
||||
self._selected_s21_reference_set = reference_set.strip()
|
||||
dialog = self._ensure_preprocess_dialog()
|
||||
self._selected_preprocess_sets = dialog.selection_snapshot()
|
||||
self._refresh_preprocess_summary_labels()
|
||||
|
||||
def _refresh_preprocess_summary_labels(self) -> None:
|
||||
"""Update compact summary labels in the main window."""
|
||||
self._selected_calibration_label.setText(self._selected_s21_calibration_set or "<not selected>")
|
||||
self._selected_reference_label.setText(self._selected_s21_reference_set or "<not selected>")
|
||||
for key in PREPROCESS_ASSET_KEYS:
|
||||
self._selected_preprocess_labels[key].setText(self._selected_preprocess_sets.get(key, "") or "<not selected>")
|
||||
|
||||
def _refresh_sets(self) -> None:
|
||||
"""Refresh calibration/reference set lists for current radar key."""
|
||||
"""Refresh preprocess set lists for current radar key."""
|
||||
config = self._build_config()
|
||||
radar_key = self._radar_key(config)
|
||||
calibration_sets = self._store.list_sets("calibration", radar_key)
|
||||
reference_sets = self._store.list_sets("reference", radar_key)
|
||||
|
||||
dialog = self._ensure_preprocess_dialog()
|
||||
dialog.set_calibration_sets(calibration_sets)
|
||||
dialog.set_reference_sets(reference_sets)
|
||||
|
||||
if self._selected_s21_calibration_set not in calibration_sets:
|
||||
self._selected_s21_calibration_set = calibration_sets[0] if calibration_sets else ""
|
||||
if self._selected_s21_reference_set not in reference_sets:
|
||||
self._selected_s21_reference_set = reference_sets[0] if reference_sets else ""
|
||||
available_sets = {
|
||||
key: self._store.list_sets(PREPROCESS_ASSET_SPECS[key].set_kind, radar_key)
|
||||
for key in PREPROCESS_ASSET_KEYS
|
||||
}
|
||||
dialog.set_available_sets(available_sets)
|
||||
|
||||
dialog.set_selected_sets(self._selected_s21_calibration_set, self._selected_s21_reference_set)
|
||||
for key, names in available_sets.items():
|
||||
if self._selected_preprocess_sets.get(key, "") not in names:
|
||||
self._selected_preprocess_sets[key] = names[0] if names else ""
|
||||
|
||||
dialog.set_selected_sets(self._selected_preprocess_sets)
|
||||
self._refresh_preprocess_summary_labels()
|
||||
self._log(f"Set lists refreshed for key={radar_key}")
|
||||
self._log(f"Preprocess set lists refreshed for key={radar_key}")
|
||||
|
||||
def _start_capture_sequence(self, kind: str) -> None:
|
||||
"""Start sequential capture session for requested preprocessing kind."""
|
||||
"""Start sequential capture session for requested preprocess asset."""
|
||||
if self._capture_session is not None:
|
||||
self._show_error("Another capture sequence is already active")
|
||||
return
|
||||
@@ -91,18 +97,19 @@ class AppWindowPreprocessMixin:
|
||||
try:
|
||||
config = self._build_config()
|
||||
radar_key = self._radar_key(config)
|
||||
existing_sets = self._store.list_sets(kind, radar_key)
|
||||
existing_sets = self._store.list_sets(PREPROCESS_ASSET_SPECS[kind].set_kind, radar_key)
|
||||
display_name = preprocess_asset_display_name(kind)
|
||||
if set_name in existing_sets:
|
||||
raise RuntimeError(f"Set '{set_name}' already exists for {kind} and cannot be overwritten")
|
||||
raise RuntimeError(f"Set '{set_name}' already exists for {display_name} and cannot be overwritten")
|
||||
|
||||
session = SequentialCaptureSession(config=config, kind=kind, set_name=set_name)
|
||||
session.open()
|
||||
self._capture_session = session
|
||||
|
||||
dialog.clear_capture_log()
|
||||
dialog.set_status(f"{kind.title()} sequence started")
|
||||
dialog.set_status(f"{display_name} sequence started")
|
||||
self._update_capture_dialog_state()
|
||||
self._log(f"{kind.title()} sequence started for set={set_name}; fill all N*M combos")
|
||||
self._log(f"{display_name} sequence started for set={set_name}; fill all N*M combos")
|
||||
except Exception as exc: # noqa: BLE001
|
||||
self._cleanup_capture_session()
|
||||
self._show_error(f"Failed to start {kind} sequence: {exc}")
|
||||
@@ -121,9 +128,11 @@ class AppWindowPreprocessMixin:
|
||||
trace = session.capture_current_combo()
|
||||
state = session.state()
|
||||
tx_label, rx_label = dialog.antenna_labels()
|
||||
display_name = preprocess_asset_display_name(session.kind)
|
||||
channel = preprocess_asset_channel(session.kind)
|
||||
|
||||
dialog.append_capture_log_entry(
|
||||
kind=session.kind,
|
||||
kind=display_name,
|
||||
captured_count=state.captured_count,
|
||||
total_count=state.total_count,
|
||||
input_pos=trace.combo.input_pos,
|
||||
@@ -132,11 +141,11 @@ class AppWindowPreprocessMixin:
|
||||
rx_label=rx_label,
|
||||
)
|
||||
|
||||
dialog.draw_last_trace(trace, title=f"{session.kind.title()} captured")
|
||||
self._draw_single_trace(trace, title=f"{session.kind.title()} last trace")
|
||||
dialog.draw_last_trace(trace, title=f"{display_name} captured", channel=channel)
|
||||
self._draw_single_trace(trace, title=f"{display_name} last trace", channel=channel)
|
||||
|
||||
self._log(
|
||||
f"{session.kind.title()} capture: {state.captured_count}/{state.total_count} | "
|
||||
f"{display_name} capture: {state.captured_count}/{state.total_count} | "
|
||||
f"input={trace.combo.input_pos} output={trace.combo.output_pos}"
|
||||
)
|
||||
|
||||
@@ -144,16 +153,13 @@ class AppWindowPreprocessMixin:
|
||||
radar_key, collection = session.finalize(self._store)
|
||||
set_name = session.set_name
|
||||
kind = session.kind
|
||||
display_name = preprocess_asset_display_name(kind)
|
||||
self._cleanup_capture_session()
|
||||
|
||||
if kind == "calibration":
|
||||
self._selected_s21_calibration_set = set_name
|
||||
else:
|
||||
self._selected_s21_reference_set = set_name
|
||||
|
||||
self._selected_preprocess_sets[kind] = set_name
|
||||
self._refresh_sets()
|
||||
dialog.set_status(f"{kind.title()} set saved: {set_name} ({len(collection.traces)} traces)")
|
||||
self._log(f"{kind.title()} sequence completed and saved: set={set_name}, key={radar_key}")
|
||||
dialog.set_status(f"{display_name} set saved: {set_name} ({len(collection.traces)} traces)")
|
||||
self._log(f"{display_name} sequence completed and saved: set={set_name}, key={radar_key}")
|
||||
self._resume_pipeline_if_needed()
|
||||
else:
|
||||
self._update_capture_dialog_state()
|
||||
@@ -166,11 +172,11 @@ class AppWindowPreprocessMixin:
|
||||
if self._capture_session is None:
|
||||
return
|
||||
|
||||
kind = self._capture_session.kind
|
||||
display_name = preprocess_asset_display_name(self._capture_session.kind)
|
||||
self._cleanup_capture_session()
|
||||
dialog = self._ensure_preprocess_dialog()
|
||||
dialog.set_status(f"{kind.title()} sequence aborted")
|
||||
self._log(f"{kind.title()} sequence aborted")
|
||||
dialog.set_status(f"{display_name} sequence aborted")
|
||||
self._log(f"{display_name} sequence aborted")
|
||||
if resume_pipeline:
|
||||
self._resume_pipeline_if_needed()
|
||||
|
||||
|
||||
@@ -4,16 +4,18 @@ from __future__ import annotations
|
||||
|
||||
from PyQt6.QtWidgets import QFormLayout, QGroupBox, QLabel
|
||||
|
||||
from python_app.orchestration.preprocess_assets import PREPROCESS_ASSET_KEYS, preprocess_asset_display_name
|
||||
|
||||
|
||||
def build_preprocess_summary_group(owner) -> QGroupBox:
|
||||
"""Create selected calibration/reference summary section."""
|
||||
"""Create selected preprocess-set summary section."""
|
||||
group = QGroupBox("Selected Preprocess Sets")
|
||||
form = QFormLayout(group)
|
||||
form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
|
||||
|
||||
owner._selected_calibration_label = QLabel("<not selected>")
|
||||
owner._selected_reference_label = QLabel("<not selected>")
|
||||
|
||||
form.addRow("Calibration", owner._selected_calibration_label)
|
||||
form.addRow("Reference", owner._selected_reference_label)
|
||||
owner._selected_preprocess_labels = {}
|
||||
for key in PREPROCESS_ASSET_KEYS:
|
||||
label = QLabel("<not selected>")
|
||||
owner._selected_preprocess_labels[key] = label
|
||||
form.addRow(preprocess_asset_display_name(key), label)
|
||||
return group
|
||||
|
||||
@@ -110,6 +110,9 @@ def build_processing_group(owner) -> QGroupBox:
|
||||
owner._bscan_axis = QComboBox()
|
||||
owner._bscan_axis.addItems(["abs", "real", "phase"])
|
||||
|
||||
owner._bscan_channel = QComboBox()
|
||||
owner._bscan_channel.addItems(["s21", "s11"])
|
||||
|
||||
owner._bscan_cut_m = QDoubleSpinBox()
|
||||
owner._bscan_cut_m.setDecimals(3)
|
||||
owner._bscan_cut_m.setRange(0.0, 2.0)
|
||||
@@ -141,6 +144,7 @@ def build_processing_group(owner) -> QGroupBox:
|
||||
owner._bscan_stop_freq_mhz.setValue(8800.0)
|
||||
|
||||
bscan_form.addRow("Axis", owner._bscan_axis)
|
||||
bscan_form.addRow("Channel", owner._bscan_channel)
|
||||
bscan_form.addRow("Cut m", owner._bscan_cut_m)
|
||||
bscan_form.addRow("Max depth m", owner._bscan_max_depth_m)
|
||||
bscan_form.addRow("Gain", owner._bscan_gain)
|
||||
@@ -221,6 +225,7 @@ def build_processing_group(owner) -> QGroupBox:
|
||||
form.addRow(owner._processing_mode_pages)
|
||||
|
||||
owner._bscan_axis.currentTextChanged.connect(owner._on_processing_live_settings_changed)
|
||||
owner._bscan_channel.currentTextChanged.connect(owner._on_processing_live_settings_changed)
|
||||
owner._bscan_cut_m.valueChanged.connect(owner._on_processing_live_settings_changed)
|
||||
owner._bscan_max_depth_m.valueChanged.connect(owner._on_processing_live_settings_changed)
|
||||
owner._bscan_gain.valueChanged.connect(owner._on_processing_live_settings_changed)
|
||||
|
||||
@@ -22,15 +22,17 @@ def _result_tail(
|
||||
for collection in result_history[-history_limit:]
|
||||
if int(collection.collection_id) > int(floor_collection_id)
|
||||
]
|
||||
unique_tail: list[ResultCollection] = []
|
||||
unique_reversed_tail: list[ResultCollection] = []
|
||||
seen_keys: set[tuple[int, int]] = set()
|
||||
for collection in filtered:
|
||||
for collection in reversed(filtered):
|
||||
key = (int(collection.collection_id), int(collection.monotonic_ns))
|
||||
if key in seen_keys:
|
||||
continue
|
||||
seen_keys.add(key)
|
||||
unique_tail.append(collection)
|
||||
return unique_tail
|
||||
unique_reversed_tail.append(collection)
|
||||
|
||||
unique_reversed_tail.reverse()
|
||||
return unique_reversed_tail
|
||||
|
||||
|
||||
def build_bscan_signature(
|
||||
@@ -47,6 +49,7 @@ def build_bscan_signature(
|
||||
)
|
||||
return (
|
||||
str(live_config.bscan_axis),
|
||||
str(live_config.bscan_channel),
|
||||
float(live_config.bscan_cut_m),
|
||||
float(live_config.bscan_max_depth_m),
|
||||
float(live_config.bscan_gain),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Dialog for calibration/reference set selection and sequential capture."""
|
||||
"""Dialog for preprocess set selection and sequential capture."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -6,6 +6,7 @@ from PyQt6.QtCore import pyqtSignal
|
||||
from PyQt6.QtWidgets import (
|
||||
QComboBox,
|
||||
QDialog,
|
||||
QFormLayout,
|
||||
QGridLayout,
|
||||
QGroupBox,
|
||||
QHBoxLayout,
|
||||
@@ -15,23 +16,24 @@ from PyQt6.QtWidgets import (
|
||||
QPushButton,
|
||||
QVBoxLayout,
|
||||
)
|
||||
import pyqtgraph as pg
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
|
||||
from python_app.models.dataset_model import TraceData
|
||||
from python_app.orchestration.preprocess_assets import (
|
||||
PREPROCESS_ASSET_KEYS,
|
||||
PREPROCESS_ASSET_SPECS,
|
||||
S11_PREPROCESS_ASSET_KEYS,
|
||||
S21_PREPROCESS_ASSET_KEYS,
|
||||
preprocess_asset_display_name,
|
||||
)
|
||||
|
||||
|
||||
class PreprocessDialog(QDialog):
|
||||
"""Standalone dialog for preprocessing capture workflows.
|
||||
|
||||
The dialog combines three concerns:
|
||||
1. Set selection (calibration/reference).
|
||||
2. Sequential capture controls for filling all N*M combinations.
|
||||
3. Quick preview of the last captured trace.
|
||||
"""
|
||||
"""Standalone dialog for preprocess set selection and capture workflows."""
|
||||
|
||||
refresh_requested = pyqtSignal()
|
||||
selection_changed = pyqtSignal(str, str)
|
||||
selection_changed = pyqtSignal()
|
||||
start_sequence_requested = pyqtSignal(str)
|
||||
capture_next_requested = pyqtSignal()
|
||||
abort_sequence_requested = pyqtSignal()
|
||||
@@ -39,13 +41,14 @@ class PreprocessDialog(QDialog):
|
||||
def __init__(self, parent=None) -> None:
|
||||
"""Initialize window metadata and compose dialog UI."""
|
||||
super().__init__(parent)
|
||||
self._set_combos: dict[str, QComboBox] = {}
|
||||
self._init_window()
|
||||
self._build_ui()
|
||||
|
||||
def _init_window(self) -> None:
|
||||
"""Set static window properties."""
|
||||
self.setWindowTitle("Preprocessing Setup")
|
||||
self.resize(1040, 760)
|
||||
self.resize(1120, 820)
|
||||
|
||||
def _build_ui(self) -> None:
|
||||
"""Build root dialog layout and all sections."""
|
||||
@@ -57,28 +60,33 @@ class PreprocessDialog(QDialog):
|
||||
|
||||
def _build_sets_group(self) -> QGroupBox:
|
||||
"""Build set-management controls used for preprocessing snapshots."""
|
||||
group = QGroupBox("Calibration / Reference Sets", self)
|
||||
layout = QGridLayout(group)
|
||||
group = QGroupBox("Preprocess Sets", self)
|
||||
layout = QVBoxLayout(group)
|
||||
|
||||
header_row = QHBoxLayout()
|
||||
self._set_name_input = QLineEdit("set_001", group)
|
||||
self._calibration_combo = QComboBox(group)
|
||||
self._reference_combo = QComboBox(group)
|
||||
|
||||
refresh_button = QPushButton("Refresh Sets", group)
|
||||
refresh_button.clicked.connect(self.refresh_requested.emit)
|
||||
header_row.addWidget(QLabel("Set name"))
|
||||
header_row.addWidget(self._set_name_input, stretch=1)
|
||||
header_row.addWidget(refresh_button)
|
||||
layout.addLayout(header_row)
|
||||
|
||||
self._calibration_combo.currentTextChanged.connect(self._emit_selection_changed)
|
||||
self._reference_combo.currentTextChanged.connect(self._emit_selection_changed)
|
||||
layout.addWidget(self._build_selector_group("S21", S21_PREPROCESS_ASSET_KEYS, group))
|
||||
layout.addWidget(self._build_selector_group("S11", S11_PREPROCESS_ASSET_KEYS, group))
|
||||
return group
|
||||
|
||||
layout.addWidget(QLabel("Set name"), 0, 0)
|
||||
layout.addWidget(self._set_name_input, 0, 1)
|
||||
layout.addWidget(refresh_button, 0, 2)
|
||||
def _build_selector_group(self, title: str, keys: tuple[str, ...], parent: QGroupBox) -> QGroupBox:
|
||||
"""Build one selector subgroup for a channel family."""
|
||||
group = QGroupBox(title, parent)
|
||||
form = QFormLayout(group)
|
||||
form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow)
|
||||
|
||||
layout.addWidget(QLabel("Calibration set"), 1, 0)
|
||||
layout.addWidget(self._calibration_combo, 1, 1, 1, 2)
|
||||
|
||||
layout.addWidget(QLabel("Reference set"), 2, 0)
|
||||
layout.addWidget(self._reference_combo, 2, 1, 1, 2)
|
||||
for key in keys:
|
||||
combo = QComboBox(group)
|
||||
combo.currentTextChanged.connect(self._emit_selection_changed)
|
||||
self._set_combos[key] = combo
|
||||
form.addRow(self._asset_row_label(key), combo)
|
||||
return group
|
||||
|
||||
def _build_sequence_group(self) -> QGroupBox:
|
||||
@@ -95,8 +103,6 @@ class PreprocessDialog(QDialog):
|
||||
self._tx_antenna_label_input.setPlaceholderText("e.g. TX_A")
|
||||
self._rx_antenna_label_input.setPlaceholderText("e.g. RX_B")
|
||||
|
||||
button_row = self._build_sequence_button_row(group)
|
||||
|
||||
layout.addWidget(QLabel("Active type"), 0, 0)
|
||||
layout.addWidget(self._active_kind_label, 0, 1)
|
||||
layout.addWidget(QLabel("Progress"), 1, 0)
|
||||
@@ -107,22 +113,26 @@ class PreprocessDialog(QDialog):
|
||||
layout.addWidget(self._tx_antenna_label_input, 3, 1)
|
||||
layout.addWidget(QLabel("RX antenna label"), 4, 0)
|
||||
layout.addWidget(self._rx_antenna_label_input, 4, 1)
|
||||
layout.addLayout(button_row, 5, 0, 1, 2)
|
||||
layout.addLayout(self._build_sequence_button_grid(group), 5, 0, 1, 2)
|
||||
layout.addLayout(self._build_sequence_action_row(group), 6, 0, 1, 2)
|
||||
|
||||
self._capture_log = QPlainTextEdit(group)
|
||||
self._capture_log.setReadOnly(True)
|
||||
self._capture_log.setPlaceholderText("Capture history per combo")
|
||||
layout.addWidget(self._capture_log, 6, 0, 1, 2)
|
||||
layout.addWidget(self._capture_log, 7, 0, 1, 2)
|
||||
return group
|
||||
|
||||
def _build_sequence_button_row(self, parent: QGroupBox) -> QHBoxLayout:
|
||||
"""Build action buttons for sequence flow control."""
|
||||
start_calibration_button = QPushButton("Start Calibration Sequence", parent)
|
||||
start_calibration_button.clicked.connect(lambda: self.start_sequence_requested.emit("calibration"))
|
||||
|
||||
start_reference_button = QPushButton("Start Reference Sequence", parent)
|
||||
start_reference_button.clicked.connect(lambda: self.start_sequence_requested.emit("reference"))
|
||||
def _build_sequence_button_grid(self, parent: QGroupBox) -> QGridLayout:
|
||||
"""Build per-asset capture start buttons."""
|
||||
layout = QGridLayout()
|
||||
for index, key in enumerate(PREPROCESS_ASSET_KEYS):
|
||||
button = QPushButton(f"Start {preprocess_asset_display_name(key)}", parent)
|
||||
button.clicked.connect(lambda _checked=False, asset_key=key: self.start_sequence_requested.emit(asset_key))
|
||||
layout.addWidget(button, index // 2, index % 2)
|
||||
return layout
|
||||
|
||||
def _build_sequence_action_row(self, parent: QGroupBox) -> QHBoxLayout:
|
||||
"""Build capture/abort row for active sequence control."""
|
||||
self._capture_next_button = QPushButton("Capture Current Combo", parent)
|
||||
self._capture_next_button.clicked.connect(self.capture_next_requested.emit)
|
||||
self._capture_next_button.setEnabled(False)
|
||||
@@ -131,12 +141,11 @@ class PreprocessDialog(QDialog):
|
||||
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)
|
||||
return button_row
|
||||
layout = QHBoxLayout()
|
||||
layout.addWidget(self._capture_next_button)
|
||||
layout.addWidget(self._abort_button)
|
||||
layout.addStretch(1)
|
||||
return layout
|
||||
|
||||
def _build_status_line(self, root_layout: QVBoxLayout) -> None:
|
||||
"""Build one-line status output for dialog operations."""
|
||||
@@ -155,13 +164,9 @@ class PreprocessDialog(QDialog):
|
||||
"""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 selection_snapshot(self) -> dict[str, str]:
|
||||
"""Return currently selected set names keyed by preprocess asset key."""
|
||||
return {key: self._set_combos[key].currentText().strip() for key in PREPROCESS_ASSET_KEYS}
|
||||
|
||||
def antenna_labels(self) -> tuple[str, str]:
|
||||
"""Return optional TX/RX user labels used in capture logs."""
|
||||
@@ -209,7 +214,8 @@ class PreprocessDialog(QDialog):
|
||||
self._abort_button.setEnabled(False)
|
||||
return
|
||||
|
||||
self._active_kind_label.setText(kind)
|
||||
active_label = preprocess_asset_display_name(kind) if kind in PREPROCESS_ASSET_SPECS else kind
|
||||
self._active_kind_label.setText(active_label)
|
||||
self._progress_label.setText(f"{captured_count} / {total_count}")
|
||||
if next_input is None or next_output is None:
|
||||
self._combo_label.setText("<complete>")
|
||||
@@ -220,35 +226,32 @@ class PreprocessDialog(QDialog):
|
||||
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_available_sets(self, available_sets: dict[str, list[str]]) -> None:
|
||||
"""Replace combo-box choices for all preprocess assets."""
|
||||
for key in PREPROCESS_ASSET_KEYS:
|
||||
combo = self._set_combos[key]
|
||||
self._set_combo_items(combo, available_sets.get(key, []), combo.currentText().strip())
|
||||
|
||||
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)
|
||||
def set_selected_sets(self, selected_sets: dict[str, str]) -> None:
|
||||
"""Apply selected set names to all comboboxes and emit selection update."""
|
||||
for key in PREPROCESS_ASSET_KEYS:
|
||||
selected_value = selected_sets.get(key, "")
|
||||
if not selected_value:
|
||||
continue
|
||||
combo = self._set_combos[key]
|
||||
index = combo.findText(selected_value)
|
||||
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)
|
||||
|
||||
combo.setCurrentIndex(index)
|
||||
self._emit_selection_changed()
|
||||
|
||||
def set_status(self, message: str) -> None:
|
||||
"""Set short human-readable status line."""
|
||||
self._status_label.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))
|
||||
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."""
|
||||
samples = trace.s11 if channel == "s11" else trace.s21
|
||||
magnitude_db = 20.0 * np.log10(np.maximum(np.abs(samples), 1e-12))
|
||||
self._preview_plot.clear()
|
||||
self._preview_plot.plot(
|
||||
trace.frequency_hz,
|
||||
@@ -261,8 +264,13 @@ class PreprocessDialog(QDialog):
|
||||
)
|
||||
|
||||
def _emit_selection_changed(self) -> None:
|
||||
"""Emit current calibration/reference selection."""
|
||||
self.selection_changed.emit(self.calibration_set(), self.reference_set())
|
||||
"""Emit current selection snapshot change."""
|
||||
self.selection_changed.emit()
|
||||
|
||||
@staticmethod
|
||||
def _asset_row_label(key: str) -> str:
|
||||
"""Return short selector label for one preprocess asset."""
|
||||
return preprocess_asset_display_name(key)
|
||||
|
||||
@staticmethod
|
||||
def _set_combo_items(combo: QComboBox, names: list[str], current_text: str) -> None:
|
||||
|
||||
@@ -7,6 +7,7 @@ from typing import TypeVar
|
||||
|
||||
from python_app.models.dataset_model import ResultCollection, SweepCollection
|
||||
from python_app.models.run_config_model import RunConfigModel
|
||||
from python_app.orchestration.preprocess_assets import PREPROCESS_ASSET_KEYS, preprocess_asset_model
|
||||
|
||||
THistoryCollection = TypeVar("THistoryCollection", SweepCollection, ResultCollection)
|
||||
|
||||
@@ -63,6 +64,7 @@ def build_run_history_signature(
|
||||
) -> tuple[object, ...]:
|
||||
"""Build deterministic signature to detect run-settings changes (excluding live processing params)."""
|
||||
combos_signature = tuple((int(combo.input), int(combo.output)) for combo in config.combos)
|
||||
preprocess_signature = tuple(preprocess_asset_model(config, key).set_name for key in PREPROCESS_ASSET_KEYS)
|
||||
return (
|
||||
str(config.radar.driver_mode),
|
||||
str(config.radar.serial),
|
||||
@@ -79,8 +81,7 @@ def build_run_history_signature(
|
||||
str(config.output_switch.driver),
|
||||
int(config.output_switch.positions),
|
||||
bool(config.output_switch.invert_logic),
|
||||
str(config.preprocess.s21_calibration_set),
|
||||
str(config.preprocess.s21_reference_set),
|
||||
preprocess_signature,
|
||||
combos_signature,
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user