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
+59 -28
View File
@@ -2,12 +2,13 @@
The B-scan used to be pinned to the C++ replay window (~50 sweeps) by two separate
mechanisms: the render-side history limit and a monotonically rising
`_bscan_history_floor_collection_id`. Widening only the first would have changed
nothing, because the floor kept filtering older collections out for good.
`collection_id` floor. Widening only the first would have changed nothing, because
the floor kept filtering older collections out for good.
These tests pin the two properties that make the stopped-mode review work: the
window follows acquisition state, and the floor is recomputed (not ratcheted) so a
widened window can bring already-discarded frames back into view.
The floor is gone: selection is positional, which is the only criterion that holds
when ids are sparse (the results ring drops) or restart from 1 (a new C++ run).
These tests pin the observable consequences — how many columns end up on screen —
rather than any internal counter.
"""
from __future__ import annotations
@@ -22,6 +23,7 @@ os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from PyQt6.QtWidgets import QApplication # noqa: E402
from python_app.gui.app_window import AppWindow # noqa: E402
from python_app.gui.runtime.history import record_result_history # noqa: E402
from python_app.models.dataset_model import ( # noqa: E402
ComboKey,
ResultBlock,
@@ -60,12 +62,12 @@ class BscanDisplayWindowTest(unittest.TestCase):
self.w = _window
self._original_is_running = self.w._supervisor.is_running
self.w._result_history.clear()
self.w._bscan_history_floor_collection_id = 0
self.w._bscan_render_signature = None
def tearDown(self) -> None:
self.w._supervisor.is_running = self._original_is_running
self.w._result_history.clear()
self.w._bscan_history_floor_collection_id = 0
self.w._bscan_render_signature = None
def _set_running(self, running: bool) -> None:
self.w._supervisor.is_running = lambda: running
@@ -86,44 +88,73 @@ class BscanDisplayWindowTest(unittest.TestCase):
self.w._bscan_history_window.setValue(300)
self.assertEqual(self.w._bscan_display_window_scans(), 300)
def test_widening_the_window_lowers_the_floor(self) -> None:
# The regression this whole change exists for: a run leaves the floor high,
# and widening the window afterwards must pull it back down.
def test_widening_the_window_brings_older_frames_back(self) -> None:
# The regression this whole change exists for: a live run renders 50 columns,
# and widening the window after Stop must reach back over the retained history
# rather than stay pinned to whatever the live path last drew.
self._fill_history(300)
self._set_running(True)
self.w._advance_bscan_floor_to_display_window()
raised_floor = self.w._bscan_history_floor_collection_id
self.assertEqual(raised_floor, 300 - self.w._bscan_cpp_replay_window)
self.w._sync_bscan_history_from_results()
self.assertEqual(
len(self.w._bscan_history_by_combo[(0, 0)]), self.w._bscan_cpp_replay_window
)
self._set_running(False)
self.w._bscan_history_window.setValue(300)
self.w._advance_bscan_floor_to_display_window()
self.assertEqual(self.w._bscan_history_floor_collection_id, 0)
self.w._sync_bscan_history_from_results()
self.assertEqual(len(self.w._bscan_history_by_combo[(0, 0)]), 300)
def test_floor_survives_collection_ids_restarting(self) -> None:
# A new C++ run restarts ids from 1. The unconditional recompute must not
# leave a stale high floor that hides the whole fresh run.
def test_restarted_collection_ids_do_not_hide_the_fresh_run(self) -> None:
# Regression: Start after a stopped review made the image melt to 0 columns and
# then snap back to 50. A new C++ run numbers from 1, so entries that are newest
# by position carry the smallest ids; any id-based cut-off derived from the old
# run rejected exactly them.
self._fill_history(300)
self._set_running(True)
self.w._advance_bscan_floor_to_display_window()
self.assertGreater(self.w._bscan_history_floor_collection_id, 0)
self._set_running(False)
self.w._bscan_history_window.setValue(300)
self.w._sync_bscan_history_from_results()
self.w._result_history.clear()
self._fill_history(5)
self.w._advance_bscan_floor_to_display_window()
self.assertEqual(self.w._bscan_history_floor_collection_id, 0)
self._set_running(True)
window = self.w._bscan_cpp_replay_window
for fresh in range(1, window + 1):
self.w._result_history.append(_bscan_result(fresh))
self.w._sync_bscan_history_from_results()
self.assertEqual(len(self.w._bscan_history_by_combo[(0, 0)]), window)
def test_window_counts_entries_not_collection_ids(self) -> None:
# The results ring overwrites unread slots when the producer outruns the GUI
# poll loop, so retained collection ids are sparse. A floor of
# `latest_id - window` then spans far fewer than `window` entries: this is
# exactly the case where asking for 150 sweeps rendered only 87.
# poll loop, so retained collection ids are sparse. Counting in ids rather than
# in entries is exactly the case where asking for 150 sweeps rendered only 87.
self._fill_history(300, id_step=3)
self._set_running(False)
self.w._bscan_history_window.setValue(150)
self.w._sync_bscan_history_from_results()
self.assertEqual(len(self.w._bscan_history_by_combo[(0, 0)]), 150)
def test_reprocessed_results_replace_rather_than_duplicate_columns(self) -> None:
# The processor's recompute is triggered by feeding sweeps back through it, and
# it republishes them under their ORIGINAL ids. Two independent mechanisms keep
# that from doubling every column, and neither may be confused with the removed
# `collection_id` floor: a threshold cannot tell a duplicate from its original,
# since they share the id. Deduplication is by key equality.
self._set_running(False)
self.w._bscan_history_window.setValue(300)
self._fill_history(200)
self.w._sync_bscan_history_from_results()
self.assertEqual(len(self.w._bscan_history_by_combo[(0, 0)]), 200)
# Intake: a recomputed collection replaces the entry holding the same key.
for collection in list(self.w._result_history):
record_result_history(self.w._result_history, _bscan_result(collection.collection_id))
self.assertEqual(len(self.w._result_history), 200)
# Render: a duplicate that reached the deque by another path is still collapsed,
# keeping the newer of the two.
self.w._result_history.append(_bscan_result(200))
self.w._bscan_render_signature = None
self.w._sync_bscan_history_from_results()
self.assertEqual(len(self.w._bscan_history_by_combo[(0, 0)]), 200)
def test_rebuild_renders_the_full_widened_window(self) -> None:
self._fill_history(300)
self._set_running(False)
+3 -3
View File
@@ -225,9 +225,9 @@ class ReplayGuardTest(unittest.TestCase):
`_pre_history` and `_result_history` come from two rings that drop
independently, so the re-sent sweeps only partly overlap the recorded results.
The non-overlapping ones get appended to the deque under old ids, breaking its
ordering, and the positional floor then cuts exactly those away. Rendering from
the replayed set instead sidesteps the whole problem.
The non-overlapping ones get appended to the deque under old ids, leaving its
newest `window` entries a mix of freshly and stale-processed frames. Rendering
from the replayed set instead sidesteps the whole problem.
"""
window = 30
replayed = [_bscan_result(cid) for cid in range(9000, 9000 + window)]
+12 -8
View File
@@ -219,7 +219,7 @@ class RebuildBscanHistoryTest(unittest.TestCase):
self._bscan_collection(1, (0, 0), [1.0, 2.0], [10.0, 20.0]),
self._bscan_collection(2, (0, 0), [1.0, 2.0], [11.0, 21.0]),
]
by_combo, axes = rebuild_bscan_history_from_results(history, history_limit=10, floor_collection_id=0)
by_combo, axes = rebuild_bscan_history_from_results(history, history_limit=10)
self.assertEqual(len(by_combo[(0, 0)]), 2)
self.assertTrue(np.array_equal(axes[(0, 0)], np.array([1.0, 2.0], dtype=np.float32)))
@@ -229,7 +229,7 @@ class RebuildBscanHistoryTest(unittest.TestCase):
self._bscan_collection(2, (0, 0), [1.0, 2.0], [1.0, 2.0], kind=2), # wrong kind
self._bscan_collection(3, (0, 0), [1.0, 2.0, 3.0], [1.0, 2.0]), # size mismatch
]
by_combo, _ = rebuild_bscan_history_from_results(history, history_limit=10, floor_collection_id=0)
by_combo, _ = rebuild_bscan_history_from_results(history, history_limit=10)
self.assertEqual(by_combo, {})
def test_depth_axis_change_resets_history(self) -> None:
@@ -237,17 +237,21 @@ class RebuildBscanHistoryTest(unittest.TestCase):
self._bscan_collection(1, (0, 0), [1.0, 2.0], [10.0, 20.0]),
self._bscan_collection(2, (0, 0), [1.0, 2.0, 3.0], [11.0, 21.0, 31.0]), # new depth axis
]
by_combo, axes = rebuild_bscan_history_from_results(history, history_limit=10, floor_collection_id=0)
by_combo, axes = rebuild_bscan_history_from_results(history, history_limit=10)
self.assertEqual(len(by_combo[(0, 0)]), 1) # reset on axis change; only the latest sweep remains
self.assertEqual(axes[(0, 0)].shape, (3,))
def test_floor_collection_id_excludes_older(self) -> None:
def test_selection_is_positional_not_by_collection_id(self) -> None:
# Ids are neither dense nor monotonic across a run boundary, so the tail is
# taken by position only. Here the newest two entries carry the SMALLEST ids;
# an id-based cut-off would have dropped exactly them.
history = [
self._bscan_collection(1, (0, 0), [1.0], [10.0]),
self._bscan_collection(2, (0, 0), [1.0], [20.0]),
self._bscan_collection(cid, (0, 0), [1.0], [float(cid)])
for cid in (98, 99, 100, 1, 2)
]
by_combo, _ = rebuild_bscan_history_from_results(history, history_limit=10, floor_collection_id=1)
self.assertEqual(len(by_combo[(0, 0)]), 1) # only collection_id > 1
by_combo, _ = rebuild_bscan_history_from_results(history, history_limit=3)
self.assertEqual(len(by_combo[(0, 0)]), 3)
self.assertEqual([sweep[0] for sweep in by_combo[(0, 0)]], [100.0, 1.0, 2.0])
# --------------------------------------------------------------------------- #