fix graph and white background added

This commit is contained in:
Ayzen
2026-10-15 14:46:06 +03:00
parent 5102ed751c
commit 9a9b8ce5c7
35 changed files with 730 additions and 463 deletions
+146 -34
View File
@@ -1,16 +1,54 @@
from contextlib import ExitStack
"""Plot capture follows client demand and actual Qt scene changes."""
"""Plot capture follows client demand and plot changes; redraws leave no stale pixels."""
from collections import deque
from contextlib import ExitStack
import unittest
from unittest import mock
import numpy as np
from PyQt6.QtCore import QSignalBlocker
from PyQt6.QtWidgets import QApplication, QComboBox
import pyqtgraph as pg
from PyQt6.QtCore import QPoint, QSignalBlocker
from PyQt6.QtGui import QImage
from PyQt6.QtWidgets import QApplication, QCheckBox
from python_app.tests.gui_test_support import isolated_window
from python_app.gui.controllers.renderers.sar3d import SAR3D_VIEWS
from python_app.models.dataset_model import ComboKey, ResultBlock, ResultCollection, ResultPayload, TraceData
from python_app.tests.gui_test_support import isolated_window, widget
def _phase_result(phase_deg: np.ndarray) -> ResultCollection:
trace = (0.5 * np.exp(1j * np.radians(phase_deg))).astype(np.complex64)
payload = ResultPayload("pass_through", 1, frequency_hz=np.linspace(1e9, 2e9, phase_deg.size), trace=trace)
return ResultCollection(1, 1, blocks=[ResultBlock(ComboKey(0, 0), [payload])])
def _gpr_result(revision: int, point: tuple[float, float], size: int) -> ResultCollection:
image = np.linspace(0, 1, size * size, dtype=np.float32).reshape(size, size)
return ResultCollection(1, 1, settings_revision=revision, collection_payloads=[
ResultPayload("gpr_accumulator", 3, image=image,
image_x_axis=np.linspace(-1.5, 1.5, size), image_y_axis=np.linspace(1, 12, size)),
ResultPayload("gpr_points", 4, table=np.array([[*point, 0.9]], dtype=np.float32)),
])
def _sar3d_result(revision: int, peak: tuple[float, float, float]) -> ResultCollection:
x, y, z = np.linspace(-1, 1, 9), np.linspace(-.5, .5, 5), np.linspace(1, 3, 9)
payloads = [ResultPayload(name, 3, image_x_axis=xx, image_y_axis=yy,
image=np.full((yy.size, xx.size), .5, dtype=np.float32))
for name, (xx, yy) in zip(SAR3D_VIEWS, ((x, y), (z, y), (x, z), (x, y)))]
return ResultCollection(1, 1, settings_revision=revision, collection_payloads=[
*payloads,
ResultPayload("sar3d_status", 4, table=np.zeros((1, 1), dtype=np.float32)),
ResultPayload("sar3d_oblique_basis", 4, table=np.array([[1, 0, 0], [0, 1, 0]], dtype=np.float32)),
ResultPayload("sar3d_peak", 4, table=np.array([[1, *peak]], dtype=np.float32)),
])
def _bscan_result(collection_id: int, depth_m: np.ndarray) -> ResultCollection:
trace = np.full(depth_m.size, collection_id, dtype=np.complex64)
payload = ResultPayload("bscan", 1, frequency_hz=depth_m.astype(np.float32), trace=trace)
return ResultCollection(collection_id, collection_id, capture_start_ns=collection_id,
capture_end_ns=collection_id + 1, blocks=[ResultBlock(ComboKey(0, 0), [payload])])
class WebPlotCaptureTest(unittest.TestCase):
@@ -37,49 +75,53 @@ class WebPlotCaptureTest(unittest.TestCase):
self.web._last_grab_s = 0
self.web._grab_frame_if_due()
def test_no_clients_never_grabs_and_first_client_gets_current_plot(self) -> None:
self.w._plots.bscan.widget.plot([0, 1], [1, 2])
self.settle()
self.grab.assert_not_called()
controller = self.web
def show_mode(self, name: str) -> None:
mode = self.w._view.processing.mode
with QSignalBlocker(mode):
mode.setCurrentText("bscan")
self.w._live.active_mode = "bscan"
mode.setCurrentText(name)
self.w._live.active_mode = name
self.w._plots.set_mode(name)
def test_no_clients_never_grabs_and_first_client_gets_current_plot(self) -> None:
trace = TraceData(ComboKey(0, 0), np.array([1., 2.]), np.array([.1 + 0j, .2 + 0j]))
self.w._plots.draw_trace(trace, "Captured")
self.settle()
self.grab.assert_not_called()
self.show_mode("bscan")
self.w._plots.bscan.history_by_combo = {(2, 3): deque([np.array([1., 2.])])}
self.w._plots.sync_display_combos()
self.web.refresh_snapshot()
self.assertEqual(controller.status()["display_combo"],
self.assertEqual(self.web.status()["display_combo"],
{"enabled": True, "input": 2, "output": 3, "options": [(2, 3)]})
self.grab.assert_not_called()
controller.set_frame_streaming(True)
self.web.set_frame_streaming(True)
self.settle()
self.assertGreater(self.grab.call_count, 0)
self.assertIsNotNone(self.web._last_png)
calls = self.grab.call_count
self.settle()
self.assertEqual(self.grab.call_count, calls) # grab does not keep itself dirty
controller.set_frame_streaming(False)
self.w._plots.bscan.widget.clear()
self.assertEqual(self.grab.call_count, calls) # a grab does not trigger another one
self.web.set_frame_streaming(False)
self.w._plots.clear()
self.settle()
self.assertEqual(self.grab.call_count, calls)
def test_data_clear_zoom_resize_and_sar_grid_trigger_capture_after_idle(self) -> None:
def test_drawing_clearing_zoom_resize_and_page_switch_trigger_capture_after_idle(self) -> None:
plots = self.w._plots
self.show_mode("pass_through")
self.web.set_frame_streaming(True)
plot = self.w._plots.bscan.widget
item = plot.getPlotItem()
assert item is not None
view = item.getViewBox()
self.w._plots.stack.setCurrentWidget(plot)
self.settle()
view = plots.trace.magnitude.view
actions = [
lambda: plot.plot([0, 1, 2], [1, 4, 2]),
lambda: view.setXRange(0.25, 1.5, padding=0),
plot.clear,
lambda: plots.draw(_phase_result(np.full(50, 30.0))),
lambda: view.setXRange(1.2e9, 1.5e9, padding=0),
plots.redraw,
plots.clear,
lambda: self.w.resize(self.w.width() + 50, self.w.height() + 50),
lambda: self.w._plots.stack.setCurrentWidget(self.w._plots.sar3d.widget),
lambda: plots.set_mode("sar3d"),
lambda: plots.draw_trace(TraceData(ComboKey(0, 0), np.array([1., 2.]), np.array([.1 + 0j, .2 + 0j])),
"Captured"),
]
actions.extend(lambda plot=plot: plot.plot([0, 1], [1, 3]) for plot in self.w._plots.sar3d.plots.values())
for action in actions:
with self.subTest(action=action):
calls = self.grab.call_count
@@ -90,18 +132,88 @@ class WebPlotCaptureTest(unittest.TestCase):
self.settle()
self.assertEqual(self.grab.call_count, calls)
def test_throttle_keeps_dirty_and_ignores_changes_emitted_during_grab(self) -> None:
def test_throttle_defers_a_change_and_a_grab_absorbs_changes_it_renders(self) -> None:
plots = self.w._plots
self.web.set_frame_streaming(True)
self.settle()
self.web._mark_dirty()
self.grab.reset_mock()
plots.clear()
self.web._last_grab_s = 100
with mock.patch("python_app.gui.controllers.web_controller.time.monotonic", return_value=100.1):
self.assertIsNone(self.web._grab_frame_if_due())
self.assertTrue(self.web._dirty)
self.grab.side_effect = lambda: (self.web._mark_dirty(), "changed-png")[1]
self.grab.assert_not_called()
self.grab.side_effect = lambda: (plots.clear(), "changed-png")[1]
with mock.patch("python_app.gui.controllers.web_controller.time.monotonic", return_value=100.3):
self.assertIsNotNone(self.web._grab_frame_if_due())
self.assertFalse(self.web._dirty)
self.assertEqual(self.web._grabbed_revision, plots.revision)
with mock.patch("python_app.gui.controllers.web_controller.time.monotonic", return_value=100.6):
self.assertIsNone(self.web._grab_frame_if_due())
self.assertEqual(self.grab.call_count, 1)
def test_failed_grab_is_retried(self) -> None:
plots = self.w._plots
self.web.set_frame_streaming(True)
self.settle()
plots.clear()
self.grab.reset_mock(side_effect=True)
self.grab.side_effect = RuntimeError("render failed")
self.web._last_grab_s = 0
with self.assertRaises(RuntimeError):
self.web._grab_frame_if_due()
self.assertNotEqual(self.web._grabbed_revision, plots.revision)
self.grab.side_effect = None
self.settle()
self.assertEqual(self.grab.call_count, 2)
self.assertEqual(self.web._grabbed_revision, plots.revision)
def assert_window_shows_fresh_render(self) -> None:
screen, page = QApplication.primaryScreen(), self.w._plots.stack.currentWidget()
assert screen is not None and page is not None
for plot in [page] if isinstance(page, pg.PlotWidget) else page.findChildren(pg.PlotWidget):
if plot.isVisible():
origin = plot.mapTo(self.w, QPoint())
shown = screen.grabWindow(self.w.winId(), origin.x(), origin.y(), plot.width(), plot.height())
self.assertEqual(shown.toImage().convertToFormat(QImage.Format.Format_RGB32),
plot.grab().toImage().convertToFormat(QImage.Format.Format_RGB32))
def test_redrawn_views_leave_no_stale_pixels_on_screen(self) -> None:
# Qt repaints only changed regions, yet the window must equal a fresh render: once
# a phase redrawn from 100 to 0 deg left the old curve visible where none was drawn.
plots, history = self.w._plots, self.w._history
for plot in plots.stack.findChildren(pg.PlotWidget):
scene = plot.scene()
assert scene is not None
self.assertEqual(scene.receivers(scene.changed), 0) # a receiver selects that update path
phase = widget(self.w._view.processing, "_show_phase_checkbox", QCheckBox)
self.addCleanup(phase.setChecked, phase.isChecked())
phase.setChecked(True)
self.web.set_frame_streaming(True)
revision = plots.required_revision or 0
later_phase = np.zeros(400)
later_phase[200:] = np.nan
def bscan(count: int, depth_m: np.ndarray) -> bool:
history.results.clear()
history.results.extend(_bscan_result(index, depth_m) for index in range(1, count + 1))
return plots.draw_bscan()
frames = {
"pass_through": (lambda: plots.draw(_phase_result(np.full(400, 100.0))),
lambda: plots.draw(_phase_result(later_phase))),
"gpr": (lambda: plots.draw(_gpr_result(revision, (-1.0, 4.0), 20)),
lambda: plots.draw(_gpr_result(revision, (1.0, 9.0), 8))),
"sar3d": (lambda: plots.draw(_sar3d_result(revision, (.6, .3, 2.6))),
lambda: plots.draw(_sar3d_result(revision, (-.6, -.3, 1.4)))),
"bscan": (lambda: bscan(6, np.linspace(.5, 3., 12)), lambda: bscan(3, np.linspace(.5, 2., 4))),
}
self.addCleanup(history.results.clear)
for mode, (first, second) in frames.items():
with self.subTest(mode=mode):
self.show_mode(mode)
for draw in (first, second):
self.assertTrue(draw())
self.settle()
self.assert_window_shows_fresh_render()
if __name__ == "__main__":