added s11!

This commit is contained in:
Ayzen
2026-03-26 18:29:42 +03:00
parent 9ddbde22bd
commit 077542cbd0
43 changed files with 713 additions and 347 deletions
@@ -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)