added a bscan extension
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
"""Configurable B-scan display window.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
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.models.dataset_model import ( # noqa: E402
|
||||
ComboKey,
|
||||
ResultBlock,
|
||||
ResultCollection,
|
||||
ResultPayload,
|
||||
)
|
||||
|
||||
_app: QApplication | None = None
|
||||
_window: AppWindow | None = None
|
||||
|
||||
|
||||
def setUpModule() -> None:
|
||||
global _app, _window
|
||||
_app = QApplication.instance() or QApplication([])
|
||||
_window = AppWindow(Path("."))
|
||||
|
||||
|
||||
def tearDownModule() -> None:
|
||||
if _window is not None:
|
||||
_window.close()
|
||||
|
||||
|
||||
def _bscan_result(collection_id: int) -> ResultCollection:
|
||||
payload = ResultPayload(
|
||||
processing_name="bscan",
|
||||
kind=1,
|
||||
frequency_hz=np.array([0.5, 1.0], dtype=np.float32),
|
||||
trace=np.array([collection_id + 0j, collection_id + 0j], dtype=np.complex64),
|
||||
)
|
||||
block = ResultBlock(combo=ComboKey(input=0, output=0), payloads=[payload])
|
||||
return ResultCollection(collection_id=collection_id, monotonic_ns=collection_id, blocks=[block])
|
||||
|
||||
|
||||
class BscanDisplayWindowTest(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
def _set_running(self, running: bool) -> None:
|
||||
self.w._supervisor.is_running = lambda: running
|
||||
|
||||
def _fill_history(self, count: int, *, id_step: int = 1) -> None:
|
||||
for index in range(count):
|
||||
self.w._result_history.append(_bscan_result(1 + index * id_step))
|
||||
|
||||
def test_running_acquisition_ignores_the_user_window(self) -> None:
|
||||
# A live rebuild runs on every incoming result, so the live path stays pinned
|
||||
# to the replay window no matter what the operator typed for stopped review.
|
||||
self._set_running(True)
|
||||
self.w._bscan_history_window.setValue(300)
|
||||
self.assertEqual(self.w._bscan_display_window_scans(), self.w._bscan_cpp_replay_window)
|
||||
|
||||
def test_stopped_acquisition_uses_the_user_window(self) -> None:
|
||||
self._set_running(False)
|
||||
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.
|
||||
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._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)
|
||||
|
||||
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.
|
||||
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.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)
|
||||
|
||||
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.
|
||||
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_rebuild_renders_the_full_widened_window(self) -> None:
|
||||
self._fill_history(300)
|
||||
self._set_running(False)
|
||||
self.w._bscan_history_window.setValue(300)
|
||||
self.w._sync_bscan_history_from_results()
|
||||
self.assertEqual(len(self.w._bscan_history_by_combo[(0, 0)]), 300)
|
||||
|
||||
self.w._bscan_history_window.setValue(50)
|
||||
self.w._sync_bscan_history_from_results()
|
||||
self.assertEqual(len(self.w._bscan_history_by_combo[(0, 0)]), 50)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user