added a bscan extension
This commit is contained in:
@@ -121,15 +121,15 @@ class AppWindowConfigProfileIOMixin:
|
||||
|
||||
Save-side deques use a config-independent limit so that processing-side
|
||||
ring capacities can stay small without truncating the save buffer. The
|
||||
B-scan render limit still follows ring capacities to keep plot updates
|
||||
responsive.
|
||||
C++ replay window still follows ring capacities, since it bounds how much
|
||||
of the history the processor can re-publish coherently.
|
||||
"""
|
||||
save_history_limit = self._save_history_limit_for_config(config)
|
||||
bscan_history_limit = self._history_limit_for_config(config)
|
||||
bscan_cpp_replay_window = self._cpp_bscan_replay_window_for_config(config)
|
||||
self._raw_history = deque(self._raw_history, maxlen=save_history_limit)
|
||||
self._pre_history = deque(self._pre_history, maxlen=save_history_limit)
|
||||
self._result_history = deque(self._result_history, maxlen=save_history_limit)
|
||||
self._bscan_history_limit = bscan_history_limit
|
||||
self._bscan_cpp_replay_window = bscan_cpp_replay_window
|
||||
self._clear_bscan_plot_history()
|
||||
|
||||
def _save_current_config(self) -> None:
|
||||
@@ -283,6 +283,7 @@ class AppWindowConfigProfileIOMixin:
|
||||
self._bscan_start_freq_mhz,
|
||||
self._bscan_stop_freq_mhz,
|
||||
self._bscan_subtract_mean_ascan,
|
||||
self._bscan_history_window,
|
||||
self._gpr_relative_permittivity,
|
||||
self._gpr_tx_geometry_input,
|
||||
self._gpr_rx_geometry_input,
|
||||
@@ -438,6 +439,7 @@ class AppWindowConfigProfileIOMixin:
|
||||
self._bscan_start_freq_mhz.setValue(float(gui_state.processing.bscan.start_freq_mhz))
|
||||
self._bscan_stop_freq_mhz.setValue(float(gui_state.processing.bscan.stop_freq_mhz))
|
||||
self._bscan_subtract_mean_ascan.setChecked(bool(gui_state.processing.bscan.subtract_mean_ascan))
|
||||
self._bscan_history_window.setValue(int(gui_state.processing.bscan.history_window_scans))
|
||||
|
||||
self._gpr_relative_permittivity.setValue(float(config.gpr.relative_permittivity))
|
||||
self._gpr_tx_geometry_input.setPlainText(
|
||||
|
||||
@@ -34,6 +34,13 @@ from python_app.storage.npz_store import radar_key_from_config
|
||||
# history without touching the processing-side ring sizes.
|
||||
GUI_SAVE_HISTORY_LIMIT: int = 1000
|
||||
|
||||
# Mirror of `kBscanReplayWindow` in
|
||||
# data_acq_and_processing/processing/data_processor/src/data_processor.cpp. When a
|
||||
# live B-scan setting changes, the C++ processor re-processes and re-publishes only
|
||||
# this many of the newest collections; anything older keeps the payload it was first
|
||||
# computed with. Keep the two constants in sync.
|
||||
CPP_BSCAN_REPLAY_WINDOW: int = 50
|
||||
|
||||
|
||||
class AppWindowConfigStateBuildersMixin:
|
||||
"""Build stable and GUI-only config models from current widget state."""
|
||||
@@ -163,14 +170,24 @@ class AppWindowConfigStateBuildersMixin:
|
||||
return (min(x_values) - margin_m, max(x_values) + margin_m)
|
||||
|
||||
@staticmethod
|
||||
def _history_limit_for_config(config: RunConfigModel) -> int:
|
||||
"""Return B-scan render history limit derived from config ring capacities."""
|
||||
def _cpp_bscan_replay_window_for_config(config: RunConfigModel) -> int:
|
||||
"""Return how many newest collections the C++ processor re-processes on a
|
||||
live B-scan settings change.
|
||||
|
||||
Deliberately reproduces `replay_history_limit()` in `data_processor.cpp`
|
||||
formula-for-formula. The ring capacities matter because `ShmRing` overwrites
|
||||
the oldest unread slot on overflow, so a replay burst must fit in the results
|
||||
ring for the GUI to receive all of it.
|
||||
|
||||
This is the single place to change if the replay window ever becomes
|
||||
configurable on the C++ side.
|
||||
"""
|
||||
return max(
|
||||
1,
|
||||
min(
|
||||
int(config.rings.raw_tap.capacity),
|
||||
int(config.rings.preprocessed_tap.capacity),
|
||||
int(config.rings.preprocessed.capacity),
|
||||
int(config.rings.results.capacity),
|
||||
CPP_BSCAN_REPLAY_WINDOW,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -180,7 +197,7 @@ class AppWindowConfigStateBuildersMixin:
|
||||
|
||||
Independent of ring capacities — see :data:`GUI_SAVE_HISTORY_LIMIT`.
|
||||
The `config` argument is kept for symmetry with
|
||||
:meth:`_history_limit_for_config` and possible future per-profile
|
||||
:meth:`_cpp_bscan_replay_window_for_config` and possible future per-profile
|
||||
overrides.
|
||||
"""
|
||||
del config
|
||||
@@ -344,6 +361,7 @@ class AppWindowConfigStateBuildersMixin:
|
||||
start_freq_mhz=float(self._bscan_start_freq_mhz.value()),
|
||||
stop_freq_mhz=float(self._bscan_stop_freq_mhz.value()),
|
||||
subtract_mean_ascan=bool(self._bscan_subtract_mean_ascan.isChecked()),
|
||||
history_window_scans=int(self._bscan_history_window.value()),
|
||||
),
|
||||
gpr=GuiGprStateModel(
|
||||
input_positions=self._gpr_input_positions_input.text().strip(),
|
||||
|
||||
@@ -52,6 +52,7 @@ def build_bscan_signature(
|
||||
floor_collection_id=floor_collection_id,
|
||||
)
|
||||
return (
|
||||
int(history_limit),
|
||||
str(live_config.bscan_axis),
|
||||
str(live_config.bscan_channel),
|
||||
float(live_config.bscan_cut_m),
|
||||
@@ -213,6 +214,7 @@ class AppWindowBscanPlotMixin:
|
||||
depth_max = float(np.max(depth_axis))
|
||||
depth_span = max(depth_max - depth_min, 1e-6)
|
||||
sweep_count = sweeps.shape[0]
|
||||
self._warn_if_bscan_exceeds_replay_window(sweep_count)
|
||||
sweep_width = float(max(sweep_count, 1))
|
||||
x_min = 0.5
|
||||
x_max = x_min + sweep_width
|
||||
@@ -236,9 +238,52 @@ class AppWindowBscanPlotMixin:
|
||||
)
|
||||
return True
|
||||
|
||||
def _warn_if_bscan_exceeds_replay_window(self, sweep_count: int) -> None:
|
||||
"""Warn once when the image reaches past what the C++ processor can replay.
|
||||
|
||||
Beyond that window a frame keeps the payload it was first computed with, so
|
||||
editing Gain / Cut / Max depth / Start-Stop MHz silently leaves the older
|
||||
columns on their previous settings — the image mixes two parameter sets.
|
||||
"""
|
||||
replay_window = int(self._bscan_cpp_replay_window)
|
||||
if sweep_count <= replay_window:
|
||||
return
|
||||
|
||||
stale_count = sweep_count - replay_window
|
||||
self._log_warning(
|
||||
f"B-scan shows {sweep_count} sweeps but the processor replays only the newest "
|
||||
f"{replay_window}; the older {stale_count} keep the settings they were captured with.",
|
||||
details=(
|
||||
"Changing Gain / Cut m / Max depth m / Start MHz / Stop MHz re-processes "
|
||||
f"only the newest {replay_window} sweeps.\n"
|
||||
f"Reduce 'Scans to show (stopped)' to {replay_window} for an image that is "
|
||||
"coherent across every column."
|
||||
),
|
||||
# Keyed on the operator-controlled window, not the live column count, so
|
||||
# that repeated "Remove Last" in stopped mode does not re-warn every click.
|
||||
once_key=(
|
||||
f"bscan_window_exceeds_replay_{replay_window}_"
|
||||
f"{self._bscan_display_window_scans()}"
|
||||
),
|
||||
)
|
||||
|
||||
def _bscan_display_window_scans(self) -> int:
|
||||
"""Return how many past sweeps the B-scan should render right now.
|
||||
|
||||
While acquisition runs the window stays at the C++ replay window: results
|
||||
arrive continuously, the whole history is rebuilt on every new one, and a
|
||||
1000-wide rebuild on the live path would cost ~20x per frame.
|
||||
|
||||
Once stopped, the operator reviews a frozen history, so the user-configured
|
||||
window applies and may reach back over the whole GUI result deque.
|
||||
"""
|
||||
if self._supervisor.is_running():
|
||||
return int(self._bscan_cpp_replay_window)
|
||||
return max(1, int(self._bscan_history_window.value()))
|
||||
|
||||
def _sync_bscan_history_from_results(self) -> None:
|
||||
"""Rebuild B-scan history cache when live params or inputs changed."""
|
||||
self._advance_bscan_floor_to_cpp_window()
|
||||
self._advance_bscan_floor_to_display_window()
|
||||
signature = self._bscan_signature()
|
||||
if signature == self._bscan_render_signature:
|
||||
return
|
||||
@@ -253,7 +298,7 @@ class AppWindowBscanPlotMixin:
|
||||
live_config=live_config,
|
||||
subtract_mean_ascan_enabled=bool(self._bscan_subtract_mean_ascan.isChecked()),
|
||||
result_history=result_history,
|
||||
history_limit=self._bscan_history_limit,
|
||||
history_limit=self._bscan_display_window_scans(),
|
||||
floor_collection_id=self._bscan_history_floor_collection_id,
|
||||
)
|
||||
|
||||
@@ -262,7 +307,7 @@ class AppWindowBscanPlotMixin:
|
||||
result_history = list(self._result_history)
|
||||
history_by_combo, depth_axis_by_combo = rebuild_bscan_history_from_results(
|
||||
result_history=result_history,
|
||||
history_limit=self._bscan_history_limit,
|
||||
history_limit=self._bscan_display_window_scans(),
|
||||
floor_collection_id=self._bscan_history_floor_collection_id,
|
||||
)
|
||||
self._bscan_history_by_combo = history_by_combo
|
||||
@@ -351,29 +396,35 @@ class AppWindowBscanPlotMixin:
|
||||
self._bscan_depth_axis_by_combo.clear()
|
||||
self._bscan_render_signature = None
|
||||
|
||||
def _advance_bscan_floor_to_cpp_window(self) -> None:
|
||||
"""Clamp B-scan source history to C++ available replay window."""
|
||||
if not self._result_history:
|
||||
def _advance_bscan_floor_to_display_window(self) -> None:
|
||||
"""Clamp B-scan source history to the active display window.
|
||||
|
||||
The window counts RETAINED ENTRIES, so the floor is read off the n-th
|
||||
newest entry rather than computed as `latest_id - window`. Collection ids
|
||||
are not dense: the results ring overwrites unread slots when the producer
|
||||
outruns the GUI poll loop, so the GUI keeps ids like 1..50, 81..130, ...
|
||||
Subtracting the window from the newest id would then span far fewer than
|
||||
`window` entries — asking for 150 sweeps yielded 87.
|
||||
|
||||
Recomputed unconditionally rather than ratcheted upwards: widening the
|
||||
window in stopped mode must be able to LOWER the floor and bring older
|
||||
frames back into view. A stale floor cannot survive this way either, so
|
||||
the previous special case for collection ids restarting on a new C++ run
|
||||
is no longer needed.
|
||||
"""
|
||||
history = self._result_history
|
||||
if not history:
|
||||
return
|
||||
|
||||
cpp_window_limit = min(
|
||||
int(self._defaults_config.rings.preprocessed.capacity),
|
||||
int(self._defaults_config.rings.results.capacity),
|
||||
)
|
||||
cpp_window_limit = max(1, cpp_window_limit)
|
||||
latest_collection_id = int(self._result_history[-1].collection_id)
|
||||
current_floor = int(self._bscan_history_floor_collection_id)
|
||||
|
||||
# Collection ids restart from 1 on new C++ run; release floor only while
|
||||
# acquisition is running, so manual "remove last" behavior in stopped mode
|
||||
# remains deterministic.
|
||||
if latest_collection_id < current_floor and self._supervisor.is_running():
|
||||
window = self._bscan_display_window_scans()
|
||||
if len(history) <= window:
|
||||
self._bscan_history_floor_collection_id = 0
|
||||
current_floor = 0
|
||||
return
|
||||
|
||||
floor_candidate = max(0, latest_collection_id - cpp_window_limit)
|
||||
if floor_candidate > current_floor:
|
||||
self._bscan_history_floor_collection_id = floor_candidate
|
||||
# `_result_tail` keeps entries with `collection_id > floor`, so sit the
|
||||
# floor one below the oldest entry that still fits in the window.
|
||||
oldest_visible = history[len(history) - window]
|
||||
self._bscan_history_floor_collection_id = max(0, int(oldest_visible.collection_id) - 1)
|
||||
|
||||
def _ensure_phase_view_box(self) -> pg.ViewBox:
|
||||
"""Create or return secondary right-axis ViewBox for phase curves."""
|
||||
|
||||
@@ -18,6 +18,7 @@ from PyQt6.QtWidgets import (
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from python_app.gui.controllers.app_window_config.state_builders import GUI_SAVE_HISTORY_LIMIT
|
||||
from python_app.gui.controllers.sections.layout_helpers import FormRow, build_two_column_form_widget
|
||||
|
||||
|
||||
@@ -160,6 +161,15 @@ def build_processing_group(owner) -> QGroupBox:
|
||||
owner._bscan_subtract_mean_ascan = QCheckBox("Subtract mean A-scan")
|
||||
owner._bscan_subtract_mean_ascan.setChecked(bool(bscan_defaults.subtract_mean_ascan))
|
||||
|
||||
owner._bscan_history_window = QSpinBox()
|
||||
owner._bscan_history_window.setMinimum(1)
|
||||
owner._bscan_history_window.setMaximum(GUI_SAVE_HISTORY_LIMIT)
|
||||
owner._bscan_history_window.setValue(int(bscan_defaults.history_window_scans))
|
||||
owner._bscan_history_window.setToolTip(
|
||||
"How many past sweeps the B-scan shows once acquisition is stopped. While "
|
||||
"running, the window stays clamped to the C++ ring capacity."
|
||||
)
|
||||
|
||||
bscan_page = _build_processing_mode_page(
|
||||
owner._processing_mode_pages,
|
||||
[
|
||||
@@ -169,6 +179,7 @@ def build_processing_group(owner) -> QGroupBox:
|
||||
("Gain", owner._bscan_gain),
|
||||
("Start MHz", owner._bscan_start_freq_mhz),
|
||||
("Stop MHz", owner._bscan_stop_freq_mhz),
|
||||
("Scans to show (stopped)", owner._bscan_history_window),
|
||||
owner._bscan_subtract_mean_ascan,
|
||||
],
|
||||
split_index=4,
|
||||
@@ -569,6 +580,7 @@ def build_processing_group(owner) -> QGroupBox:
|
||||
owner._bscan_start_freq_mhz.valueChanged.connect(owner._on_processing_live_settings_changed)
|
||||
owner._bscan_stop_freq_mhz.valueChanged.connect(owner._on_processing_live_settings_changed)
|
||||
owner._bscan_subtract_mean_ascan.toggled.connect(owner._on_processing_live_settings_changed)
|
||||
owner._bscan_history_window.valueChanged.connect(owner._on_processing_live_settings_changed)
|
||||
owner._gpr_input_positions_input.editingFinished.connect(owner._on_processing_live_settings_changed)
|
||||
owner._gpr_output_positions_input.editingFinished.connect(owner._on_processing_live_settings_changed)
|
||||
owner._gpr_min_depth_m.valueChanged.connect(owner._on_processing_live_settings_changed)
|
||||
|
||||
Reference in New Issue
Block a user