mirror of
https://github.com/bec-project/bec_widgets.git
synced 2025-07-14 11:41:49 +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"
|
LMFitDialog = "LMFitDialog"
|
||||||
LogPanel = "LogPanel"
|
LogPanel = "LogPanel"
|
||||||
Minesweeper = "Minesweeper"
|
Minesweeper = "Minesweeper"
|
||||||
|
MotorMap = "MotorMap"
|
||||||
PositionIndicator = "PositionIndicator"
|
PositionIndicator = "PositionIndicator"
|
||||||
PositionerBox = "PositionerBox"
|
PositionerBox = "PositionerBox"
|
||||||
PositionerBox2D = "PositionerBox2D"
|
PositionerBox2D = "PositionerBox2D"
|
||||||
@ -2727,6 +2728,14 @@ class Image(RPCBase):
|
|||||||
**kwargs: Keyword arguments for the properties to be set.
|
**kwargs: Keyword arguments for the properties to be set.
|
||||||
|
|
||||||
Possible properties:
|
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
|
@property
|
||||||
@ -3353,6 +3362,395 @@ class LogPanel(RPCBase):
|
|||||||
class Minesweeper(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):
|
class PositionIndicator(RPCBase):
|
||||||
@rpc_call
|
@rpc_call
|
||||||
def set_value(self, position: float):
|
def set_value(self, position: float):
|
||||||
@ -3780,6 +4178,8 @@ class ScanMetadata(RPCBase):
|
|||||||
|
|
||||||
|
|
||||||
class ScatterCurve(RPCBase):
|
class ScatterCurve(RPCBase):
|
||||||
|
"""Scatter curve item for the scatter waveform widget."""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@rpc_call
|
@rpc_call
|
||||||
def color_map(self) -> "str":
|
def color_map(self) -> "str":
|
||||||
@ -3840,6 +4240,14 @@ class ScatterWaveform(RPCBase):
|
|||||||
**kwargs: Keyword arguments for the properties to be set.
|
**kwargs: Keyword arguments for the properties to be set.
|
||||||
|
|
||||||
Possible properties:
|
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
|
@property
|
||||||
@ -4232,6 +4640,14 @@ class Waveform(RPCBase):
|
|||||||
**kwargs: Keyword arguments for the properties to be set.
|
**kwargs: Keyword arguments for the properties to be set.
|
||||||
|
|
||||||
Possible properties:
|
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
|
@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.
|
**kwargs: Keyword arguments for the properties to be set.
|
||||||
|
|
||||||
Possible properties:
|
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 = {
|
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