added a bscan extension

This commit is contained in:
2026-07-31 15:46:08 +03:00
parent 7c6cab07fc
commit d61b59b9a4
8 changed files with 270 additions and 36 deletions
@@ -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."""