|
|
|
@@ -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()
|
|
|
|
|