mirror of
https://github.com/bec-project/bec_widgets.git
synced 2025-07-14 03:31:50 +02:00
feat(motor_map): new MotorMap widget based on PlotBase
This commit is contained in:
@ -32,6 +32,7 @@ class Widgets(str, enum.Enum):
|
||||
LMFitDialog = "LMFitDialog"
|
||||
LogPanel = "LogPanel"
|
||||
Minesweeper = "Minesweeper"
|
||||
MotorMap = "MotorMap"
|
||||
PositionIndicator = "PositionIndicator"
|
||||
PositionerBox = "PositionerBox"
|
||||
PositionerBox2D = "PositionerBox2D"
|
||||
@ -2727,6 +2728,14 @@ class Image(RPCBase):
|
||||
**kwargs: Keyword arguments for the properties to be set.
|
||||
|
||||
Possible properties:
|
||||
- title: str
|
||||
- x_label: str
|
||||
- y_label: str
|
||||
- x_scale: Literal["linear", "log"]
|
||||
- y_scale: Literal["linear", "log"]
|
||||
- x_lim: tuple
|
||||
- y_lim: tuple
|
||||
- legend_label_size: int
|
||||
"""
|
||||
|
||||
@property
|
||||
@ -3353,6 +3362,395 @@ class LogPanel(RPCBase):
|
||||
class Minesweeper(RPCBase): ...
|
||||
|
||||
|
||||
class MotorMap(RPCBase):
|
||||
@property
|
||||
@rpc_call
|
||||
def enable_toolbar(self) -> "bool":
|
||||
"""
|
||||
Show Toolbar.
|
||||
"""
|
||||
|
||||
@enable_toolbar.setter
|
||||
@rpc_call
|
||||
def enable_toolbar(self) -> "bool":
|
||||
"""
|
||||
Show Toolbar.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def enable_side_panel(self) -> "bool":
|
||||
"""
|
||||
Show Side Panel
|
||||
"""
|
||||
|
||||
@enable_side_panel.setter
|
||||
@rpc_call
|
||||
def enable_side_panel(self) -> "bool":
|
||||
"""
|
||||
Show Side Panel
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def enable_fps_monitor(self) -> "bool":
|
||||
"""
|
||||
Enable the FPS monitor.
|
||||
"""
|
||||
|
||||
@enable_fps_monitor.setter
|
||||
@rpc_call
|
||||
def enable_fps_monitor(self) -> "bool":
|
||||
"""
|
||||
Enable the FPS monitor.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set(self, **kwargs):
|
||||
"""
|
||||
Set the properties of the plot widget.
|
||||
|
||||
Args:
|
||||
**kwargs: Keyword arguments for the properties to be set.
|
||||
|
||||
Possible properties:
|
||||
- title: str
|
||||
- x_label: str
|
||||
- y_label: str
|
||||
- x_scale: Literal["linear", "log"]
|
||||
- y_scale: Literal["linear", "log"]
|
||||
- x_lim: tuple
|
||||
- y_lim: tuple
|
||||
- legend_label_size: int
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def title(self) -> "str":
|
||||
"""
|
||||
Set title of the plot.
|
||||
"""
|
||||
|
||||
@title.setter
|
||||
@rpc_call
|
||||
def title(self) -> "str":
|
||||
"""
|
||||
Set title of the plot.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def x_label(self) -> "str":
|
||||
"""
|
||||
The set label for the x-axis.
|
||||
"""
|
||||
|
||||
@x_label.setter
|
||||
@rpc_call
|
||||
def x_label(self) -> "str":
|
||||
"""
|
||||
The set label for the x-axis.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def y_label(self) -> "str":
|
||||
"""
|
||||
The set label for the y-axis.
|
||||
"""
|
||||
|
||||
@y_label.setter
|
||||
@rpc_call
|
||||
def y_label(self) -> "str":
|
||||
"""
|
||||
The set label for the y-axis.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def x_limits(self) -> "QPointF":
|
||||
"""
|
||||
Get the x limits of the plot.
|
||||
"""
|
||||
|
||||
@x_limits.setter
|
||||
@rpc_call
|
||||
def x_limits(self) -> "QPointF":
|
||||
"""
|
||||
Get the x limits of the plot.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def y_limits(self) -> "QPointF":
|
||||
"""
|
||||
Get the y limits of the plot.
|
||||
"""
|
||||
|
||||
@y_limits.setter
|
||||
@rpc_call
|
||||
def y_limits(self) -> "QPointF":
|
||||
"""
|
||||
Get the y limits of the plot.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def x_grid(self) -> "bool":
|
||||
"""
|
||||
Show grid on the x-axis.
|
||||
"""
|
||||
|
||||
@x_grid.setter
|
||||
@rpc_call
|
||||
def x_grid(self) -> "bool":
|
||||
"""
|
||||
Show grid on the x-axis.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def y_grid(self) -> "bool":
|
||||
"""
|
||||
Show grid on the y-axis.
|
||||
"""
|
||||
|
||||
@y_grid.setter
|
||||
@rpc_call
|
||||
def y_grid(self) -> "bool":
|
||||
"""
|
||||
Show grid on the y-axis.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def inner_axes(self) -> "bool":
|
||||
"""
|
||||
Show inner axes of the plot widget.
|
||||
"""
|
||||
|
||||
@inner_axes.setter
|
||||
@rpc_call
|
||||
def inner_axes(self) -> "bool":
|
||||
"""
|
||||
Show inner axes of the plot widget.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def outer_axes(self) -> "bool":
|
||||
"""
|
||||
Show the outer axes of the plot widget.
|
||||
"""
|
||||
|
||||
@outer_axes.setter
|
||||
@rpc_call
|
||||
def outer_axes(self) -> "bool":
|
||||
"""
|
||||
Show the outer axes of the plot widget.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def lock_aspect_ratio(self) -> "bool":
|
||||
"""
|
||||
Lock aspect ratio of the plot widget.
|
||||
"""
|
||||
|
||||
@lock_aspect_ratio.setter
|
||||
@rpc_call
|
||||
def lock_aspect_ratio(self) -> "bool":
|
||||
"""
|
||||
Lock aspect ratio of the plot widget.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def auto_range_x(self) -> "bool":
|
||||
"""
|
||||
Set auto range for the x-axis.
|
||||
"""
|
||||
|
||||
@auto_range_x.setter
|
||||
@rpc_call
|
||||
def auto_range_x(self) -> "bool":
|
||||
"""
|
||||
Set auto range for the x-axis.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def auto_range_y(self) -> "bool":
|
||||
"""
|
||||
Set auto range for the y-axis.
|
||||
"""
|
||||
|
||||
@auto_range_y.setter
|
||||
@rpc_call
|
||||
def auto_range_y(self) -> "bool":
|
||||
"""
|
||||
Set auto range for the y-axis.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def x_log(self) -> "bool":
|
||||
"""
|
||||
Set X-axis to log scale if True, linear if False.
|
||||
"""
|
||||
|
||||
@x_log.setter
|
||||
@rpc_call
|
||||
def x_log(self) -> "bool":
|
||||
"""
|
||||
Set X-axis to log scale if True, linear if False.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def y_log(self) -> "bool":
|
||||
"""
|
||||
Set Y-axis to log scale if True, linear if False.
|
||||
"""
|
||||
|
||||
@y_log.setter
|
||||
@rpc_call
|
||||
def y_log(self) -> "bool":
|
||||
"""
|
||||
Set Y-axis to log scale if True, linear if False.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def legend_label_size(self) -> "int":
|
||||
"""
|
||||
The font size of the legend font.
|
||||
"""
|
||||
|
||||
@legend_label_size.setter
|
||||
@rpc_call
|
||||
def legend_label_size(self) -> "int":
|
||||
"""
|
||||
The font size of the legend font.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def color(self) -> "tuple":
|
||||
"""
|
||||
Get the color of the motor trace.
|
||||
|
||||
Returns:
|
||||
tuple: Color of the motor trace.
|
||||
"""
|
||||
|
||||
@color.setter
|
||||
@rpc_call
|
||||
def color(self) -> "tuple":
|
||||
"""
|
||||
Get the color of the motor trace.
|
||||
|
||||
Returns:
|
||||
tuple: Color of the motor trace.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def max_points(self) -> "int":
|
||||
"""
|
||||
Get the maximum number of points to display.
|
||||
"""
|
||||
|
||||
@max_points.setter
|
||||
@rpc_call
|
||||
def max_points(self) -> "int":
|
||||
"""
|
||||
Get the maximum number of points to display.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def precision(self) -> "int":
|
||||
"""
|
||||
Set the decimal precision of the motor position.
|
||||
"""
|
||||
|
||||
@precision.setter
|
||||
@rpc_call
|
||||
def precision(self) -> "int":
|
||||
"""
|
||||
Set the decimal precision of the motor position.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def num_dim_points(self) -> "int":
|
||||
"""
|
||||
Get the number of dim points for the motor map.
|
||||
"""
|
||||
|
||||
@num_dim_points.setter
|
||||
@rpc_call
|
||||
def num_dim_points(self) -> "int":
|
||||
"""
|
||||
Get the number of dim points for the motor map.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def background_value(self) -> "int":
|
||||
"""
|
||||
Get the background value of the motor map.
|
||||
"""
|
||||
|
||||
@background_value.setter
|
||||
@rpc_call
|
||||
def background_value(self) -> "int":
|
||||
"""
|
||||
Get the background value of the motor map.
|
||||
"""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def scatter_size(self) -> "int":
|
||||
"""
|
||||
Get the scatter size of the motor map plot.
|
||||
"""
|
||||
|
||||
@scatter_size.setter
|
||||
@rpc_call
|
||||
def scatter_size(self) -> "int":
|
||||
"""
|
||||
Get the scatter size of the motor map plot.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def map(self, x_name: "str", y_name: "str", validate_bec: "bool" = True) -> "None":
|
||||
"""
|
||||
Set the x and y motor names.
|
||||
|
||||
Args:
|
||||
x_name(str): The name of the x motor.
|
||||
y_name(str): The name of the y motor.
|
||||
validate_bec(bool, optional): If True, validate the signal with BEC. Defaults to True.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def reset_history(self):
|
||||
"""
|
||||
Reset the history of the motor map.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def get_data(self) -> "dict":
|
||||
"""
|
||||
Get the data of the motor map.
|
||||
|
||||
Returns:
|
||||
dict: Data of the motor map.
|
||||
"""
|
||||
|
||||
|
||||
class PositionIndicator(RPCBase):
|
||||
@rpc_call
|
||||
def set_value(self, position: float):
|
||||
@ -3780,6 +4178,8 @@ class ScanMetadata(RPCBase):
|
||||
|
||||
|
||||
class ScatterCurve(RPCBase):
|
||||
"""Scatter curve item for the scatter waveform widget."""
|
||||
|
||||
@property
|
||||
@rpc_call
|
||||
def color_map(self) -> "str":
|
||||
@ -3840,6 +4240,14 @@ class ScatterWaveform(RPCBase):
|
||||
**kwargs: Keyword arguments for the properties to be set.
|
||||
|
||||
Possible properties:
|
||||
- title: str
|
||||
- x_label: str
|
||||
- y_label: str
|
||||
- x_scale: Literal["linear", "log"]
|
||||
- y_scale: Literal["linear", "log"]
|
||||
- x_lim: tuple
|
||||
- y_lim: tuple
|
||||
- legend_label_size: int
|
||||
"""
|
||||
|
||||
@property
|
||||
@ -4232,6 +4640,14 @@ class Waveform(RPCBase):
|
||||
**kwargs: Keyword arguments for the properties to be set.
|
||||
|
||||
Possible properties:
|
||||
- title: str
|
||||
- x_label: str
|
||||
- y_label: str
|
||||
- x_scale: Literal["linear", "log"]
|
||||
- y_scale: Literal["linear", "log"]
|
||||
- x_lim: tuple
|
||||
- y_lim: tuple
|
||||
- legend_label_size: int
|
||||
"""
|
||||
|
||||
@property
|
||||
|
793
bec_widgets/widgets/plots_next_gen/motor_map/motor_map.py
Normal file
793
bec_widgets/widgets/plots_next_gen/motor_map/motor_map.py
Normal file
@ -0,0 +1,793 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
from bec_lib import bec_logger
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from pydantic_core import PydanticCustomError
|
||||
from qtpy import QtCore, QtGui
|
||||
from qtpy.QtCore import Signal
|
||||
from qtpy.QtGui import QColor
|
||||
from qtpy.QtWidgets import QHBoxLayout, QMainWindow, QWidget
|
||||
|
||||
from bec_widgets.qt_utils.error_popups import SafeProperty, SafeSlot
|
||||
from bec_widgets.qt_utils.settings_dialog import SettingsDialog
|
||||
from bec_widgets.qt_utils.toolbar import MaterialIconAction
|
||||
from bec_widgets.utils import Colors, ConnectionConfig
|
||||
from bec_widgets.utils.colors import set_theme
|
||||
from bec_widgets.widgets.plots_next_gen.motor_map.settings.motor_map_settings import (
|
||||
MotorMapSettings,
|
||||
)
|
||||
from bec_widgets.widgets.plots_next_gen.motor_map.toolbar_bundles.motor_selection import (
|
||||
MotorSelectionToolbarBundle,
|
||||
)
|
||||
from bec_widgets.widgets.plots_next_gen.plot_base import PlotBase
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class MotorConfig(BaseModel):
|
||||
name: str | None = Field(None, description="Motor name.")
|
||||
limits: list[float] | None = Field(None, description="Motor limits.")
|
||||
|
||||
|
||||
# noinspection PyDataclass
|
||||
class MotorMapConfig(ConnectionConfig):
|
||||
x_motor: MotorConfig = Field(default_factory=MotorConfig, description="Motor X name.")
|
||||
y_motor: MotorConfig = Field(default_factory=MotorConfig, description="Motor Y name.")
|
||||
color: str | tuple | None = Field(
|
||||
(255, 255, 255, 255), description="The color of the last point of current position."
|
||||
)
|
||||
scatter_size: int | None = Field(5, description="Size of the scatter points.")
|
||||
max_points: int | None = Field(5000, description="Maximum number of points to display.")
|
||||
num_dim_points: int | None = Field(
|
||||
100,
|
||||
description="Number of points to dim before the color remains same for older recorded position.",
|
||||
)
|
||||
precision: int | None = Field(2, description="Decimal precision of the motor position.")
|
||||
background_value: int | None = Field(
|
||||
25, description="Background value of the motor map. Has to be between 0 and 255."
|
||||
)
|
||||
|
||||
model_config: dict = {"validate_assignment": True}
|
||||
|
||||
_validate_color = field_validator("color")(Colors.validate_color)
|
||||
|
||||
@field_validator("background_value")
|
||||
def validate_background_value(cls, value):
|
||||
if not 0 <= value <= 255:
|
||||
raise PydanticCustomError(
|
||||
"wrong_value", f"'{value}' hs to be between 0 and 255.", {"wrong_value": value}
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
class MotorMap(PlotBase):
|
||||
PLUGIN = True
|
||||
RPC = True
|
||||
ICON_NAME = "my_location"
|
||||
USER_ACCESS = [
|
||||
# General PlotBase Settings
|
||||
"enable_toolbar",
|
||||
"enable_toolbar.setter",
|
||||
"enable_side_panel",
|
||||
"enable_side_panel.setter",
|
||||
"enable_fps_monitor",
|
||||
"enable_fps_monitor.setter",
|
||||
"set",
|
||||
"title",
|
||||
"title.setter",
|
||||
"x_label",
|
||||
"x_label.setter",
|
||||
"y_label",
|
||||
"y_label.setter",
|
||||
"x_limits",
|
||||
"x_limits.setter",
|
||||
"y_limits",
|
||||
"y_limits.setter",
|
||||
"x_grid",
|
||||
"x_grid.setter",
|
||||
"y_grid",
|
||||
"y_grid.setter",
|
||||
"inner_axes",
|
||||
"inner_axes.setter",
|
||||
"outer_axes",
|
||||
"outer_axes.setter",
|
||||
"lock_aspect_ratio",
|
||||
"lock_aspect_ratio.setter",
|
||||
"auto_range_x",
|
||||
"auto_range_x.setter",
|
||||
"auto_range_y",
|
||||
"auto_range_y.setter",
|
||||
"x_log",
|
||||
"x_log.setter",
|
||||
"y_log",
|
||||
"y_log.setter",
|
||||
"legend_label_size",
|
||||
"legend_label_size.setter",
|
||||
# motor_map specific
|
||||
"color",
|
||||
"color.setter",
|
||||
"max_points",
|
||||
"max_points.setter",
|
||||
"precision",
|
||||
"precision.setter",
|
||||
"num_dim_points",
|
||||
"num_dim_points.setter",
|
||||
"background_value",
|
||||
"background_value.setter",
|
||||
"scatter_size",
|
||||
"scatter_size.setter",
|
||||
"map",
|
||||
"reset_history",
|
||||
"get_data",
|
||||
]
|
||||
|
||||
update_signal = Signal()
|
||||
"""Motor map widget for plotting motor positions."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent: QWidget | None = None,
|
||||
config: MotorMapConfig | None = None,
|
||||
client=None,
|
||||
gui_id: str | None = None,
|
||||
popups: bool = True,
|
||||
**kwargs,
|
||||
):
|
||||
if config is None:
|
||||
config = MotorMapConfig(widget_class=self.__class__.__name__)
|
||||
super().__init__(
|
||||
parent=parent, config=config, client=client, gui_id=gui_id, popups=popups, **kwargs
|
||||
)
|
||||
|
||||
# For PropertyManager identification
|
||||
self.setObjectName("MotorMap")
|
||||
|
||||
# Default values for PlotBase
|
||||
self.x_grid = True
|
||||
self.y_grid = True
|
||||
|
||||
# Gui specific
|
||||
self._buffer = {"x": [], "y": []}
|
||||
self._limit_map = None
|
||||
self._trace = None
|
||||
self.v_line = None
|
||||
self.h_line = None
|
||||
self.coord_label = None
|
||||
self.motor_map_settings = None
|
||||
|
||||
# Connect slots
|
||||
self.proxy_update_plot = pg.SignalProxy(
|
||||
self.update_signal, rateLimit=25, slot=self._update_plot
|
||||
)
|
||||
self._add_motor_map_settings()
|
||||
|
||||
################################################################################
|
||||
# Widget Specific GUI interactions
|
||||
################################################################################
|
||||
|
||||
def _init_toolbar(self):
|
||||
"""
|
||||
Initialize the toolbar for the motor map widget.
|
||||
"""
|
||||
self.motor_selection_bundle = MotorSelectionToolbarBundle(
|
||||
bundle_id="motor_selection", target_widget=self
|
||||
)
|
||||
self.toolbar.add_bundle(self.motor_selection_bundle, target_widget=self)
|
||||
super()._init_toolbar()
|
||||
self.toolbar.widgets["reset_legend"].action.setVisible(False)
|
||||
|
||||
self.reset_legend_action = MaterialIconAction(
|
||||
icon_name="history", tooltip="Reset the position of legend."
|
||||
)
|
||||
self.toolbar.add_action_to_bundle(
|
||||
bundle_id="roi",
|
||||
action_id="motor_map_history",
|
||||
action=self.reset_legend_action,
|
||||
target_widget=self,
|
||||
)
|
||||
self.reset_legend_action.action.triggered.connect(self.reset_history)
|
||||
|
||||
def _add_motor_map_settings(self):
|
||||
"""Add the motor map settings to the side panel."""
|
||||
motor_map_settings = MotorMapSettings(target_widget=self, popup=False)
|
||||
self.side_panel.add_menu(
|
||||
action_id="motor_map_settings",
|
||||
icon_name="settings_brightness",
|
||||
tooltip="Show Motor Map Settings",
|
||||
widget=motor_map_settings,
|
||||
title="Motor Map Settings",
|
||||
)
|
||||
|
||||
def add_popups(self):
|
||||
"""
|
||||
Add popups to the ScatterWaveform widget.
|
||||
"""
|
||||
super().add_popups()
|
||||
scatter_curve_setting_action = MaterialIconAction(
|
||||
icon_name="settings_brightness",
|
||||
tooltip="Show Motor Map Settings",
|
||||
checkable=True,
|
||||
parent=self,
|
||||
)
|
||||
self.toolbar.add_action_to_bundle(
|
||||
bundle_id="popup_bundle",
|
||||
action_id="motor_map_settings",
|
||||
action=scatter_curve_setting_action,
|
||||
target_widget=self,
|
||||
)
|
||||
self.toolbar.widgets["motor_map_settings"].action.triggered.connect(
|
||||
self.show_motor_map_settings
|
||||
)
|
||||
|
||||
def show_motor_map_settings(self):
|
||||
"""
|
||||
Show the DAP summary popup.
|
||||
"""
|
||||
action = self.toolbar.widgets["motor_map_settings"].action
|
||||
if self.motor_map_settings is None or not self.motor_map_settings.isVisible():
|
||||
motor_map_settings = MotorMapSettings(target_widget=self, popup=True)
|
||||
self.motor_map_settings = SettingsDialog(
|
||||
self,
|
||||
settings_widget=motor_map_settings,
|
||||
window_title="Motor Map Settings",
|
||||
modal=False,
|
||||
)
|
||||
self.motor_map_settings.setFixedSize(250, 300)
|
||||
# When the dialog is closed, update the toolbar icon and clear the reference
|
||||
self.motor_map_settings.finished.connect(self._motor_map_settings_closed)
|
||||
self.motor_map_settings.show()
|
||||
action.setChecked(True)
|
||||
else:
|
||||
# If already open, bring it to the front
|
||||
self.motor_map_settings.raise_()
|
||||
self.motor_map_settings.activateWindow()
|
||||
action.setChecked(True) # keep it toggled
|
||||
|
||||
def _motor_map_settings_closed(self):
|
||||
"""
|
||||
Slot for when the axis settings dialog is closed.
|
||||
"""
|
||||
self.motor_map_settings.deleteLater()
|
||||
self.motor_map_settings = None
|
||||
self.toolbar.widgets["motor_map_settings"].action.setChecked(False)
|
||||
|
||||
################################################################################
|
||||
# Widget Specific Properties
|
||||
################################################################################
|
||||
|
||||
# color_scatter for designer, color for CLI to not bother users with QColor
|
||||
@SafeProperty("QColor")
|
||||
def color_scatter(self) -> QtGui.QColor:
|
||||
"""
|
||||
Get the color of the motor trace.
|
||||
|
||||
Returns:
|
||||
QColor: Color of the motor trace.
|
||||
"""
|
||||
return QColor(*self.config.color)
|
||||
|
||||
@color_scatter.setter
|
||||
def color_scatter(self, color: str | tuple | QColor) -> None:
|
||||
"""
|
||||
Set color of the motor trace.
|
||||
|
||||
Args:
|
||||
color(str|tuple): Color of the motor trace. Can be HEX(str) or RGBA(tuple).
|
||||
"""
|
||||
if isinstance(color, str):
|
||||
color = Colors.hex_to_rgba(color, 255)
|
||||
if isinstance(color, QColor):
|
||||
color = (color.red(), color.green(), color.blue(), color.alpha())
|
||||
color = Colors.validate_color(color)
|
||||
self.config.color = color
|
||||
self.update_signal.emit()
|
||||
self.property_changed.emit("color_scatter", color)
|
||||
|
||||
@property
|
||||
def color(self) -> tuple:
|
||||
"""
|
||||
Get the color of the motor trace.
|
||||
|
||||
Returns:
|
||||
tuple: Color of the motor trace.
|
||||
"""
|
||||
return self.config.color
|
||||
|
||||
@color.setter
|
||||
def color(self, color: str | tuple) -> None:
|
||||
"""
|
||||
Set color of the motor trace.
|
||||
|
||||
Args:
|
||||
color(str|tuple): Color of the motor trace. Can be HEX(str) or RGBA(tuple).
|
||||
"""
|
||||
self.color_scatter = color
|
||||
|
||||
@SafeProperty(int)
|
||||
def max_points(self) -> int:
|
||||
"""Get the maximum number of points to display."""
|
||||
return self.config.max_points
|
||||
|
||||
@max_points.setter
|
||||
def max_points(self, max_points: int) -> None:
|
||||
"""
|
||||
Set the maximum number of points to display.
|
||||
|
||||
Args:
|
||||
max_points(int): Maximum number of points to display.
|
||||
"""
|
||||
self.config.max_points = max_points
|
||||
self.update_signal.emit()
|
||||
self.property_changed.emit("max_points", max_points)
|
||||
|
||||
@SafeProperty(int)
|
||||
def precision(self) -> int:
|
||||
"""
|
||||
Set the decimal precision of the motor position.
|
||||
"""
|
||||
return self.config.precision
|
||||
|
||||
@precision.setter
|
||||
def precision(self, precision: int) -> None:
|
||||
"""
|
||||
Set the decimal precision of the motor position.
|
||||
|
||||
Args:
|
||||
precision(int): Decimal precision of the motor position.
|
||||
"""
|
||||
self.config.precision = precision
|
||||
self.update_signal.emit()
|
||||
self.property_changed.emit("precision", precision)
|
||||
|
||||
@SafeProperty(int)
|
||||
def num_dim_points(self) -> int:
|
||||
"""
|
||||
Get the number of dim points for the motor map.
|
||||
"""
|
||||
return self.config.num_dim_points
|
||||
|
||||
@num_dim_points.setter
|
||||
def num_dim_points(self, num_dim_points: int) -> None:
|
||||
"""
|
||||
Set the number of dim points for the motor map.
|
||||
|
||||
Args:
|
||||
num_dim_points(int): Number of dim points.
|
||||
"""
|
||||
self.config.num_dim_points = num_dim_points
|
||||
self.update_signal.emit()
|
||||
self.property_changed.emit("num_dim_points", num_dim_points)
|
||||
|
||||
@SafeProperty(int)
|
||||
def background_value(self) -> int:
|
||||
"""
|
||||
Get the background value of the motor map.
|
||||
"""
|
||||
return self.config.background_value
|
||||
|
||||
@background_value.setter
|
||||
def background_value(self, background_value: int) -> None:
|
||||
"""
|
||||
Set the background value of the motor map.
|
||||
|
||||
Args:
|
||||
background_value(int): Background value of the motor map.
|
||||
"""
|
||||
self.config.background_value = background_value
|
||||
self._swap_limit_map()
|
||||
self.property_changed.emit("background_value", background_value)
|
||||
|
||||
@SafeProperty(int)
|
||||
def scatter_size(self) -> int:
|
||||
"""
|
||||
Get the scatter size of the motor map plot.
|
||||
"""
|
||||
return self.config.scatter_size
|
||||
|
||||
@scatter_size.setter
|
||||
def scatter_size(self, scatter_size: int) -> None:
|
||||
"""
|
||||
Set the scatter size of the motor map plot.
|
||||
|
||||
Args:
|
||||
scatter_size(int): Size of the scatter points.
|
||||
"""
|
||||
self.config.scatter_size = scatter_size
|
||||
self.update_signal.emit()
|
||||
self.property_changed.emit("scatter_size", scatter_size)
|
||||
|
||||
################################################################################
|
||||
# High Level methods for API
|
||||
################################################################################
|
||||
@SafeSlot()
|
||||
def map(self, x_name: str, y_name: str, validate_bec: bool = True) -> None:
|
||||
"""
|
||||
Set the x and y motor names.
|
||||
|
||||
Args:
|
||||
x_name(str): The name of the x motor.
|
||||
y_name(str): The name of the y motor.
|
||||
validate_bec(bool, optional): If True, validate the signal with BEC. Defaults to True.
|
||||
"""
|
||||
self.plot_item.clear()
|
||||
|
||||
if validate_bec:
|
||||
self.entry_validator.validate_signal(x_name, None)
|
||||
self.entry_validator.validate_signal(y_name, None)
|
||||
|
||||
self.config.x_motor.name = x_name
|
||||
self.config.y_motor.name = y_name
|
||||
|
||||
motor_x_limit = self._get_motor_limit(self.config.x_motor.name)
|
||||
motor_y_limit = self._get_motor_limit(self.config.y_motor.name)
|
||||
|
||||
self.config.x_motor.limits = motor_x_limit
|
||||
self.config.y_motor.limits = motor_y_limit
|
||||
|
||||
# reconnect the signals
|
||||
self._connect_motor_to_slots()
|
||||
|
||||
# Reset the buffer
|
||||
self._buffer = {"x": [], "y": []}
|
||||
|
||||
# Redraw the motor map
|
||||
self._make_motor_map()
|
||||
|
||||
self._sync_motor_map_selection_toolbar()
|
||||
|
||||
def reset_history(self):
|
||||
"""
|
||||
Reset the history of the motor map.
|
||||
"""
|
||||
self._buffer["x"] = [self._buffer["x"][-1]]
|
||||
self._buffer["y"] = [self._buffer["y"][-1]]
|
||||
self.update_signal.emit()
|
||||
|
||||
################################################################################
|
||||
# BEC Update Methods
|
||||
################################################################################
|
||||
@SafeSlot()
|
||||
def _update_plot(self, _=None):
|
||||
"""Update the motor map plot."""
|
||||
if self._trace is None:
|
||||
return
|
||||
# If the number of points exceeds max_points, delete the oldest points
|
||||
if len(self._buffer["x"]) > self.config.max_points:
|
||||
self._buffer["x"] = self._buffer["x"][-self.config.max_points :]
|
||||
self._buffer["y"] = self._buffer["y"][-self.config.max_points :]
|
||||
|
||||
x = self._buffer["x"]
|
||||
y = self._buffer["y"]
|
||||
|
||||
# Setup gradient brush for history
|
||||
brushes = [pg.mkBrush(50, 50, 50, 255)] * len(x)
|
||||
|
||||
# RGB color
|
||||
r, g, b, a = self.config.color
|
||||
|
||||
# Calculate the decrement step based on self.num_dim_points
|
||||
num_dim_points = self.config.num_dim_points
|
||||
decrement_step = (255 - 50) / num_dim_points
|
||||
|
||||
for i in range(1, min(num_dim_points + 1, len(x) + 1)):
|
||||
brightness = max(60, 255 - decrement_step * (i - 1))
|
||||
dim_r = int(r * (brightness / 255))
|
||||
dim_g = int(g * (brightness / 255))
|
||||
dim_b = int(b * (brightness / 255))
|
||||
brushes[-i] = pg.mkBrush(dim_r, dim_g, dim_b, a)
|
||||
brushes[-1] = pg.mkBrush(r, g, b, a) # Newest point is always full brightness
|
||||
scatter_size = self.config.scatter_size
|
||||
|
||||
# Update the scatter plot
|
||||
self._trace.setData(x=x, y=y, brush=brushes, pen=None, size=scatter_size)
|
||||
|
||||
# Get last know position for crosshair
|
||||
current_x = x[-1]
|
||||
current_y = y[-1]
|
||||
|
||||
# Update the crosshair
|
||||
self._set_motor_indicator_position(current_x, current_y)
|
||||
|
||||
@SafeSlot(dict, dict)
|
||||
def on_device_readback(self, msg: dict, metadata: dict) -> None:
|
||||
"""
|
||||
Update the motor map plot with the new motor position.
|
||||
|
||||
Args:
|
||||
msg(dict): Message from the device readback.
|
||||
metadata(dict): Metadata of the message.
|
||||
"""
|
||||
x_motor = self.config.x_motor.name
|
||||
y_motor = self.config.y_motor.name
|
||||
|
||||
if x_motor is None or y_motor is None:
|
||||
return
|
||||
|
||||
if x_motor in msg["signals"]:
|
||||
x = msg["signals"][x_motor]["value"]
|
||||
self._buffer["x"].append(x)
|
||||
self._buffer["y"].append(self._buffer["y"][-1])
|
||||
|
||||
elif y_motor in msg["signals"]:
|
||||
y = msg["signals"][y_motor]["value"]
|
||||
self._buffer["y"].append(y)
|
||||
self._buffer["x"].append(self._buffer["x"][-1])
|
||||
|
||||
self.update_signal.emit()
|
||||
|
||||
def _connect_motor_to_slots(self):
|
||||
"""Connect motors to slots."""
|
||||
self._disconnect_current_motors()
|
||||
|
||||
endpoints_readback = [
|
||||
MessageEndpoints.device_readback(self.config.x_motor.name),
|
||||
MessageEndpoints.device_readback(self.config.y_motor.name),
|
||||
]
|
||||
endpoints_limits = [
|
||||
MessageEndpoints.device_limits(self.config.x_motor.name),
|
||||
MessageEndpoints.device_limits(self.config.y_motor.name),
|
||||
]
|
||||
|
||||
self.bec_dispatcher.connect_slot(self.on_device_readback, endpoints_readback)
|
||||
self.bec_dispatcher.connect_slot(self.on_device_limits, endpoints_limits)
|
||||
|
||||
def _disconnect_current_motors(self):
|
||||
"""Disconnect the current motors from the slots."""
|
||||
if self.config.x_motor.name is not None and self.config.y_motor.name is not None:
|
||||
endpoints_readback = [
|
||||
MessageEndpoints.device_readback(self.config.x_motor.name),
|
||||
MessageEndpoints.device_readback(self.config.y_motor.name),
|
||||
]
|
||||
endpoints_limits = [
|
||||
MessageEndpoints.device_limits(self.config.x_motor.name),
|
||||
MessageEndpoints.device_limits(self.config.y_motor.name),
|
||||
]
|
||||
self.bec_dispatcher.disconnect_slot(self.on_device_readback, endpoints_readback)
|
||||
self.bec_dispatcher.disconnect_slot(self.on_device_limits, endpoints_limits)
|
||||
|
||||
################################################################################
|
||||
# Utility Methods
|
||||
################################################################################
|
||||
@SafeSlot(dict, dict)
|
||||
def on_device_limits(self, msg: dict, metadata: dict) -> None:
|
||||
"""
|
||||
Update the motor limits in the config.
|
||||
|
||||
Args:
|
||||
msg(dict): Message from the device limits.
|
||||
metadata(dict): Metadata of the message.
|
||||
"""
|
||||
self.config.x_motor.limits = self._get_motor_limit(self.config.x_motor.name)
|
||||
self.config.y_motor.limits = self._get_motor_limit(self.config.y_motor.name)
|
||||
self._swap_limit_map()
|
||||
|
||||
def _get_motor_limit(self, motor: str) -> list | None:
|
||||
"""
|
||||
Get the motor limit from the config.
|
||||
|
||||
Args:
|
||||
motor(str): Motor name.
|
||||
|
||||
Returns:
|
||||
float: Motor limit.
|
||||
"""
|
||||
try:
|
||||
limits = self.dev[motor].limits
|
||||
if limits == [0, 0]:
|
||||
return None
|
||||
return limits
|
||||
except AttributeError: # TODO maybe not needed, if no limits it returns [0,0]
|
||||
# If the motor doesn't have a 'limits' attribute, return a default value or raise a custom exception
|
||||
logger.error(f"The device '{motor}' does not have defined limits.")
|
||||
return None
|
||||
|
||||
def _make_motor_map(self) -> None:
|
||||
"""
|
||||
Make the motor map.
|
||||
"""
|
||||
|
||||
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)
|
||||
|
||||
# Create scatter plot
|
||||
scatter_size = self.config.scatter_size
|
||||
self._trace = pg.ScatterPlotItem(size=scatter_size, brush=pg.mkBrush(255, 255, 255, 255))
|
||||
self.plot_item.addItem(self._trace)
|
||||
self._trace.setZValue(0)
|
||||
|
||||
# Add the crosshair for initial motor coordinates
|
||||
initial_position_x = self._get_motor_init_position(
|
||||
self.config.x_motor.name, self.config.precision
|
||||
)
|
||||
initial_position_y = self._get_motor_init_position(
|
||||
self.config.y_motor.name, self.config.precision
|
||||
)
|
||||
|
||||
self._buffer["x"] = [initial_position_x]
|
||||
self._buffer["y"] = [initial_position_y]
|
||||
|
||||
self._trace.setData([initial_position_x], [initial_position_y])
|
||||
|
||||
# Add initial crosshair
|
||||
self._add_coordinates_crosshair(initial_position_x, initial_position_y)
|
||||
|
||||
# Set default labels for the plot
|
||||
self.set_x_label_suffix(f"[{self.config.x_motor.name}-{self.config.x_motor.name}]")
|
||||
self.set_y_label_suffix(f"[{self.config.y_motor.name}-{self.config.y_motor.name}]")
|
||||
|
||||
self.update_signal.emit()
|
||||
|
||||
def _add_coordinates_crosshair(self, x: float, y: float) -> None:
|
||||
"""
|
||||
Add position crosshair indicator to the plot.
|
||||
|
||||
Args:
|
||||
x(float): X coordinate of the crosshair.
|
||||
y(float): Y coordinate of the crosshair.
|
||||
"""
|
||||
if self.v_line is not None and self.h_line is not None and self.coord_label is not None:
|
||||
self.plot_item.removeItem(self.h_line)
|
||||
self.plot_item.removeItem(self.v_line)
|
||||
self.plot_item.removeItem(self.coord_label)
|
||||
|
||||
self.h_line = pg.InfiniteLine(
|
||||
angle=0, movable=False, pen=pg.mkPen(color="r", width=1, style=QtCore.Qt.DashLine)
|
||||
)
|
||||
self.v_line = pg.InfiniteLine(
|
||||
angle=90, movable=False, pen=pg.mkPen(color="r", width=1, style=QtCore.Qt.DashLine)
|
||||
)
|
||||
|
||||
self.coord_label = pg.TextItem("", anchor=(1, 1), fill=(0, 0, 0, 100))
|
||||
|
||||
# Add crosshair to the plot
|
||||
self.plot_item.addItem(self.h_line)
|
||||
self.plot_item.addItem(self.v_line)
|
||||
self.plot_item.addItem(self.coord_label)
|
||||
|
||||
self._set_motor_indicator_position(x, y)
|
||||
|
||||
def _set_motor_indicator_position(self, x: float, y: float) -> None:
|
||||
"""
|
||||
Set the position of the motor indicator.
|
||||
|
||||
Args:
|
||||
x(float): X coordinate of the motor indicator.
|
||||
y(float): Y coordinate of the motor indicator.
|
||||
"""
|
||||
if self.v_line is None or self.h_line is None or self.coord_label is None:
|
||||
return
|
||||
|
||||
text = f"({x:.{self.config.precision}f}, {y:.{self.config.precision}f})"
|
||||
|
||||
self.v_line.setPos(x)
|
||||
self.h_line.setPos(y)
|
||||
self.coord_label.setText(text)
|
||||
self.coord_label.setPos(x, y)
|
||||
|
||||
def _make_limit_map(self, limits_x: list, limits_y: list) -> pg.ImageItem:
|
||||
"""
|
||||
Create a limit map for the motor map plot.
|
||||
|
||||
Args:
|
||||
limits_x(list): Motor limits for the x axis.
|
||||
limits_y(list): Motor limits for the y axis.
|
||||
|
||||
Returns:
|
||||
pg.ImageItem: Limit map.
|
||||
"""
|
||||
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
|
||||
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)
|
||||
|
||||
return limit_map
|
||||
|
||||
def _swap_limit_map(self):
|
||||
"""Swap the limit map."""
|
||||
self.plot_item.removeItem(self._limit_map)
|
||||
x_limits = self.config.x_motor.limits
|
||||
y_limits = self.config.y_motor.limits
|
||||
if x_limits is not None and y_limits is not None:
|
||||
self._limit_map = self._make_limit_map(x_limits, y_limits)
|
||||
self._limit_map.setZValue(-1)
|
||||
self.plot_item.addItem(self._limit_map)
|
||||
|
||||
def _get_motor_init_position(self, name: str, precision: int) -> float:
|
||||
"""
|
||||
Get the motor initial position from the config.
|
||||
|
||||
Args:
|
||||
name(str): Motor name.
|
||||
precision(int): Decimal precision of the motor position.
|
||||
|
||||
Returns:
|
||||
float: Motor initial position.
|
||||
"""
|
||||
entry = self.entry_validator.validate_signal(name, None)
|
||||
init_position = round(float(self.dev[name].read()[entry]["value"]), precision)
|
||||
return init_position
|
||||
|
||||
def _sync_motor_map_selection_toolbar(self):
|
||||
"""
|
||||
Sync the motor map selection toolbar with the current motor map.
|
||||
"""
|
||||
if self.motor_selection_bundle is not None:
|
||||
motor_x = self.motor_selection_bundle.motor_x.currentText()
|
||||
motor_y = self.motor_selection_bundle.motor_y.currentText()
|
||||
|
||||
if motor_x != self.config.x_motor.name:
|
||||
self.motor_selection_bundle.motor_x.blockSignals(True)
|
||||
self.motor_selection_bundle.motor_x.set_device(self.config.x_motor.name)
|
||||
self.motor_selection_bundle.motor_x.check_validity(self.config.x_motor.name)
|
||||
self.motor_selection_bundle.motor_x.blockSignals(False)
|
||||
if motor_y != self.config.y_motor.name:
|
||||
self.motor_selection_bundle.motor_y.blockSignals(True)
|
||||
self.motor_selection_bundle.motor_y.set_device(self.config.y_motor.name)
|
||||
self.motor_selection_bundle.motor_y.check_validity(self.config.y_motor.name)
|
||||
self.motor_selection_bundle.motor_y.blockSignals(False)
|
||||
|
||||
################################################################################
|
||||
# Export Methods
|
||||
################################################################################
|
||||
|
||||
def get_data(self) -> dict:
|
||||
"""
|
||||
Get the data of the motor map.
|
||||
|
||||
Returns:
|
||||
dict: Data of the motor map.
|
||||
"""
|
||||
data = {"x": self._buffer["x"], "y": self._buffer["y"]}
|
||||
return data
|
||||
|
||||
|
||||
class DemoApp(QMainWindow): # pragma: no cover
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setWindowTitle("Waveform Demo")
|
||||
self.resize(800, 600)
|
||||
self.main_widget = QWidget()
|
||||
self.layout = QHBoxLayout(self.main_widget)
|
||||
self.setCentralWidget(self.main_widget)
|
||||
|
||||
self.motor_map_popup = MotorMap(popups=True)
|
||||
self.motor_map_popup.map(x_name="samx", y_name="samy", validate_bec=True)
|
||||
|
||||
self.motor_map_side = MotorMap(popups=False)
|
||||
self.motor_map_side.map(x_name="samx", y_name="samy", validate_bec=True)
|
||||
|
||||
self.layout.addWidget(self.motor_map_side)
|
||||
self.layout.addWidget(self.motor_map_popup)
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import sys
|
||||
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
set_theme("dark")
|
||||
widget = DemoApp()
|
||||
widget.show()
|
||||
widget.resize(1400, 600)
|
||||
sys.exit(app.exec_())
|
@ -0,0 +1 @@
|
||||
{'files': ['motor_map.py']}
|
@ -0,0 +1,54 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.plots_next_gen.motor_map.motor_map import MotorMap
|
||||
|
||||
DOM_XML = """
|
||||
<ui language='c++'>
|
||||
<widget class='MotorMap' name='motor_map'>
|
||||
</widget>
|
||||
</ui>
|
||||
"""
|
||||
|
||||
|
||||
class MotorMapPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._form_editor = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
t = MotorMap(parent)
|
||||
return t
|
||||
|
||||
def domXml(self):
|
||||
return DOM_XML
|
||||
|
||||
def group(self):
|
||||
return "Plot Widgets Next Gen"
|
||||
|
||||
def icon(self):
|
||||
return designer_material_icon(MotorMap.ICON_NAME)
|
||||
|
||||
def includeFile(self):
|
||||
return "motor_map"
|
||||
|
||||
def initialize(self, form_editor):
|
||||
self._form_editor = form_editor
|
||||
|
||||
def isContainer(self):
|
||||
return False
|
||||
|
||||
def isInitialized(self):
|
||||
return self._form_editor is not None
|
||||
|
||||
def name(self):
|
||||
return "MotorMap"
|
||||
|
||||
def toolTip(self):
|
||||
return "MotorMap"
|
||||
|
||||
def whatsThis(self):
|
||||
return self.toolTip()
|
@ -0,0 +1,15 @@
|
||||
def main(): # pragma: no cover
|
||||
from qtpy import PYSIDE6
|
||||
|
||||
if not PYSIDE6:
|
||||
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
|
||||
return
|
||||
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
|
||||
|
||||
from bec_widgets.widgets.plots_next_gen.motor_map.motor_map_plugin import MotorMapPlugin
|
||||
|
||||
QPyDesignerCustomWidgetCollection.addCustomWidget(MotorMapPlugin())
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main()
|
@ -0,0 +1,130 @@
|
||||
import os
|
||||
|
||||
from qtpy.QtWidgets import QFrame, QScrollArea, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.qt_utils.error_popups import SafeSlot
|
||||
from bec_widgets.qt_utils.settings_dialog import SettingWidget
|
||||
from bec_widgets.utils import UILoader
|
||||
from bec_widgets.utils.widget_io import WidgetIO
|
||||
|
||||
|
||||
class MotorMapSettings(SettingWidget):
|
||||
"""
|
||||
A settings widget for the MotorMap widget.
|
||||
|
||||
The widget has skip_settings property set to True, which means it should not be saved
|
||||
in the settings file. It is used to mirror the properties of the target widget.
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None, target_widget=None, popup=False, *args, **kwargs):
|
||||
super().__init__(parent=parent, *args, **kwargs)
|
||||
|
||||
self.setProperty("skip_settings", True)
|
||||
self.setObjectName("MotorMapSettings")
|
||||
current_path = os.path.dirname(__file__)
|
||||
|
||||
form = UILoader().load_ui(os.path.join(current_path, "motor_map_settings.ui"), self)
|
||||
|
||||
self.target_widget = target_widget
|
||||
self.popup = popup
|
||||
|
||||
# # Scroll area
|
||||
self.scroll_area = QScrollArea(self)
|
||||
self.scroll_area.setWidgetResizable(True)
|
||||
self.scroll_area.setFrameShape(QFrame.NoFrame)
|
||||
self.scroll_area.setWidget(form)
|
||||
|
||||
self.layout = QVBoxLayout(self)
|
||||
self.layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.layout.addWidget(self.scroll_area)
|
||||
self.ui = form
|
||||
|
||||
self.ui_widget_list = [
|
||||
self.ui.max_points,
|
||||
self.ui.num_dim_points,
|
||||
self.ui.precision,
|
||||
self.ui.scatter_size,
|
||||
self.ui.background_value,
|
||||
]
|
||||
|
||||
if self.target_widget is not None and self.popup is False:
|
||||
self.connect_all_signals()
|
||||
self.target_widget.property_changed.connect(self.update_property)
|
||||
|
||||
self.fetch_all_properties()
|
||||
|
||||
def connect_all_signals(self):
|
||||
for widget in self.ui_widget_list:
|
||||
WidgetIO.connect_widget_change_signal(widget, self.set_property)
|
||||
self.ui.color_scatter.color_selected.connect(
|
||||
lambda color: self.target_widget.setProperty("color_scatter", color)
|
||||
)
|
||||
|
||||
@SafeSlot()
|
||||
def set_property(self, widget: QWidget, value):
|
||||
"""
|
||||
Set property of the target widget based on the widget that emitted the signal.
|
||||
The name of the property has to be the same as the objectName of the widget
|
||||
and compatible with WidgetIO.
|
||||
|
||||
Args:
|
||||
widget(QWidget): The widget that emitted the signal.
|
||||
value(): The value to set the property to.
|
||||
"""
|
||||
|
||||
try: # to avoid crashing when the widget is not found in Designer
|
||||
property_name = widget.objectName()
|
||||
setattr(self.target_widget, property_name, value)
|
||||
except RuntimeError:
|
||||
return
|
||||
if property_name == "color_scatter":
|
||||
# Update the color scatter button
|
||||
self.ui.color_scatter.set_color(value)
|
||||
|
||||
@SafeSlot()
|
||||
def update_property(self, property_name: str, value):
|
||||
"""
|
||||
Update the value of the widget based on the property name and value.
|
||||
The name of the property has to be the same as the objectName of the widget
|
||||
and compatible with WidgetIO.
|
||||
|
||||
Args:
|
||||
property_name(str): The name of the property to update.
|
||||
value: The value to set the property to.
|
||||
"""
|
||||
try: # to avoid crashing when the widget is not found in Designer
|
||||
widget_to_set = self.ui.findChild(QWidget, property_name)
|
||||
except RuntimeError:
|
||||
return
|
||||
if widget_to_set is None:
|
||||
return
|
||||
if widget_to_set is self.ui.color_scatter:
|
||||
# Update the color scatter button
|
||||
self.ui.color_scatter.set_color(value)
|
||||
return
|
||||
# Block signals to avoid triggering set_property again
|
||||
was_blocked = widget_to_set.blockSignals(True)
|
||||
WidgetIO.set_value(widget_to_set, value)
|
||||
widget_to_set.blockSignals(was_blocked)
|
||||
|
||||
def fetch_all_properties(self):
|
||||
"""
|
||||
Fetch all properties from the target widget and update the settings widget.
|
||||
"""
|
||||
for widget in self.ui_widget_list:
|
||||
property_name = widget.objectName()
|
||||
value = getattr(self.target_widget, property_name)
|
||||
WidgetIO.set_value(widget, value)
|
||||
|
||||
self.ui.color_scatter.set_color(self.target_widget.color)
|
||||
|
||||
def accept_changes(self):
|
||||
"""
|
||||
Apply all properties from the settings widget to the target widget.
|
||||
"""
|
||||
for widget in self.ui_widget_list:
|
||||
property_name = widget.objectName()
|
||||
value = WidgetIO.get_value(widget)
|
||||
setattr(self.target_widget, property_name, value)
|
||||
|
||||
self.target_widget.color_scatter = self.ui.color_scatter.get_color()
|
@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Form</class>
|
||||
<widget class="QWidget" name="Form">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>235</width>
|
||||
<height>228</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>228</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>228</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="max_point_label">
|
||||
<property name="text">
|
||||
<string>Max Points</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QSpinBox" name="max_points">
|
||||
<property name="maximum">
|
||||
<number>10000</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="trace_label">
|
||||
<property name="text">
|
||||
<string>Trace Dim</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QSpinBox" name="num_dim_points">
|
||||
<property name="maximum">
|
||||
<number>1000</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="precision_label">
|
||||
<property name="text">
|
||||
<string>Precision</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QSpinBox" name="precision">
|
||||
<property name="maximum">
|
||||
<number>15</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="scatter_size_label">
|
||||
<property name="text">
|
||||
<string>Scatter Size</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QSpinBox" name="scatter_size">
|
||||
<property name="maximum">
|
||||
<number>20</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="background_label">
|
||||
<property name="text">
|
||||
<string>Background Intensity</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<widget class="QSpinBox" name="background_value">
|
||||
<property name="maximum">
|
||||
<number>100</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<widget class="QLabel" name="color_label">
|
||||
<property name="text">
|
||||
<string>Color</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="1">
|
||||
<widget class="ColorButton" name="color_scatter"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>ColorButton</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>color_button</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
@ -0,0 +1,60 @@
|
||||
from bec_lib.device import ReadoutPriority
|
||||
from qtpy.QtCore import Qt
|
||||
from qtpy.QtWidgets import QStyledItemDelegate
|
||||
|
||||
from bec_widgets.qt_utils.error_popups import SafeSlot
|
||||
from bec_widgets.qt_utils.toolbar import ToolbarBundle, WidgetAction
|
||||
from bec_widgets.widgets.control.device_input.base_classes.device_input_base import BECDeviceFilter
|
||||
from bec_widgets.widgets.control.device_input.device_combobox.device_combobox import DeviceComboBox
|
||||
|
||||
|
||||
class NoCheckDelegate(QStyledItemDelegate):
|
||||
"""To reduce space in combo boxes by removing the checkmark."""
|
||||
|
||||
def initStyleOption(self, option, index):
|
||||
super().initStyleOption(option, index)
|
||||
# Remove any check indicator
|
||||
option.checkState = Qt.Unchecked
|
||||
|
||||
|
||||
class MotorSelectionToolbarBundle(ToolbarBundle):
|
||||
"""
|
||||
A bundle of actions for a toolbar that selects motors.
|
||||
"""
|
||||
|
||||
def __init__(self, bundle_id="motor_selection", target_widget=None, **kwargs):
|
||||
super().__init__(bundle_id=bundle_id, actions=[], **kwargs)
|
||||
self.target_widget = target_widget
|
||||
|
||||
# Motor X
|
||||
self.motor_x = DeviceComboBox(device_filter=[BECDeviceFilter.POSITIONER])
|
||||
self.motor_x.addItem("", None)
|
||||
self.motor_x.setCurrentText("")
|
||||
self.motor_x.setToolTip("Select Motor X")
|
||||
self.motor_x.setItemDelegate(NoCheckDelegate(self.motor_x))
|
||||
|
||||
# Motor X
|
||||
self.motor_y = DeviceComboBox(device_filter=[BECDeviceFilter.POSITIONER])
|
||||
self.motor_y.addItem("", None)
|
||||
self.motor_y.setCurrentText("")
|
||||
self.motor_y.setToolTip("Select Motor Y")
|
||||
self.motor_y.setItemDelegate(NoCheckDelegate(self.motor_y))
|
||||
|
||||
self.add_action("motor_x", WidgetAction(widget=self.motor_x, adjust_size=False))
|
||||
self.add_action("motor_y", WidgetAction(widget=self.motor_y, adjust_size=False))
|
||||
|
||||
# Connect slots, a device will be connected upon change of any combobox
|
||||
self.motor_x.currentTextChanged.connect(lambda: self.connect_motors())
|
||||
self.motor_y.currentTextChanged.connect(lambda: self.connect_motors())
|
||||
|
||||
@SafeSlot()
|
||||
def connect_motors(self):
|
||||
motor_x = self.motor_x.currentText()
|
||||
motor_y = self.motor_y.currentText()
|
||||
|
||||
if motor_x != "" and motor_y != "":
|
||||
if (
|
||||
motor_x != self.target_widget.config.x_motor.name
|
||||
or motor_y != self.target_widget.config.y_motor.name
|
||||
):
|
||||
self.target_widget.map(motor_x, motor_y)
|
@ -378,6 +378,14 @@ class PlotBase(BECWidget, QWidget):
|
||||
**kwargs: Keyword arguments for the properties to be set.
|
||||
|
||||
Possible properties:
|
||||
- title: str
|
||||
- x_label: str
|
||||
- y_label: str
|
||||
- x_scale: Literal["linear", "log"]
|
||||
- y_scale: Literal["linear", "log"]
|
||||
- x_lim: tuple
|
||||
- y_lim: tuple
|
||||
- legend_label_size: int
|
||||
|
||||
"""
|
||||
property_map = {
|
||||
|
322
tests/unit_tests/test_motor_map_next_gen.py
Normal file
322
tests/unit_tests/test_motor_map_next_gen.py
Normal file
@ -0,0 +1,322 @@
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
|
||||
from bec_widgets.widgets.plots_next_gen.motor_map.motor_map import MotorMap
|
||||
from tests.unit_tests.client_mocks import mocked_client
|
||||
|
||||
from .conftest import create_widget
|
||||
|
||||
|
||||
def test_motor_map_initialization(qtbot, mocked_client):
|
||||
"""Test the initialization of the MotorMap widget."""
|
||||
mm = create_widget(qtbot, MotorMap, client=mocked_client)
|
||||
|
||||
# test default values
|
||||
assert mm.config.widget_class == "MotorMap"
|
||||
assert mm.config.scatter_size == 5
|
||||
assert mm.config.max_points == 5000
|
||||
assert mm.config.num_dim_points == 100
|
||||
assert mm.x_grid is True
|
||||
assert mm.y_grid is True
|
||||
|
||||
|
||||
def test_motor_map_select_motor(qtbot, mocked_client):
|
||||
"""Test selecting motors for the motor map."""
|
||||
mm = create_widget(qtbot, MotorMap, client=mocked_client)
|
||||
|
||||
mm.map(x_name="samx", y_name="samy", validate_bec=True)
|
||||
|
||||
assert mm.config.x_motor.name == "samx"
|
||||
assert mm.config.y_motor.name == "samy"
|
||||
assert mm.config.x_motor.limits == [-10, 10]
|
||||
assert mm.config.y_motor.limits == [-5, 5]
|
||||
assert mm.config.scatter_size == 5
|
||||
assert mm.config.max_points == 5000
|
||||
assert mm.config.num_dim_points == 100
|
||||
assert mm.x_grid is True
|
||||
assert mm.y_grid is True
|
||||
|
||||
|
||||
def test_motor_map_properties(qtbot, mocked_client):
|
||||
"""Test setting and getting properties of MotorMap."""
|
||||
mm = create_widget(qtbot, MotorMap, client=mocked_client)
|
||||
mm.map(x_name="samx", y_name="samy")
|
||||
|
||||
# Test color property
|
||||
mm.color = (100, 150, 200, 255)
|
||||
assert mm.color == (100, 150, 200, 255)
|
||||
|
||||
mm.color = "#FF5500" # Test hex color
|
||||
assert mm.color[0] == 255
|
||||
assert mm.color[1] == 85
|
||||
assert mm.color[2] == 0
|
||||
|
||||
# Test scatter_size property
|
||||
mm.scatter_size = 10
|
||||
qtbot.wait(200)
|
||||
assert mm.scatter_size == 10
|
||||
assert mm.config.scatter_size == 10
|
||||
assert mm._trace.opts["size"] == 10
|
||||
|
||||
# Test max_points property
|
||||
mm.max_points = 2000
|
||||
assert mm.max_points == 2000
|
||||
assert mm.config.max_points == 2000
|
||||
|
||||
# Test precision property
|
||||
mm.precision = 3
|
||||
assert mm.precision == 3
|
||||
assert mm.config.precision == 3
|
||||
|
||||
# Test num_dim_points property
|
||||
mm.num_dim_points = 50
|
||||
assert mm.num_dim_points == 50
|
||||
assert mm.config.num_dim_points == 50
|
||||
|
||||
# Test background_value property
|
||||
mm.background_value = 40
|
||||
qtbot.wait(200)
|
||||
assert mm.background_value == 40
|
||||
assert mm.config.background_value == 40
|
||||
np.all(mm._limit_map.image == 40.0)
|
||||
|
||||
|
||||
def test_motor_map_get_limits(qtbot, mocked_client):
|
||||
"""Test getting motor limits."""
|
||||
mm = create_widget(qtbot, MotorMap, client=mocked_client)
|
||||
mm.map(x_name="samx", y_name="samy")
|
||||
expected_limits = {"samx": [-10, 10], "samy": [-5, 5]}
|
||||
|
||||
for motor_name, expected_limit in expected_limits.items():
|
||||
actual_limit = mm._get_motor_limit(motor_name)
|
||||
assert actual_limit == expected_limit
|
||||
|
||||
|
||||
def test_motor_map_get_init_position(qtbot, mocked_client):
|
||||
"""Test getting the initial position of motors."""
|
||||
mm = create_widget(qtbot, MotorMap, client=mocked_client)
|
||||
mm.map("samx", "samy")
|
||||
mm.precision = 2
|
||||
|
||||
motor_map_dev = mm.client.device_manager.devices
|
||||
|
||||
expected_positions = {
|
||||
("samx", "samx"): motor_map_dev["samx"].read()["samx"]["value"],
|
||||
("samy", "samy"): motor_map_dev["samy"].read()["samy"]["value"],
|
||||
}
|
||||
|
||||
for (motor_name, entry), expected_position in expected_positions.items():
|
||||
actual_position = mm._get_motor_init_position(motor_name, 2)
|
||||
assert actual_position == expected_position
|
||||
|
||||
|
||||
def test_motor_map_reset_history(qtbot, mocked_client):
|
||||
"""Test resetting the history of motor positions."""
|
||||
mm = create_widget(qtbot, MotorMap, client=mocked_client)
|
||||
mm.map(motor_x="samx", motor_y="samy")
|
||||
|
||||
# Simulate some motor movement history
|
||||
mm._buffer = {"x": [1.0, 2.0, 3.0, 4.0], "y": [5.0, 6.0, 7.0, 8.0]}
|
||||
|
||||
# Reset history
|
||||
mm.reset_history()
|
||||
|
||||
# Should keep only the last point
|
||||
assert len(mm._buffer["x"]) == 1
|
||||
assert len(mm._buffer["y"]) == 1
|
||||
assert mm._buffer["x"][0] == 4.0
|
||||
assert mm._buffer["y"][0] == 8.0
|
||||
|
||||
|
||||
def test_motor_map_on_device_readback(qtbot, mocked_client):
|
||||
"""Test the motor map updates when receiving device readback."""
|
||||
mm = create_widget(qtbot, MotorMap, client=mocked_client)
|
||||
mm.map(x_name="samx", y_name="samy")
|
||||
|
||||
# Clear the buffer and add initial position
|
||||
mm._buffer = {"x": [1.0], "y": [2.0]}
|
||||
|
||||
# Simulate device readback for x motor
|
||||
msg_x = {"signals": {"samx": {"value": 3.0}}}
|
||||
mm.on_device_readback(msg_x, {})
|
||||
qtbot.wait(200) # Allow time for the update to process
|
||||
|
||||
assert len(mm._buffer["x"]) == 2
|
||||
assert len(mm._buffer["y"]) == 2
|
||||
assert mm._buffer["x"][1] == 3.0
|
||||
assert mm._buffer["y"][1] == 2.0 # Y should remain the same
|
||||
|
||||
# Simulate device readback for y motor
|
||||
msg_y = {"signals": {"samy": {"value": 4.0}}}
|
||||
mm.on_device_readback(msg_y, {})
|
||||
|
||||
assert len(mm._buffer["x"]) == 3
|
||||
assert len(mm._buffer["y"]) == 3
|
||||
assert mm._buffer["x"][2] == 3.0 # X should remain the same
|
||||
assert mm._buffer["y"][2] == 4.0
|
||||
|
||||
|
||||
def test_motor_map_max_points_limit(qtbot, mocked_client):
|
||||
"""Test that the buffer doesn't exceed max_points."""
|
||||
mm = create_widget(qtbot, MotorMap, client=mocked_client)
|
||||
mm.map(x_name="samx", y_name="samy")
|
||||
|
||||
# Add more points than max_points
|
||||
mm._buffer = {"x": [1.0, 2.0, 3.0, 4.0], "y": [5.0, 6.0, 7.0, 8.0]}
|
||||
|
||||
mm.config.max_points = 3
|
||||
# Trigger update that should trim buffer
|
||||
mm._update_plot()
|
||||
|
||||
# Check that buffer was trimmed to max_points
|
||||
assert len(mm._buffer["x"]) == 3
|
||||
assert len(mm._buffer["y"]) == 3
|
||||
# Should keep the most recent points
|
||||
assert mm._buffer["x"] == [2.0, 3.0, 4.0]
|
||||
assert mm._buffer["y"] == [6.0, 7.0, 8.0]
|
||||
|
||||
|
||||
def test_motor_map_crosshair_creation(qtbot, mocked_client):
|
||||
"""Test the creation of the coordinate crosshair."""
|
||||
mm = create_widget(qtbot, MotorMap, client=mocked_client)
|
||||
|
||||
# The Initial state should be None
|
||||
assert mm.v_line is None
|
||||
assert mm.h_line is None
|
||||
assert mm.coord_label is None
|
||||
|
||||
# Create the crosshair
|
||||
mm._add_coordinates_crosshair(3.0, 4.0)
|
||||
|
||||
# Check if crosshair elements were created
|
||||
assert mm.v_line is not None
|
||||
assert mm.h_line is not None
|
||||
assert mm.coord_label is not None
|
||||
|
||||
# Test position
|
||||
assert mm.v_line.value() == 3.0
|
||||
assert mm.h_line.value() == 4.0
|
||||
|
||||
|
||||
def test_motor_map_limit_map(qtbot, mocked_client):
|
||||
"""Test the creation of the limit map."""
|
||||
mm = create_widget(qtbot, MotorMap, client=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)
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
def test_motor_map_change_limits(qtbot, mocked_client):
|
||||
mm = create_widget(qtbot, MotorMap, client=mocked_client)
|
||||
mm.map(x_name="samx", y_name="samy")
|
||||
|
||||
# Original mocked limits are
|
||||
# samx: [-10, 10]
|
||||
# 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
|
||||
assert mm.config.x_motor.limits == [-10, 10]
|
||||
assert mm.config.y_motor.limits == [-5, 5]
|
||||
|
||||
# Change the limits of the samx motor
|
||||
mm.dev["samx"].limits = [-20, 20]
|
||||
msg = {"signals": {"high": {"value": 20}, "low": {"value": -20}}}
|
||||
mm.on_device_limits(msg, {})
|
||||
qtbot.wait(200) # Allow time for the update to process
|
||||
|
||||
# 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
|
||||
|
||||
# Change back the limits
|
||||
mm.dev["samx"].limits = [-10, 10]
|
||||
|
||||
|
||||
def test_motor_map_get_data(qtbot, mocked_client):
|
||||
"""Test getting data from the motor map."""
|
||||
mm = create_widget(qtbot, MotorMap, client=mocked_client)
|
||||
|
||||
# Set up some test data
|
||||
mm._buffer = {"x": [1.0, 2.0, 3.0], "y": [4.0, 5.0, 6.0]}
|
||||
|
||||
# Get data
|
||||
data = mm.get_data()
|
||||
|
||||
# Check that the data is correct
|
||||
assert "x" in data
|
||||
assert "y" in data
|
||||
assert data["x"] == [1.0, 2.0, 3.0]
|
||||
assert data["y"] == [4.0, 5.0, 6.0]
|
||||
|
||||
|
||||
def test_motor_map_toolbar_selection(qtbot, mocked_client):
|
||||
"""Test motor selection via the toolbar bundle."""
|
||||
mm = create_widget(qtbot, MotorMap, client=mocked_client)
|
||||
|
||||
# Verify toolbar bundle was created during initialization
|
||||
assert hasattr(mm, "motor_selection_bundle")
|
||||
assert mm.motor_selection_bundle is not None
|
||||
|
||||
mm.motor_selection_bundle.motor_x.setCurrentText("samx")
|
||||
mm.motor_selection_bundle.motor_y.setCurrentText("samy")
|
||||
|
||||
assert mm.config.x_motor.name == "samx"
|
||||
assert mm.config.y_motor.name == "samy"
|
||||
|
||||
mm.motor_selection_bundle.motor_y.setCurrentText("samz")
|
||||
|
||||
assert mm.config.x_motor.name == "samx"
|
||||
assert mm.config.y_motor.name == "samz"
|
||||
|
||||
|
||||
def test_motor_map_settings_dialog(qtbot, mocked_client):
|
||||
"""Test the settings dialog for the motor map."""
|
||||
mm = create_widget(qtbot, MotorMap, client=mocked_client, popups=True)
|
||||
|
||||
assert "popup_bundle" in mm.toolbar.bundles
|
||||
for action_id in mm.toolbar.bundles["popup_bundle"]:
|
||||
assert mm.toolbar.widgets[action_id].action.isVisible() is True
|
||||
|
||||
# set properties to be fetched by dialog
|
||||
mm.map(x_name="samx", y_name="samy")
|
||||
mm.precision = 2
|
||||
mm.max_points = 1000
|
||||
mm.scatter_size = 10
|
||||
mm.background_value = 50
|
||||
mm.num_dim_points = 20
|
||||
mm.color = (255, 0, 0, 255)
|
||||
|
||||
mm.show_motor_map_settings()
|
||||
qtbot.wait(200)
|
||||
|
||||
assert mm.motor_map_settings is not None
|
||||
assert mm.motor_map_settings.isVisible() is True
|
||||
|
||||
# Check that the settings dialog has the correct values
|
||||
assert mm.motor_map_settings.widget.ui.precision.value() == 2
|
||||
assert mm.motor_map_settings.widget.ui.max_points.value() == 1000
|
||||
assert mm.motor_map_settings.widget.ui.scatter_size.value() == 10
|
||||
assert mm.motor_map_settings.widget.ui.background_value.value() == 50
|
||||
assert mm.motor_map_settings.widget.ui.num_dim_points.value() == 20
|
||||
assert mm.motor_map_settings.widget.ui.color_scatter.get_color(format="RGBA") == (
|
||||
255,
|
||||
0,
|
||||
0,
|
||||
255,
|
||||
)
|
||||
|
||||
mm.motor_map_settings.close()
|
||||
qtbot.wait(200)
|
||||
assert mm.motor_map_settings is None
|
Reference in New Issue
Block a user