bec-widgets 0.44.5__py3-none-any.whl → 0.45.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bec_widgets/cli/client.py +100 -1
- bec_widgets/cli/generate_cli.py +2 -1
- bec_widgets/widgets/__init__.py +1 -1
- bec_widgets/widgets/figure/figure.py +72 -1
- bec_widgets/widgets/plots/__init__.py +1 -0
- bec_widgets/widgets/plots/motor_map.py +423 -0
- bec_widgets/widgets/plots/plot_base.py +1 -1
- bec_widgets/widgets/plots/waveform1d.py +3 -2
- {bec_widgets-0.44.5.dist-info → bec_widgets-0.45.0.dist-info}/METADATA +1 -1
- {bec_widgets-0.44.5.dist-info → bec_widgets-0.45.0.dist-info}/RECORD +21 -19
- tests/client_mocks.py +76 -31
- tests/test_bec_figure.py +12 -1
- tests/test_bec_monitor.py +2 -66
- tests/test_bec_motor_map.py +125 -0
- tests/test_config_dialog.py +2 -63
- tests/test_motor_control.py +17 -83
- tests/test_motor_map.py +9 -66
- tests/test_waveform1d.py +26 -8
- {bec_widgets-0.44.5.dist-info → bec_widgets-0.45.0.dist-info}/LICENSE +0 -0
- {bec_widgets-0.44.5.dist-info → bec_widgets-0.45.0.dist-info}/WHEEL +0 -0
- {bec_widgets-0.44.5.dist-info → bec_widgets-0.45.0.dist-info}/top_level.txt +0 -0
bec_widgets/cli/client.py
CHANGED
@@ -450,12 +450,35 @@ class BECFigure(RPCBase, BECFigureClientMixin):
|
|
450
450
|
row(int): The row coordinate of the widget in the figure. If not provided, the next empty row will be used.
|
451
451
|
col(int): The column coordinate of the widget in the figure. If not provided, the next empty column will be used.
|
452
452
|
config(dict): Additional configuration for the widget.
|
453
|
-
**axis_kwargs:
|
453
|
+
**axis_kwargs: Additional axis properties to set on the widget after creation.
|
454
454
|
|
455
455
|
Returns:
|
456
456
|
BECImageShow: The image widget.
|
457
457
|
"""
|
458
458
|
|
459
|
+
@rpc_call
|
460
|
+
def add_motor_map(
|
461
|
+
self,
|
462
|
+
motor_x: "str" = None,
|
463
|
+
motor_y: "str" = None,
|
464
|
+
row: "int" = None,
|
465
|
+
col: "int" = None,
|
466
|
+
config=None,
|
467
|
+
**axis_kwargs
|
468
|
+
) -> "BECMotorMap":
|
469
|
+
"""
|
470
|
+
Args:
|
471
|
+
motor_x(str): The name of the motor for the X axis.
|
472
|
+
motor_y(str): The name of the motor for the Y axis.
|
473
|
+
row(int): The row coordinate of the widget in the figure. If not provided, the next empty row will be used.
|
474
|
+
col(int): The column coordinate of the widget in the figure. If not provided, the next empty column will be used.
|
475
|
+
config(dict): Additional configuration for the widget.
|
476
|
+
**axis_kwargs:
|
477
|
+
|
478
|
+
Returns:
|
479
|
+
BECMotorMap: The motor map widget.
|
480
|
+
"""
|
481
|
+
|
459
482
|
@rpc_call
|
460
483
|
def plot(
|
461
484
|
self,
|
@@ -512,6 +535,21 @@ class BECFigure(RPCBase, BECFigureClientMixin):
|
|
512
535
|
BECImageShow: The image widget.
|
513
536
|
"""
|
514
537
|
|
538
|
+
@rpc_call
|
539
|
+
def motor_map(
|
540
|
+
self, motor_x: "str" = None, motor_y: "str" = None, **axis_kwargs
|
541
|
+
) -> "BECMotorMap":
|
542
|
+
"""
|
543
|
+
Add a motor map to the figure. Always access the first motor map widget in the figure.
|
544
|
+
Args:
|
545
|
+
motor_x(str): The name of the motor for the X axis.
|
546
|
+
motor_y(str): The name of the motor for the Y axis.
|
547
|
+
**axis_kwargs: Additional axis properties to set on the widget after creation.
|
548
|
+
|
549
|
+
Returns:
|
550
|
+
BECMotorMap: The motor map widget.
|
551
|
+
"""
|
552
|
+
|
515
553
|
@rpc_call
|
516
554
|
def remove(
|
517
555
|
self,
|
@@ -1091,3 +1129,64 @@ class BECImageItem(RPCBase):
|
|
1091
1129
|
Returns:
|
1092
1130
|
dict: The configuration of the plot widget.
|
1093
1131
|
"""
|
1132
|
+
|
1133
|
+
|
1134
|
+
class BECMotorMap(RPCBase):
|
1135
|
+
@rpc_call
|
1136
|
+
def change_motors(
|
1137
|
+
self,
|
1138
|
+
motor_x: "str",
|
1139
|
+
motor_y: "str",
|
1140
|
+
motor_x_entry: "str" = None,
|
1141
|
+
motor_y_entry: "str" = None,
|
1142
|
+
validate_bec: "bool" = True,
|
1143
|
+
) -> "None":
|
1144
|
+
"""
|
1145
|
+
Change the active motors for the plot.
|
1146
|
+
Args:
|
1147
|
+
motor_x(str): Motor name for the X axis.
|
1148
|
+
motor_y(str): Motor name for the Y axis.
|
1149
|
+
motor_x_entry(str): Motor entry for the X axis.
|
1150
|
+
motor_y_entry(str): Motor entry for the Y axis.
|
1151
|
+
validate_bec(bool, optional): If True, validate the signal with BEC. Defaults to True.
|
1152
|
+
"""
|
1153
|
+
|
1154
|
+
@rpc_call
|
1155
|
+
def set_max_points(self, max_points: "int") -> "None":
|
1156
|
+
"""
|
1157
|
+
Set the maximum number of points to display.
|
1158
|
+
Args:
|
1159
|
+
max_points(int): Maximum number of points to display.
|
1160
|
+
"""
|
1161
|
+
|
1162
|
+
@rpc_call
|
1163
|
+
def set_precision(self, precision: "int") -> "None":
|
1164
|
+
"""
|
1165
|
+
Set the decimal precision of the motor position.
|
1166
|
+
Args:
|
1167
|
+
precision(int): Decimal precision of the motor position.
|
1168
|
+
"""
|
1169
|
+
|
1170
|
+
@rpc_call
|
1171
|
+
def set_num_dim_points(self, num_dim_points: "int") -> "None":
|
1172
|
+
"""
|
1173
|
+
Set the number of dim points for the motor map.
|
1174
|
+
Args:
|
1175
|
+
num_dim_points(int): Number of dim points.
|
1176
|
+
"""
|
1177
|
+
|
1178
|
+
@rpc_call
|
1179
|
+
def set_background_value(self, background_value: "int") -> "None":
|
1180
|
+
"""
|
1181
|
+
Set the background value of the motor map.
|
1182
|
+
Args:
|
1183
|
+
background_value(int): Background value of the motor map.
|
1184
|
+
"""
|
1185
|
+
|
1186
|
+
@rpc_call
|
1187
|
+
def set_scatter_size(self, scatter_size: "int") -> "None":
|
1188
|
+
"""
|
1189
|
+
Set the scatter size of the motor map plot.
|
1190
|
+
Args:
|
1191
|
+
scatter_size(int): Size of the scatter points.
|
1192
|
+
"""
|
bec_widgets/cli/generate_cli.py
CHANGED
@@ -109,7 +109,7 @@ if __name__ == "__main__": # pragma: no cover
|
|
109
109
|
|
110
110
|
from bec_widgets.utils import BECConnector
|
111
111
|
from bec_widgets.widgets.figure import BECFigure
|
112
|
-
from bec_widgets.widgets.plots import BECImageShow, BECPlotBase, BECWaveform1D
|
112
|
+
from bec_widgets.widgets.plots import BECImageShow, BECMotorMap, BECPlotBase, BECWaveform1D
|
113
113
|
from bec_widgets.widgets.plots.image import BECImageItem
|
114
114
|
from bec_widgets.widgets.plots.waveform1d import BECCurve
|
115
115
|
|
@@ -123,6 +123,7 @@ if __name__ == "__main__": # pragma: no cover
|
|
123
123
|
BECImageShow,
|
124
124
|
BECConnector,
|
125
125
|
BECImageItem,
|
126
|
+
BECMotorMap,
|
126
127
|
]
|
127
128
|
generator = ClientGenerator()
|
128
129
|
generator.generate_client(clss)
|
bec_widgets/widgets/__init__.py
CHANGED
@@ -17,12 +17,14 @@ from qtpy.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget
|
|
17
17
|
from bec_widgets.utils import BECConnector, BECDispatcher, ConnectionConfig
|
18
18
|
from bec_widgets.widgets.plots import (
|
19
19
|
BECImageShow,
|
20
|
+
BECMotorMap,
|
20
21
|
BECPlotBase,
|
21
22
|
BECWaveform1D,
|
22
23
|
Waveform1DConfig,
|
23
24
|
WidgetConfig,
|
24
25
|
)
|
25
26
|
from bec_widgets.widgets.plots.image import ImageConfig
|
27
|
+
from bec_widgets.widgets.plots.motor_map import MotorMapConfig
|
26
28
|
|
27
29
|
|
28
30
|
class FigureConfig(ConnectionConfig):
|
@@ -44,6 +46,7 @@ class WidgetHandler:
|
|
44
46
|
"PlotBase": (BECPlotBase, WidgetConfig),
|
45
47
|
"Waveform1D": (BECWaveform1D, Waveform1DConfig),
|
46
48
|
"ImShow": (BECImageShow, ImageConfig),
|
49
|
+
"MotorMap": (BECMotorMap, MotorMapConfig),
|
47
50
|
}
|
48
51
|
|
49
52
|
def create_widget(
|
@@ -98,8 +101,10 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
|
|
98
101
|
"widgets",
|
99
102
|
"add_plot",
|
100
103
|
"add_image",
|
104
|
+
"add_motor_map",
|
101
105
|
"plot",
|
102
106
|
"image",
|
107
|
+
"motor_map",
|
103
108
|
"remove",
|
104
109
|
"change_layout",
|
105
110
|
"change_theme",
|
@@ -347,7 +352,7 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
|
|
347
352
|
row(int): The row coordinate of the widget in the figure. If not provided, the next empty row will be used.
|
348
353
|
col(int): The column coordinate of the widget in the figure. If not provided, the next empty column will be used.
|
349
354
|
config(dict): Additional configuration for the widget.
|
350
|
-
**axis_kwargs:
|
355
|
+
**axis_kwargs: Additional axis properties to set on the widget after creation.
|
351
356
|
|
352
357
|
Returns:
|
353
358
|
BECImageShow: The image widget.
|
@@ -389,6 +394,72 @@ class BECFigure(BECConnector, pg.GraphicsLayoutWidget):
|
|
389
394
|
|
390
395
|
return image
|
391
396
|
|
397
|
+
def motor_map(self, motor_x: str = None, motor_y: str = None, **axis_kwargs) -> BECMotorMap:
|
398
|
+
"""
|
399
|
+
Add a motor map to the figure. Always access the first motor map widget in the figure.
|
400
|
+
Args:
|
401
|
+
motor_x(str): The name of the motor for the X axis.
|
402
|
+
motor_y(str): The name of the motor for the Y axis.
|
403
|
+
**axis_kwargs: Additional axis properties to set on the widget after creation.
|
404
|
+
|
405
|
+
Returns:
|
406
|
+
BECMotorMap: The motor map widget.
|
407
|
+
"""
|
408
|
+
motor_map = self._find_first_widget_by_class(BECMotorMap, can_fail=True)
|
409
|
+
if motor_map is not None:
|
410
|
+
if axis_kwargs:
|
411
|
+
motor_map.set(**axis_kwargs)
|
412
|
+
else:
|
413
|
+
motor_map = self.add_motor_map(**axis_kwargs)
|
414
|
+
|
415
|
+
if motor_x is not None and motor_y is not None:
|
416
|
+
motor_map.change_motors(motor_x, motor_y)
|
417
|
+
|
418
|
+
return motor_map
|
419
|
+
|
420
|
+
def add_motor_map(
|
421
|
+
self,
|
422
|
+
motor_x: str = None,
|
423
|
+
motor_y: str = None,
|
424
|
+
row: int = None,
|
425
|
+
col: int = None,
|
426
|
+
config=None,
|
427
|
+
**axis_kwargs,
|
428
|
+
) -> BECMotorMap:
|
429
|
+
"""
|
430
|
+
|
431
|
+
Args:
|
432
|
+
motor_x(str): The name of the motor for the X axis.
|
433
|
+
motor_y(str): The name of the motor for the Y axis.
|
434
|
+
row(int): The row coordinate of the widget in the figure. If not provided, the next empty row will be used.
|
435
|
+
col(int): The column coordinate of the widget in the figure. If not provided, the next empty column will be used.
|
436
|
+
config(dict): Additional configuration for the widget.
|
437
|
+
**axis_kwargs:
|
438
|
+
|
439
|
+
Returns:
|
440
|
+
BECMotorMap: The motor map widget.
|
441
|
+
"""
|
442
|
+
widget_id = self._generate_unique_widget_id()
|
443
|
+
if config is None:
|
444
|
+
config = MotorMapConfig(
|
445
|
+
widget_class="BECMotorMap",
|
446
|
+
gui_id=widget_id,
|
447
|
+
parent_id=self.gui_id,
|
448
|
+
)
|
449
|
+
motor_map = self.add_widget(
|
450
|
+
widget_type="MotorMap",
|
451
|
+
widget_id=widget_id,
|
452
|
+
row=row,
|
453
|
+
col=col,
|
454
|
+
config=config,
|
455
|
+
**axis_kwargs,
|
456
|
+
)
|
457
|
+
|
458
|
+
if motor_x is not None and motor_y is not None:
|
459
|
+
motor_map.change_motors(motor_x, motor_y)
|
460
|
+
|
461
|
+
return motor_map
|
462
|
+
|
392
463
|
def add_widget(
|
393
464
|
self,
|
394
465
|
widget_type: Literal["PlotBase", "Waveform1D", "ImShow"] = "PlotBase",
|
@@ -0,0 +1,423 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from collections import defaultdict
|
4
|
+
from typing import Optional, Union
|
5
|
+
|
6
|
+
import numpy as np
|
7
|
+
import pyqtgraph as pg
|
8
|
+
from bec_lib import MessageEndpoints
|
9
|
+
from pydantic import Field
|
10
|
+
from qtpy import QtCore, QtGui
|
11
|
+
from qtpy.QtCore import Signal as pyqtSignal
|
12
|
+
from qtpy.QtCore import Slot as pyqtSlot
|
13
|
+
from qtpy.QtWidgets import QWidget
|
14
|
+
|
15
|
+
from bec_widgets.utils import EntryValidator
|
16
|
+
from bec_widgets.widgets.plots.plot_base import BECPlotBase, WidgetConfig
|
17
|
+
from bec_widgets.widgets.plots.waveform1d import Signal, SignalData
|
18
|
+
|
19
|
+
|
20
|
+
class MotorMapConfig(WidgetConfig):
|
21
|
+
signals: Optional[Signal] = Field(None, description="Signals of the motor map")
|
22
|
+
color_map: Optional[str] = Field(
|
23
|
+
"Greys", description="Color scheme of the motor position gradient."
|
24
|
+
) # TODO decide if useful for anything, or just keep GREYS always
|
25
|
+
scatter_size: Optional[int] = Field(5, description="Size of the scatter points.")
|
26
|
+
max_points: Optional[int] = Field(1000, description="Maximum number of points to display.")
|
27
|
+
num_dim_points: Optional[int] = Field(
|
28
|
+
100,
|
29
|
+
description="Number of points to dim before the color remains same for older recorded position.",
|
30
|
+
)
|
31
|
+
precision: Optional[int] = Field(2, description="Decimal precision of the motor position.")
|
32
|
+
background_value: Optional[int] = Field(
|
33
|
+
25, description="Background value of the motor map."
|
34
|
+
) # TODO can be percentage from 255 calculated
|
35
|
+
|
36
|
+
|
37
|
+
class BECMotorMap(BECPlotBase):
|
38
|
+
USER_ACCESS = [
|
39
|
+
"change_motors",
|
40
|
+
"set_max_points",
|
41
|
+
"set_precision",
|
42
|
+
"set_num_dim_points",
|
43
|
+
"set_background_value",
|
44
|
+
"set_scatter_size",
|
45
|
+
]
|
46
|
+
|
47
|
+
# QT Signals
|
48
|
+
update_signal = pyqtSignal()
|
49
|
+
|
50
|
+
def __init__(
|
51
|
+
self,
|
52
|
+
parent: Optional[QWidget] = None,
|
53
|
+
parent_figure=None,
|
54
|
+
config: Optional[MotorMapConfig] = None,
|
55
|
+
client=None,
|
56
|
+
gui_id: Optional[str] = None,
|
57
|
+
):
|
58
|
+
if config is None:
|
59
|
+
config = MotorMapConfig(widget_class=self.__class__.__name__)
|
60
|
+
super().__init__(
|
61
|
+
parent=parent, parent_figure=parent_figure, config=config, client=client, gui_id=gui_id
|
62
|
+
)
|
63
|
+
|
64
|
+
# Get bec shortcuts dev, scans, queue, scan_storage, dap
|
65
|
+
self.get_bec_shortcuts()
|
66
|
+
self.entry_validator = EntryValidator(self.dev)
|
67
|
+
|
68
|
+
self.motor_x = None
|
69
|
+
self.motor_y = None
|
70
|
+
self.database_buffer = {"x": [], "y": []}
|
71
|
+
self.plot_components = defaultdict(dict) # container for plot components
|
72
|
+
|
73
|
+
# connect update signal to update plot
|
74
|
+
self.proxy_update_plot = pg.SignalProxy(
|
75
|
+
self.update_signal, rateLimit=25, slot=self._update_plot
|
76
|
+
)
|
77
|
+
|
78
|
+
# TODO decide if needed to implement, maybe there will be no children widgets for motormap for now...
|
79
|
+
# def find_widget_by_id(self, item_id: str) -> BECCurve:
|
80
|
+
# """
|
81
|
+
# Find the curve by its ID.
|
82
|
+
# Args:
|
83
|
+
# item_id(str): ID of the curve.
|
84
|
+
#
|
85
|
+
# Returns:
|
86
|
+
# BECCurve: The curve object.
|
87
|
+
# """
|
88
|
+
# for curve in self.plot_item.curves:
|
89
|
+
# if curve.gui_id == item_id:
|
90
|
+
# return curve
|
91
|
+
|
92
|
+
@pyqtSlot(str, str, str, str, bool)
|
93
|
+
def change_motors(
|
94
|
+
self,
|
95
|
+
motor_x: str,
|
96
|
+
motor_y: str,
|
97
|
+
motor_x_entry: str = None,
|
98
|
+
motor_y_entry: str = None,
|
99
|
+
validate_bec: bool = True,
|
100
|
+
) -> None:
|
101
|
+
"""
|
102
|
+
Change the active motors for the plot.
|
103
|
+
Args:
|
104
|
+
motor_x(str): Motor name for the X axis.
|
105
|
+
motor_y(str): Motor name for the Y axis.
|
106
|
+
motor_x_entry(str): Motor entry for the X axis.
|
107
|
+
motor_y_entry(str): Motor entry for the Y axis.
|
108
|
+
validate_bec(bool, optional): If True, validate the signal with BEC. Defaults to True.
|
109
|
+
"""
|
110
|
+
motor_x_entry, motor_y_entry = self._validate_signal_entries(
|
111
|
+
motor_x, motor_y, motor_x_entry, motor_y_entry, validate_bec
|
112
|
+
)
|
113
|
+
|
114
|
+
motor_x_limit = self._get_motor_limit(motor_x)
|
115
|
+
motor_y_limit = self._get_motor_limit(motor_y)
|
116
|
+
|
117
|
+
signal = Signal(
|
118
|
+
source="device_readback",
|
119
|
+
x=SignalData(name=motor_x, entry=motor_x_entry, limits=motor_x_limit),
|
120
|
+
y=SignalData(name=motor_y, entry=motor_y_entry, limits=motor_y_limit),
|
121
|
+
)
|
122
|
+
self.config.signals = signal
|
123
|
+
|
124
|
+
# reconnect the signals
|
125
|
+
self._connect_motor_to_slots()
|
126
|
+
|
127
|
+
# Redraw the motor map
|
128
|
+
self._make_motor_map()
|
129
|
+
|
130
|
+
# TODO setup all visual properties
|
131
|
+
def set_max_points(self, max_points: int) -> None:
|
132
|
+
"""
|
133
|
+
Set the maximum number of points to display.
|
134
|
+
Args:
|
135
|
+
max_points(int): Maximum number of points to display.
|
136
|
+
"""
|
137
|
+
self.config.max_points = max_points
|
138
|
+
|
139
|
+
def set_precision(self, precision: int) -> None:
|
140
|
+
"""
|
141
|
+
Set the decimal precision of the motor position.
|
142
|
+
Args:
|
143
|
+
precision(int): Decimal precision of the motor position.
|
144
|
+
"""
|
145
|
+
self.config.precision = precision
|
146
|
+
|
147
|
+
def set_num_dim_points(self, num_dim_points: int) -> None:
|
148
|
+
"""
|
149
|
+
Set the number of dim points for the motor map.
|
150
|
+
Args:
|
151
|
+
num_dim_points(int): Number of dim points.
|
152
|
+
"""
|
153
|
+
self.config.num_dim_points = num_dim_points
|
154
|
+
|
155
|
+
def set_background_value(self, background_value: int) -> None:
|
156
|
+
"""
|
157
|
+
Set the background value of the motor map.
|
158
|
+
Args:
|
159
|
+
background_value(int): Background value of the motor map.
|
160
|
+
"""
|
161
|
+
self.config.background_value = background_value
|
162
|
+
|
163
|
+
def set_scatter_size(self, scatter_size: int) -> None:
|
164
|
+
"""
|
165
|
+
Set the scatter size of the motor map plot.
|
166
|
+
Args:
|
167
|
+
scatter_size(int): Size of the scatter points.
|
168
|
+
"""
|
169
|
+
self.config.scatter_size = scatter_size
|
170
|
+
|
171
|
+
def _connect_motor_to_slots(self):
|
172
|
+
"""Connect motors to slots."""
|
173
|
+
if self.motor_x is not None and self.motor_y is not None:
|
174
|
+
old_endpoints = [
|
175
|
+
MessageEndpoints.device_readback(self.motor_x),
|
176
|
+
MessageEndpoints.device_readback(self.motor_y),
|
177
|
+
]
|
178
|
+
self.bec_dispatcher.disconnect_slot(self.on_device_readback, old_endpoints)
|
179
|
+
|
180
|
+
self.motor_x = self.config.signals.x.name
|
181
|
+
self.motor_y = self.config.signals.y.name
|
182
|
+
|
183
|
+
endpoints = [
|
184
|
+
MessageEndpoints.device_readback(self.motor_x),
|
185
|
+
MessageEndpoints.device_readback(self.motor_y),
|
186
|
+
]
|
187
|
+
|
188
|
+
self.bec_dispatcher.connect_slot(
|
189
|
+
self.on_device_readback, endpoints, single_callback_for_all_topics=True
|
190
|
+
)
|
191
|
+
|
192
|
+
def _make_motor_map(self):
|
193
|
+
"""
|
194
|
+
Create the motor map plot.
|
195
|
+
"""
|
196
|
+
# Create limit map
|
197
|
+
motor_x_limit = self.config.signals.x.limits
|
198
|
+
motor_y_limit = self.config.signals.y.limits
|
199
|
+
self.plot_components["limit_map"] = self._make_limit_map(motor_x_limit, motor_y_limit)
|
200
|
+
self.plot_item.addItem(self.plot_components["limit_map"])
|
201
|
+
self.plot_components["limit_map"].setZValue(-1)
|
202
|
+
|
203
|
+
# Create scatter plot
|
204
|
+
scatter_size = self.config.scatter_size
|
205
|
+
self.plot_components["scatter"] = pg.ScatterPlotItem(
|
206
|
+
size=scatter_size, brush=pg.mkBrush(255, 255, 255, 255)
|
207
|
+
)
|
208
|
+
self.plot_item.addItem(self.plot_components["scatter"])
|
209
|
+
self.plot_components["scatter"].setZValue(0)
|
210
|
+
|
211
|
+
# Enable Grid
|
212
|
+
self.set_grid(True, True)
|
213
|
+
|
214
|
+
# Add the crosshair for initial motor coordinates
|
215
|
+
initial_position_x = self._get_motor_init_position(
|
216
|
+
self.motor_x, self.config.signals.x.entry, self.config.precision
|
217
|
+
)
|
218
|
+
initial_position_y = self._get_motor_init_position(
|
219
|
+
self.motor_y, self.config.signals.y.entry, self.config.precision
|
220
|
+
)
|
221
|
+
|
222
|
+
self.database_buffer["x"] = [initial_position_x]
|
223
|
+
self.database_buffer["y"] = [initial_position_y]
|
224
|
+
|
225
|
+
self.plot_components["scatter"].setData([initial_position_x], [initial_position_y])
|
226
|
+
self._add_coordinantes_crosshair(initial_position_x, initial_position_y)
|
227
|
+
|
228
|
+
# Set default labels for the plot
|
229
|
+
self.set(x_label=f"Motor X ({self.motor_x})", y_label=f"Motor Y ({self.motor_y})")
|
230
|
+
|
231
|
+
def _add_coordinantes_crosshair(self, x: float, y: float) -> None:
|
232
|
+
"""
|
233
|
+
Add crosshair to the plot to highlight the current position.
|
234
|
+
Args:
|
235
|
+
x(float): X coordinate.
|
236
|
+
y(float): Y coordinate.
|
237
|
+
"""
|
238
|
+
|
239
|
+
# Crosshair to highlight the current position
|
240
|
+
highlight_H = pg.InfiniteLine(
|
241
|
+
angle=0, movable=False, pen=pg.mkPen(color="r", width=1, style=QtCore.Qt.DashLine)
|
242
|
+
)
|
243
|
+
highlight_V = pg.InfiniteLine(
|
244
|
+
angle=90, movable=False, pen=pg.mkPen(color="r", width=1, style=QtCore.Qt.DashLine)
|
245
|
+
)
|
246
|
+
|
247
|
+
# Add crosshair to the curve list for future referencing
|
248
|
+
self.plot_components["highlight_H"] = highlight_H
|
249
|
+
self.plot_components["highlight_V"] = highlight_V
|
250
|
+
|
251
|
+
# Add crosshair to the plot
|
252
|
+
self.plot_item.addItem(highlight_H)
|
253
|
+
self.plot_item.addItem(highlight_V)
|
254
|
+
|
255
|
+
highlight_V.setPos(x)
|
256
|
+
highlight_H.setPos(y)
|
257
|
+
|
258
|
+
def _make_limit_map(self, limits_x: list, limits_y: list) -> pg.ImageItem:
|
259
|
+
"""
|
260
|
+
Create a limit map for the motor map plot.
|
261
|
+
Args:
|
262
|
+
limits_x(list): Motor limits for the x axis.
|
263
|
+
limits_y(list): Motor limits for the y axis.
|
264
|
+
|
265
|
+
Returns:
|
266
|
+
pg.ImageItem: Limit map.
|
267
|
+
"""
|
268
|
+
limit_x_min, limit_x_max = limits_x
|
269
|
+
limit_y_min, limit_y_max = limits_y
|
270
|
+
|
271
|
+
map_width = int(limit_x_max - limit_x_min + 1)
|
272
|
+
map_height = int(limit_y_max - limit_y_min + 1)
|
273
|
+
|
274
|
+
# Create limits map
|
275
|
+
background_value = self.config.background_value
|
276
|
+
limit_map_data = np.full((map_width, map_height), background_value, dtype=np.float32)
|
277
|
+
limit_map = pg.ImageItem()
|
278
|
+
limit_map.setImage(limit_map_data)
|
279
|
+
|
280
|
+
# Translate and scale the image item to match the motor coordinates
|
281
|
+
tr = QtGui.QTransform()
|
282
|
+
tr.translate(limit_x_min, limit_y_min)
|
283
|
+
limit_map.setTransform(tr)
|
284
|
+
|
285
|
+
return limit_map
|
286
|
+
|
287
|
+
def _get_motor_init_position(self, name: str, entry: str, precision: int) -> float:
|
288
|
+
"""
|
289
|
+
Get the motor initial position from the config.
|
290
|
+
Args:
|
291
|
+
name(str): Motor name.
|
292
|
+
entry(str): Motor entry.
|
293
|
+
precision(int): Decimal precision of the motor position.
|
294
|
+
Returns:
|
295
|
+
float: Motor initial position.
|
296
|
+
"""
|
297
|
+
init_position = round(self.dev[name].read()[entry]["value"], precision)
|
298
|
+
return init_position
|
299
|
+
|
300
|
+
def _validate_signal_entries(
|
301
|
+
self,
|
302
|
+
x_name: str,
|
303
|
+
y_name: str,
|
304
|
+
x_entry: str | None,
|
305
|
+
y_entry: str | None,
|
306
|
+
validate_bec: bool = True,
|
307
|
+
) -> tuple[str, str]:
|
308
|
+
"""
|
309
|
+
Validate the signal name and entry.
|
310
|
+
Args:
|
311
|
+
x_name(str): Name of the x signal.
|
312
|
+
y_name(str): Name of the y signal.
|
313
|
+
x_entry(str|None): Entry of the x signal.
|
314
|
+
y_entry(str|None): Entry of the y signal.
|
315
|
+
validate_bec(bool, optional): If True, validate the signal with BEC. Defaults to True.
|
316
|
+
Returns:
|
317
|
+
tuple[str,str]: Validated x and y entries.
|
318
|
+
"""
|
319
|
+
if validate_bec:
|
320
|
+
x_entry = self.entry_validator.validate_signal(x_name, x_entry)
|
321
|
+
y_entry = self.entry_validator.validate_signal(y_name, y_entry)
|
322
|
+
else:
|
323
|
+
x_entry = x_name if x_entry is None else x_entry
|
324
|
+
y_entry = y_name if y_entry is None else y_entry
|
325
|
+
return x_entry, y_entry
|
326
|
+
|
327
|
+
def _get_motor_limit(self, motor: str) -> Union[list | None]: # TODO check if works correctly
|
328
|
+
"""
|
329
|
+
Get the motor limit from the config.
|
330
|
+
Args:
|
331
|
+
motor(str): Motor name.
|
332
|
+
|
333
|
+
Returns:
|
334
|
+
float: Motor limit.
|
335
|
+
"""
|
336
|
+
try:
|
337
|
+
limits = self.dev[motor].limits
|
338
|
+
if limits == [0, 0]:
|
339
|
+
return None
|
340
|
+
return limits
|
341
|
+
except AttributeError: # TODO maybe not needed, if no limits it returns [0,0]
|
342
|
+
# If the motor doesn't have a 'limits' attribute, return a default value or raise a custom exception
|
343
|
+
print(f"The device '{motor}' does not have defined limits.")
|
344
|
+
return None
|
345
|
+
|
346
|
+
def _update_plot(self):
|
347
|
+
"""Update the motor map plot."""
|
348
|
+
x = self.database_buffer["x"]
|
349
|
+
y = self.database_buffer["y"]
|
350
|
+
|
351
|
+
# Setup gradient brush for history
|
352
|
+
brushes = [pg.mkBrush(50, 50, 50, 255)] * len(x)
|
353
|
+
|
354
|
+
# Calculate the decrement step based on self.num_dim_points
|
355
|
+
num_dim_points = self.config.num_dim_points
|
356
|
+
decrement_step = (255 - 50) / num_dim_points
|
357
|
+
for i in range(1, min(num_dim_points + 1, len(x) + 1)):
|
358
|
+
brightness = max(60, 255 - decrement_step * (i - 1))
|
359
|
+
brushes[-i] = pg.mkBrush(brightness, brightness, brightness, 255)
|
360
|
+
brushes[-1] = pg.mkBrush(255, 255, 255, 255) # Newest point is always full brightness
|
361
|
+
scatter_size = self.config.scatter_size
|
362
|
+
|
363
|
+
# Update the scatter plot
|
364
|
+
self.plot_components["scatter"].setData(
|
365
|
+
x=x,
|
366
|
+
y=y,
|
367
|
+
brush=brushes,
|
368
|
+
pen=None,
|
369
|
+
size=scatter_size,
|
370
|
+
)
|
371
|
+
|
372
|
+
# Get last know position for crosshair
|
373
|
+
current_x = x[-1]
|
374
|
+
current_y = y[-1]
|
375
|
+
|
376
|
+
# Update the crosshair
|
377
|
+
self.plot_components["highlight_V"].setPos(current_x)
|
378
|
+
self.plot_components["highlight_H"].setPos(current_y)
|
379
|
+
|
380
|
+
# TODO not update title but some label
|
381
|
+
# Update plot title
|
382
|
+
precision = self.config.precision
|
383
|
+
self.set_title(
|
384
|
+
f"Motor position: ({round(current_x,precision)}, {round(current_y,precision)})"
|
385
|
+
)
|
386
|
+
|
387
|
+
@pyqtSlot(dict)
|
388
|
+
def on_device_readback(self, msg: dict) -> None:
|
389
|
+
"""
|
390
|
+
Update the motor map plot with the new motor position.
|
391
|
+
Args:
|
392
|
+
msg(dict): Message from the device readback.
|
393
|
+
"""
|
394
|
+
if self.motor_x is None or self.motor_y is None:
|
395
|
+
return
|
396
|
+
|
397
|
+
if self.motor_x in msg["signals"]:
|
398
|
+
x = msg["signals"][self.motor_x]["value"]
|
399
|
+
self.database_buffer["x"].append(x)
|
400
|
+
self.database_buffer["y"].append(self.database_buffer["y"][-1])
|
401
|
+
|
402
|
+
elif self.motor_y in msg["signals"]:
|
403
|
+
y = msg["signals"][self.motor_y]["value"]
|
404
|
+
self.database_buffer["y"].append(y)
|
405
|
+
self.database_buffer["x"].append(self.database_buffer["x"][-1])
|
406
|
+
|
407
|
+
self.update_signal.emit()
|
408
|
+
|
409
|
+
|
410
|
+
if __name__ == "__main__": # pragma: no cover
|
411
|
+
import sys
|
412
|
+
|
413
|
+
import pyqtgraph as pg
|
414
|
+
from qtpy.QtWidgets import QApplication
|
415
|
+
|
416
|
+
app = QApplication(sys.argv)
|
417
|
+
glw = pg.GraphicsLayoutWidget()
|
418
|
+
motor_map = BECMotorMap()
|
419
|
+
motor_map.change_motors("samx", "samy")
|
420
|
+
glw.addItem(motor_map)
|
421
|
+
widget = glw
|
422
|
+
widget.show()
|
423
|
+
sys.exit(app.exec_())
|