From 6282210698484b435d70d78c0277b6c130effdd9 Mon Sep 17 00:00:00 2001 From: wyzula-jan Date: Wed, 19 Mar 2025 23:04:23 +0100 Subject: [PATCH] fix(motor_map): limit map creating optimized --- .../plots_next_gen/motor_map/motor_map.py | 78 ++++++++++++++----- tests/unit_tests/test_motor_map_next_gen.py | 28 ++++--- 2 files changed, 74 insertions(+), 32 deletions(-) diff --git a/bec_widgets/widgets/plots_next_gen/motor_map/motor_map.py b/bec_widgets/widgets/plots_next_gen/motor_map/motor_map.py index bcfb186c..3bc5ffcc 100644 --- a/bec_widgets/widgets/plots_next_gen/motor_map/motor_map.py +++ b/bec_widgets/widgets/plots_next_gen/motor_map/motor_map.py @@ -27,6 +27,27 @@ from bec_widgets.widgets.plots_next_gen.plot_base import PlotBase logger = bec_logger.logger +class FilledRectItem(pg.GraphicsObject): + """ + Custom rectangle item for the motor map plot defined by 4 points and a brush. + """ + + def __init__(self, x: float, y: float, width: float, height: float, brush: QtGui.QBrush): + super().__init__() + self._rect = QtCore.QRectF(x, y, width, height) + self._brush = brush + self._pen = pg.mkPen(None) + + def boundingRect(self): + return self._rect + + def paint(self, painter, *args): + painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing, True) + painter.setBrush(self._brush) + painter.setPen(self._pen) + painter.drawRect(self.boundingRect()) + + class MotorConfig(BaseModel): name: str | None = Field(None, description="Motor name.") limits: list[float] | None = Field(None, description="Motor limits.") @@ -592,10 +613,9 @@ class MotorMap(PlotBase): motor_x_limit = self.config.x_motor.limits motor_y_limit = self.config.y_motor.limits - if motor_x_limit is not None or motor_y_limit is not None: - self._limit_map = self._make_limit_map(motor_x_limit, motor_y_limit) - self.plot_item.addItem(self._limit_map) - self._limit_map.setZValue(-1) + self._limit_map = self._make_limit_map(motor_x_limit, motor_y_limit) + self.plot_item.addItem(self._limit_map) + self._limit_map.setZValue(-1) # Create scatter plot scatter_size = self.config.scatter_size @@ -672,35 +692,51 @@ class MotorMap(PlotBase): self.coord_label.setText(text) self.coord_label.setPos(x, y) - def _make_limit_map(self, limits_x: list, limits_y: list) -> pg.ImageItem: + def _make_limit_map(self, limits_x: list | None, limits_y: list | None) -> FilledRectItem: """ - Create a limit map for the motor map plot. + Create a limit map for the motor map plot. Each limit can be: + - [int, int] + - [None, None] + - [int, None] + - [None, int] + - or None + If any element of a limit list is None, it is treated as unbounded, + and replaced with ±1e6 (or any large float of your choice). Args: - limits_x(list): Motor limits for the x axis. - limits_y(list): Motor limits for the y axis. + limits_x(list): Motor limits for the x-axis. + limits_y(list): Motor limits for the y-axis. Returns: - pg.ImageItem: Limit map. + FilledRectItem: Limit map. """ + + def fix_limit_pair(limits): + if not limits: + return [-1e6, 1e6] + low, high = limits + if low is None: + low = -1e6 + if high is None: + high = 1e6 + return [low, high] + + limits_x = fix_limit_pair(limits_x) + limits_y = fix_limit_pair(limits_y) + limit_x_min, limit_x_max = limits_x limit_y_min, limit_y_max = limits_y - map_width = int(limit_x_max - limit_x_min + 1) - map_height = int(limit_y_max - limit_y_min + 1) - - # Create limits map + rect_width = limit_x_max - limit_x_min + rect_height = limit_y_max - limit_y_min background_value = self.config.background_value - limit_map_data = np.full((map_width, map_height), background_value, dtype=np.float32) - limit_map = pg.ImageItem() - limit_map.setImage(limit_map_data) - # Translate and scale the image item to match the motor coordinates - tr = QtGui.QTransform() - tr.translate(limit_x_min, limit_y_min) - limit_map.setTransform(tr) + brush_color = pg.mkBrush(background_value, background_value, background_value, 150) - return limit_map + filled_rect = FilledRectItem( + x=limit_x_min, y=limit_y_min, width=rect_width, height=rect_height, brush=brush_color + ) + return filled_rect def _swap_limit_map(self): """Swap the limit map.""" diff --git a/tests/unit_tests/test_motor_map_next_gen.py b/tests/unit_tests/test_motor_map_next_gen.py index 3f5a2a2a..50f769ca 100644 --- a/tests/unit_tests/test_motor_map_next_gen.py +++ b/tests/unit_tests/test_motor_map_next_gen.py @@ -78,7 +78,10 @@ def test_motor_map_properties(qtbot, mocked_client): qtbot.wait(200) assert mm.background_value == 40 assert mm.config.background_value == 40 - np.all(mm._limit_map.image == 40.0) + filled_rect = mm._limit_map + rect_color = filled_rect._brush.color().getRgb() + expected = (40, 40, 40, 150) + assert rect_color == expected def test_motor_map_get_limits(qtbot, mocked_client): @@ -205,13 +208,14 @@ def test_motor_map_limit_map(qtbot, mocked_client): # Create a limit map limit_map = mm._make_limit_map([0, 10], [0, 5]) - # Check that the limit map was created with the right type - assert isinstance(limit_map, pg.ImageItem) + from qtpy import QtCore - # Check the dimensions of the image data - image_data = limit_map.image - assert image_data.shape[0] == 11 # 0 to 10 inclusive - assert image_data.shape[1] == 6 # 0 to 5 inclusive + from bec_widgets.widgets.plots_next_gen.motor_map.motor_map import FilledRectItem + + assert isinstance(limit_map, FilledRectItem) + rect = limit_map.boundingRect() + # For [0,10] on x and [0,5] on y, width=10, height=5 + assert rect == QtCore.QRectF(0, 0, 10, 5) def test_motor_map_change_limits(qtbot, mocked_client): @@ -223,8 +227,9 @@ def test_motor_map_change_limits(qtbot, mocked_client): # samy: [-5, 5] # Original Limits Map - assert mm._limit_map.image.shape[0] == 21 # -10 to 10 inclusive - assert mm._limit_map.image.shape[1] == 11 # -5 to 5 inclusive + rect = mm._limit_map.boundingRect() + assert rect.width() == 20 # -10 to 10 inclusive + assert rect.height() == 10 # -5 to 5 inclusive assert mm.config.x_motor.limits == [-10, 10] assert mm.config.y_motor.limits == [-5, 5] @@ -237,8 +242,9 @@ def test_motor_map_change_limits(qtbot, mocked_client): # Check that the limits map was updated assert mm.config.x_motor.limits == [-20, 20] assert mm.config.y_motor.limits == [-5, 5] - assert mm._limit_map.image.shape[0] == 41 # -20 to 20 inclusive - assert mm._limit_map.image.shape[1] == 11 # -5 to 5 inclusive -> same as before + rect = mm._limit_map.boundingRect() + assert rect.width() == 40 # -20 to 20 inclusive + assert rect.height() == 10 # -5 to 5 inclusive -> same as before # Change back the limits mm.dev["samx"].limits = [-10, 10]