added s11 collections

This commit is contained in:
Ayzen
2026-03-26 15:48:16 +03:00
parent 9581730e41
commit 24f7ebb2fb
22 changed files with 184 additions and 859 deletions
@@ -2,7 +2,6 @@
from __future__ import annotations
from python_app.gui.runtime.history import remove_last_aligned_histories
from python_app.hardware_full.librevna_service import LibreVnaService
from python_app.models.run_config_model import ComboModel, GprRxGeometryModel, GprTxGeometryModel, RunConfigModel
from python_app.models.run_config_validation import validate_gpr_model
@@ -200,6 +199,8 @@ class AppWindowConfigMixin:
self._clear_gpr_plot()
elif self._result_history:
self._draw_results(self._result_history[-1])
else:
self._clear_trace_plots()
except Exception as exc: # noqa: BLE001
self._show_error(f"Failed to update live processing settings: {exc}")
@@ -218,72 +219,16 @@ class AppWindowConfigMixin:
self._processing_mode_pages.updateGeometry()
self._on_processing_live_settings_changed()
def _on_bscan_clear_history_clicked(self) -> None:
"""Permanently clear all runtime histories, ring backlogs, and B-scan cache."""
self._apply_history_mode_deletion(mode_label="B-scan", remove_last_only=False)
def _on_bscan_remove_last_sweep_clicked(self) -> None:
"""Permanently delete the latest sweep from runtime histories and rings."""
self._apply_history_mode_deletion(mode_label="B-scan", remove_last_only=True)
def _on_gpr_clear_history_clicked(self) -> None:
"""Permanently clear all runtime histories, ring backlogs, and GPR cache."""
self._apply_history_mode_deletion(mode_label="GPR", remove_last_only=False)
def _on_gpr_remove_last_measurement_clicked(self) -> None:
"""Permanently delete the latest measurement from runtime histories and rings."""
self._apply_history_mode_deletion(mode_label="GPR", remove_last_only=True)
def _clear_history_mode_caches(self) -> None:
"""Drop mode-specific cached render state."""
"""Drop cached render state for pass-through, B-scan, and GPR views."""
self._bscan_history_floor_collection_id = 0
self._clear_bscan_plot_history()
if hasattr(self, "_bscan_plot"):
self._bscan_plot.clear()
self._clear_trace_plots()
if hasattr(self, "_gpr_plot"):
self._clear_gpr_plot()
def _apply_history_mode_deletion(self, *, mode_label: str, remove_last_only: bool) -> None:
"""Apply destructive history deletion via C++ processor history commands."""
resume_acquisition = self._supervisor.is_running()
history_command = "remove_last" if remove_last_only else "clear_all"
action = "last measurement removed" if remove_last_only else "history fully cleared"
dropped_results = 0
try:
if resume_acquisition:
self._stop_run()
if self._result_reader is not None:
dropped_results = self._result_reader.drop_all()
if remove_last_only:
retained_raw, retained_pre, retained_result = remove_last_aligned_histories(
list(self._raw_history),
list(self._pre_history),
list(self._result_history),
)
else:
retained_raw = []
retained_pre = []
retained_result = []
self._replace_runtime_history(
retained_raw=retained_raw,
retained_pre=retained_pre,
retained_result=retained_result,
)
self._clear_history_mode_caches()
self._write_live_processing_config(history_command=history_command, bump_history_seq=True)
if self._supervisor.is_processor_running():
self._drain_results_until_quiet(timeout_s=0.35, poll_s=0.01)
self._update_history_indicator()
self._redraw_after_history_deletion()
if resume_acquisition:
self._start_run()
self._log(f"{mode_label} {action}; dropped pending results={dropped_results}")
except Exception as exc: # noqa: BLE001
self._show_error(f"Failed to delete {mode_label} history: {exc}")
def _redraw_after_history_deletion(self) -> None:
"""Refresh plot immediately after destructive history deletion."""
if self._processing_mode.currentText() == "bscan":
@@ -536,7 +536,7 @@ class AppWindowPlotMixin:
plot.setLabel("left", "Depth", units="m")
view_box = plot.getViewBox()
view_box.invertY(True)
view_box.invertY(False)
view_box.enableAutoRange(x=False, y=False)
if self._gpr_lookup_table is None:
@@ -673,6 +673,7 @@ class AppWindowPlotMixin:
plot.setUpdatesEnabled(False)
try:
self._ensure_gpr_plot_items()
plot.getViewBox().invertY(False)
self._clear_gpr_point_labels()
self._clear_gpr_region_labels()
self._clear_gpr_region_masks()
@@ -684,7 +685,7 @@ class AppWindowPlotMixin:
self._gpr_image_item.show()
plot.setXRange(x_min, x_max, padding=0.02)
plot.setYRange(y_min, y_max, padding=0.02)
plot.setYRange(min(0.0, y_min), y_max, padding=0.02)
x_tx, x_rx = self._selected_gpr_geometry()
if x_tx.size > 0:
@@ -6,7 +6,7 @@ from pathlib import Path
import time
from PyQt6.QtWidgets import QFileDialog
from python_app.gui.runtime.history import record_result_history
from python_app.gui.runtime.history import record_result_history, remove_last_aligned_histories
class AppWindowSnapshotMixin:
@@ -90,16 +90,27 @@ class AppWindowSnapshotMixin:
except Exception as exc: # noqa: BLE001
self._show_error(f"Failed to save VNA history JSON: {exc}")
def _remove_last_runtime_history(self) -> None:
"""Remove the newest runtime measurement from all stages and processor replay state."""
self._apply_runtime_history_deletion(remove_last_only=True)
def _clear_all_runtime_history(self) -> None:
"""Clear all runtime histories, ring backlogs, and processor replay state."""
self._apply_runtime_history_deletion(remove_last_only=False)
def _apply_runtime_history_deletion(self, *, remove_last_only: bool) -> None:
"""Apply destructive runtime-history deletion across readers, caches, and processor replay state."""
if self._capture_session is not None:
self._show_error("Cannot clear runtime history during active capture sequence")
self._show_error("Cannot modify runtime history during active capture sequence")
return
resume_acquisition = self._supervisor.is_running()
dropped_raw = 0
dropped_pre = 0
dropped_results = 0
history_command = "remove_last" if remove_last_only else "clear_all"
action_label = "last runtime measurement removed" if remove_last_only else "runtime history fully cleared"
error_action = "remove last runtime measurement" if remove_last_only else "clear runtime history"
try:
if resume_acquisition:
@@ -109,23 +120,36 @@ class AppWindowSnapshotMixin:
dropped_pre = self._pre_reader.drop_all() if self._pre_reader is not None else 0
dropped_results = self._result_reader.drop_all() if self._result_reader is not None else 0
self._replace_runtime_history(retained_raw=[], retained_pre=[], retained_result=[])
if remove_last_only:
retained_raw, retained_pre, retained_result = remove_last_aligned_histories(
list(self._raw_history),
list(self._pre_history),
list(self._result_history),
)
else:
retained_raw = []
retained_pre = []
retained_result = []
self._replace_runtime_history(
retained_raw=retained_raw,
retained_pre=retained_pre,
retained_result=retained_result,
)
self._bscan_history_floor_collection_id = 0
self._clear_history_mode_caches()
# Clear processor-side replay cache so newly rendered B-scan starts clean.
self._write_live_processing_config(history_command="clear_all", bump_history_seq=True)
self._write_live_processing_config(history_command=history_command, bump_history_seq=True)
if self._supervisor.is_processor_running():
self._drain_results_until_quiet(timeout_s=0.25, poll_s=0.01)
self._drain_results_until_quiet(timeout_s=0.35, poll_s=0.01)
if self._result_reader is not None:
dropped_results += self._result_reader.drop_all()
self._result_history.clear()
self._update_history_indicator()
self._redraw_after_history_deletion()
self._log(
"Runtime history fully cleared: "
f"{action_label}: "
f"dropped raw={dropped_raw}, "
f"preprocessed={dropped_pre}, "
f"results={dropped_results}"
@@ -134,7 +158,7 @@ class AppWindowSnapshotMixin:
if resume_acquisition:
self._start_run()
except Exception as exc: # noqa: BLE001
self._show_error(f"Failed to clear runtime history: {exc}")
self._show_error(f"Failed to {error_action}: {exc}")
def _browse_save_path(self) -> None:
"""Open directory picker for snapshot output path."""
@@ -18,6 +18,8 @@ def build_data_actions_group(owner) -> QGroupBox:
save_button.clicked.connect(owner._save_snapshot)
save_vna_json_button = QPushButton("Save VNA History JSON")
save_vna_json_button.clicked.connect(owner._save_vna_history_json)
remove_last_button = QPushButton("Remove Last Runtime Measurement")
remove_last_button.clicked.connect(owner._remove_last_runtime_history)
clear_history_button = QPushButton("Clear ALL Runtime History")
clear_history_button.clicked.connect(owner._clear_all_runtime_history)
owner._save_count = QSpinBox()
@@ -35,6 +37,7 @@ def build_data_actions_group(owner) -> QGroupBox:
save_row.addWidget(save_button)
save_row.addWidget(save_vna_json_button)
save_row.addWidget(remove_last_button)
save_row.addWidget(clear_history_button)
save_row.addWidget(QLabel("Last N"))
save_row.addWidget(owner._save_count)
@@ -8,9 +8,7 @@ from PyQt6.QtWidgets import (
QDoubleSpinBox,
QFormLayout,
QGroupBox,
QHBoxLayout,
QLineEdit,
QPushButton,
QSizePolicy,
QSpinBox,
QStackedWidget,
@@ -138,24 +136,12 @@ def build_processing_group(owner) -> QGroupBox:
owner._bscan_stop_freq_mhz.setSingleStep(10.0)
owner._bscan_stop_freq_mhz.setValue(8800.0)
owner._bscan_clear_history_button = QPushButton("Clear B-scan History")
owner._bscan_clear_history_button.clicked.connect(owner._on_bscan_clear_history_clicked)
owner._bscan_remove_last_button = QPushButton("Remove Last Sweep")
owner._bscan_remove_last_button.clicked.connect(owner._on_bscan_remove_last_sweep_clicked)
bscan_actions = QWidget(owner._processing_mode_pages)
bscan_actions_layout = QHBoxLayout(bscan_actions)
bscan_actions_layout.setContentsMargins(0, 0, 0, 0)
bscan_actions_layout.setSpacing(8)
bscan_actions_layout.addWidget(owner._bscan_remove_last_button)
bscan_actions_layout.addWidget(owner._bscan_clear_history_button)
bscan_form.addRow("Axis", owner._bscan_axis)
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)
bscan_form.addRow("Start MHz", owner._bscan_start_freq_mhz)
bscan_form.addRow("Stop MHz", owner._bscan_stop_freq_mhz)
bscan_form.addRow(bscan_actions)
owner._processing_mode_pages.addWidget(bscan_page)
gpr_page = QWidget(owner._processing_mode_pages)
@@ -206,17 +192,6 @@ def build_processing_group(owner) -> QGroupBox:
owner._gpr_background_mean_count.setRange(0, 10_000)
owner._gpr_background_mean_count.setValue(10)
owner._gpr_clear_history_button = QPushButton("Clear GPR History")
owner._gpr_clear_history_button.clicked.connect(owner._on_gpr_clear_history_clicked)
owner._gpr_remove_last_button = QPushButton("Remove Last Measurement")
owner._gpr_remove_last_button.clicked.connect(owner._on_gpr_remove_last_measurement_clicked)
gpr_actions = QWidget(owner._processing_mode_pages)
gpr_actions_layout = QHBoxLayout(gpr_actions)
gpr_actions_layout.setContentsMargins(0, 0, 0, 0)
gpr_actions_layout.setSpacing(8)
gpr_actions_layout.addWidget(owner._gpr_remove_last_button)
gpr_actions_layout.addWidget(owner._gpr_clear_history_button)
gpr_form.addRow("Input positions", owner._gpr_input_positions_input)
gpr_form.addRow("Output positions", owner._gpr_output_positions_input)
gpr_form.addRow("Min depth m", owner._gpr_min_depth_m)
@@ -226,7 +201,6 @@ def build_processing_group(owner) -> QGroupBox:
gpr_form.addRow("Stop MHz", owner._gpr_stop_freq_mhz)
gpr_form.addRow(owner._gpr_background_subtract_enabled)
gpr_form.addRow("Mean count", owner._gpr_background_mean_count)
gpr_form.addRow(gpr_actions)
owner._processing_mode_pages.addWidget(gpr_page)
owner._processing_mode.currentTextChanged.connect(owner._on_processing_mode_changed)
+5 -2
View File
@@ -14,8 +14,8 @@ from python_app.models.dataset_model import (
)
from python_app.orchestration.shm.binary_cursor import ByteCursor
RAW_MAGIC = 0x31574152
PREPROC_MAGIC = 0x31525050
RAW_MAGIC = 0x32574152
PREPROC_MAGIC = 0x32525050
RESULT_MAGIC = 0x314C5352
@@ -40,6 +40,9 @@ def decode_trace_collection(payload: bytes, expected_magic: int) -> SweepCollect
freq = np.frombuffer(cursor.read_bytes(freq_bytes), dtype="<f4").astype(np.float32, copy=False)
interleaved_bytes = point_count * 8
# Runtime trace payloads now carry S11 before S21. The Python layer
# still works with S21 only for now, so consume and discard S11 here.
cursor.read_bytes(interleaved_bytes)
interleaved = np.frombuffer(cursor.read_bytes(interleaved_bytes), dtype="<f4")
s21 = (interleaved[0::2] + 1j * interleaved[1::2]).astype(np.complex64, copy=False)
+17 -9
View File
@@ -8,31 +8,39 @@ import numpy as np
from python_app.models.dataset_model import ResultCollection, SweepCollection
RAW_MAGIC = 0x31574152
PREPROC_MAGIC = 0x31525050
RAW_MAGIC = 0x32574152
PREPROC_MAGIC = 0x32525050
RESULT_MAGIC = 0x314C5352
def _write_interleaved_complex(buffer: bytearray, values: np.ndarray) -> None:
"""Append complex64 array as interleaved float32 real/imag pairs."""
interleaved = np.empty(values.size * 2, dtype="<f4")
interleaved[0::2] = values.real.astype("<f4", copy=False)
interleaved[1::2] = values.imag.astype("<f4", copy=False)
buffer.extend(interleaved.tobytes())
def serialize_trace_collection(collection: SweepCollection, magic: int) -> bytes:
"""Serialize one raw/preprocessed trace collection into ring-compatible binary format."""
buffer = bytearray()
buffer.extend(
struct.pack("<IQQI", magic, collection.collection_id, collection.monotonic_ns, len(collection.traces))
)
buffer.extend(struct.pack("<IQQI", magic, collection.collection_id, collection.monotonic_ns, len(collection.traces)))
for trace in collection.traces:
freq = np.asarray(trace.frequency_hz, dtype=np.float32)
s21 = np.asarray(trace.s21, dtype=np.complex64)
if freq.size != s21.size:
raise ValueError("Trace frequency and S21 sizes must match")
# Python workflows still operate on S21 only. Emit a zero-filled S11
# channel so C++ trace bundles keep the same wire format as runtime
# rings while the Python layer remains unchanged.
s11 = np.zeros(freq.size, dtype=np.complex64)
buffer.extend(struct.pack("<III", trace.combo.input_pos, trace.combo.output_pos, int(freq.size)))
buffer.extend(freq.astype("<f4", copy=False).tobytes())
interleaved = np.empty(freq.size * 2, dtype="<f4")
interleaved[0::2] = s21.real.astype("<f4", copy=False)
interleaved[1::2] = s21.imag.astype("<f4", copy=False)
buffer.extend(interleaved.tobytes())
_write_interleaved_complex(buffer, s11)
_write_interleaved_complex(buffer, s21)
return bytes(buffer)
-1
View File
@@ -46,7 +46,6 @@ class NpzStore(StoreApi):
suffix = f"i{trace.combo.input_pos}_o{trace.combo.output_pos}"
freq_key = f"freq_{suffix}"
s21_key = f"s21_{suffix}"
payload[freq_key] = np.asarray(trace.frequency_hz, dtype=np.float32)
payload[s21_key] = np.asarray(trace.s21, dtype=np.complex64)
combo_records.append(