removed the b-scan collection_id floor
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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])
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
Reference in New Issue
Block a user