added s11 collections
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user