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 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
+ """
@@ -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)
@@ -10,5 +10,5 @@ from .motor_control import (
10
10
  MotorThread,
11
11
  )
12
12
  from .motor_map import MotorMap
13
- from .plots import BECCurve, BECPlotBase, BECWaveform1D
13
+ from .plots import BECCurve, BECMotorMap, BECWaveform1D
14
14
  from .scan_control import ScanControl
@@ -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",
@@ -1,3 +1,4 @@
1
1
  from .image import BECImageItem, BECImageShow, ImageItemConfig
2
+ from .motor_map import BECMotorMap, MotorMapConfig
2
3
  from .plot_base import AxisConfig, BECPlotBase, WidgetConfig
3
4
  from .waveform1d import BECCurve, BECWaveform1D, Waveform1DConfig
@@ -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_())
@@ -66,7 +66,7 @@ class BECPlotBase(BECConnector, pg.GraphicsLayout):
66
66
  pg.GraphicsLayout.__init__(self, parent)
67
67
 
68
68
  self.figure = parent_figure
69
- self.plot_item = self.addPlot()
69
+ self.plot_item = self.addPlot(row=0, col=0)
70
70
 
71
71
  self.add_legend()
72
72