removed the b-scan collection_id floor

This commit is contained in:
2026-08-03 18:59:34 +03:00
parent b183401e6f
commit 7c0ae1ecf8
9 changed files with 92 additions and 101 deletions
@@ -511,7 +511,6 @@ class AppWindowLiveProcessingMixin:
def _clear_history_mode_caches(self) -> None:
"""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()
@@ -689,7 +689,6 @@ class AppWindowPipelineMixin:
def _reset_runtime_history(self) -> None:
"""Reset runtime history and B-scan caches."""
self._replace_runtime_history(retained_raw=[], retained_pre=[], retained_result=[])
self._bscan_history_floor_collection_id = 0
self._discard_bscan_replay_results()
self._clear_history_mode_caches()
self._update_history_indicator()
@@ -16,17 +16,19 @@ def _result_tail(
*,
result_history: list[ResultCollection],
history_limit: int,
floor_collection_id: int,
) -> list[ResultCollection]:
"""Return filtered and de-duplicated result-history tail for B-scan usage."""
filtered = [
collection
for collection in result_history[-history_limit:]
if int(collection.collection_id) > int(floor_collection_id)
]
"""Return the de-duplicated newest `history_limit` entries for B-scan usage.
Selection is purely positional. An earlier version also dropped entries below a
`collection_id` floor, which cannot work here: ids are neither dense (the results
ring overwrites unread slots) nor monotonic across a run boundary (the C++ side
numbers from 1 again). Given the floor was derived from the first entry of this
very slice, the comparison provably removed nothing when ids ascend, and removed
exactly the newest frames when they do not.
"""
unique_reversed_tail: list[ResultCollection] = []
seen_keys: set[tuple[int, int]] = set()
for collection in reversed(filtered):
for collection in reversed(result_history[-history_limit:]):
key = (int(collection.collection_id), int(collection.monotonic_ns))
if key in seen_keys:
continue
@@ -43,13 +45,11 @@ def build_bscan_signature(
subtract_mean_ascan_enabled: bool,
result_history: list[ResultCollection],
history_limit: int,
floor_collection_id: int,
) -> tuple[object, ...]:
"""Build deterministic signature used to detect B-scan cache invalidation."""
result_tail = _result_tail(
result_history=result_history,
history_limit=history_limit,
floor_collection_id=floor_collection_id,
)
return (
int(history_limit),
@@ -61,7 +61,6 @@ def build_bscan_signature(
float(live_config.bscan_start_freq_mhz),
float(live_config.bscan_stop_freq_mhz),
bool(subtract_mean_ascan_enabled),
int(floor_collection_id),
tuple((int(collection.collection_id), int(collection.monotonic_ns), len(collection.blocks)) for collection in result_tail),
)
@@ -82,7 +81,6 @@ def apply_mean_ascan_subtraction(
def rebuild_bscan_history_from_results(
result_history: list[ResultCollection],
history_limit: int,
floor_collection_id: int,
stats: dict[str, object] | None = None,
) -> tuple[dict[tuple[int, int], deque[np.ndarray]], dict[tuple[int, int], np.ndarray]]:
"""Rebuild B-scan history and depth axes from processed result payloads.
@@ -98,7 +96,6 @@ def rebuild_bscan_history_from_results(
result_tail = _result_tail(
result_history=result_history,
history_limit=history_limit,
floor_collection_id=floor_collection_id,
)
axis_resets = 0
without_bscan = 0
@@ -305,7 +302,6 @@ class AppWindowBscanPlotMixin:
def _sync_bscan_history_from_results(self) -> None:
"""Rebuild B-scan history cache when live params or inputs changed."""
self._advance_bscan_floor_to_display_window()
signature = self._bscan_signature()
if signature == self._bscan_render_signature:
return
@@ -317,13 +313,12 @@ class AppWindowBscanPlotMixin:
live_config = self._live_processing_config()
# Must read the same source the rebuild will, or the cache decides nothing
# changed while the image would in fact be built from different collections.
result_history, from_replay = self._bscan_source_collections()
result_history, _from_replay = self._bscan_source_collections()
return build_bscan_signature(
live_config=live_config,
subtract_mean_ascan_enabled=bool(self._bscan_subtract_mean_ascan.isChecked()),
result_history=result_history,
history_limit=self._bscan_display_window_scans(),
floor_collection_id=0 if from_replay else self._bscan_history_floor_collection_id,
)
def _bscan_source_collections(self) -> tuple[list[ResultCollection], bool]:
@@ -332,7 +327,7 @@ class AppWindowBscanPlotMixin:
A completed replay is the better source: it is exactly `window` long and every
entry went through the processor with the same settings. The runtime history is
not usable right after one, because results whose ids it never held are appended
out of order and the positional floor then cuts them away again.
out of order, leaving the deque unsorted for the rest of the session.
"""
replayed = getattr(self, "_bscan_replay_results", None)
if replayed:
@@ -343,20 +338,16 @@ class AppWindowBscanPlotMixin:
"""Recompute B-scan history cache from results history buffer."""
result_history, from_replay = self._bscan_source_collections()
window = self._bscan_display_window_scans()
# A replay already selected the window; re-applying the floor would cut it again.
floor_collection_id = 0 if from_replay else int(self._bscan_history_floor_collection_id)
stats: dict[str, object] = {}
history_by_combo, depth_axis_by_combo = rebuild_bscan_history_from_results(
result_history=result_history,
history_limit=window,
floor_collection_id=floor_collection_id,
stats=stats,
)
self._bscan_history_by_combo = history_by_combo
self._bscan_depth_axis_by_combo = depth_axis_by_combo
self._log_bscan_window_shortfall(
window=window,
floor_collection_id=floor_collection_id,
result_history_len=len(result_history),
from_replay=from_replay,
history_by_combo=history_by_combo,
@@ -367,7 +358,6 @@ class AppWindowBscanPlotMixin:
self,
*,
window: int,
floor_collection_id: int,
result_history_len: int,
from_replay: bool,
history_by_combo: dict[tuple[int, int], deque[np.ndarray]],
@@ -375,9 +365,9 @@ class AppWindowBscanPlotMixin:
) -> None:
"""Explain at DEBUG why the image holds fewer columns than were requested.
Four independent causes produce the same symptom, so each is reported as its
Three independent causes produce the same symptom, so each is reported as its
own number rather than a single verdict:
* `result history` / `tail` the run simply produced fewer sweeps;
* `tail` the run simply produced fewer sweeps than were asked for;
* `without_bscan` frames processed in another mode carry no bscan payload;
* `axis_resets` a changed depth axis restarted the deque, dropping every
column gathered before it (the usual cause after a partial replay);
@@ -394,7 +384,7 @@ class AppWindowBscanPlotMixin:
self._log_debug(
f"B-scan window not filled: rendered={rendered} of requested={window}. "
f"source={'replay' if from_replay else 'result history'} len={result_history_len}, "
f"above floor(id>{floor_collection_id})={stats.get('tail')}, "
f"newest-{window} tail={stats.get('tail')}, "
f"of those without a bscan payload={stats.get('without_bscan')}, "
f"depth-axis resets={stats.get('axis_resets')}. "
f"Per combo: {per_combo or 'none'}."
@@ -483,36 +473,6 @@ class AppWindowBscanPlotMixin:
self._bscan_depth_axis_by_combo.clear()
self._bscan_render_signature = None
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
window = self._bscan_display_window_scans()
if len(history) <= window:
self._bscan_history_floor_collection_id = 0
return
# `_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."""
plot_item = self._bscan_plot.getPlotItem()
@@ -128,9 +128,9 @@ class AppWindowBscanReplayMixin:
# The two histories are fed by different rings that drop independently, so the
# sweeps we re-sent only partly overlap the results already on record. The
# non-overlapping ones get appended to the deque even though their ids are old,
# which breaks its ordering — and the positional floor then cuts exactly those
# off again. Observed: 300 re-sent, 300 recovered, 145 drawn. What came back is
# by construction the right count and uniformly processed, so use it directly.
# so its newest `window` entries are a mix of freshly and stale-processed
# frames. What came back is by construction the right count and uniformly
# processed, so use it directly.
self._bscan_replay_results = replayed
# Replayed collections keep their original ids and timestamps, so the render
@@ -241,7 +241,6 @@ class AppWindowSnapshotMixin:
retained_pre=retained_pre,
retained_result=retained_result,
)
self._bscan_history_floor_collection_id = 0
self._discard_bscan_replay_results()
self._clear_history_mode_caches()