bec-widgets 0.44.5__py3-none-any.whl → 0.46.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 +123 -1
- bec_widgets/cli/generate_cli.py +4 -3
- bec_widgets/cli/server.py +2 -2
- bec_widgets/widgets/__init__.py +1 -2
- bec_widgets/widgets/figure/figure.py +148 -22
- bec_widgets/widgets/plots/__init__.py +2 -1
- bec_widgets/widgets/plots/motor_map.py +423 -0
- bec_widgets/widgets/plots/plot_base.py +1 -1
- bec_widgets/widgets/plots/{waveform1d.py → waveform.py} +77 -11
- {bec_widgets-0.44.5.dist-info → bec_widgets-0.46.0.dist-info}/METADATA +1 -1
- {bec_widgets-0.44.5.dist-info → bec_widgets-0.46.0.dist-info}/RECORD +23 -24
- tests/client_mocks.py +76 -31
- tests/test_bec_dispatcher.py +2 -2
- tests/test_bec_figure.py +28 -4
- 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 +75 -10
- bec_widgets/widgets/monitor_scatter_2D/__init__.py +0 -1
- bec_widgets/widgets/monitor_scatter_2D/monitor_scatter_2D.py +0 -374
- tests/test_bec_monitor_scatter2D.py +0 -162
- {bec_widgets-0.44.5.dist-info → bec_widgets-0.46.0.dist-info}/LICENSE +0 -0
- {bec_widgets-0.44.5.dist-info → bec_widgets-0.46.0.dist-info}/WHEEL +0 -0
- {bec_widgets-0.44.5.dist-info → bec_widgets-0.46.0.dist-info}/top_level.txt +0 -0
@@ -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.waveform 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_())
|
@@ -15,7 +15,7 @@ from qtpy.QtCore import Slot as pyqtSlot
|
|
15
15
|
from qtpy.QtWidgets import QWidget
|
16
16
|
|
17
17
|
from bec_widgets.utils import BECConnector, Colors, ConnectionConfig, EntryValidator
|
18
|
-
from bec_widgets.widgets.plots import BECPlotBase, WidgetConfig
|
18
|
+
from bec_widgets.widgets.plots.plot_base import BECPlotBase, WidgetConfig
|
19
19
|
|
20
20
|
|
21
21
|
class SignalData(BaseModel):
|
@@ -25,14 +25,16 @@ class SignalData(BaseModel):
|
|
25
25
|
entry: str
|
26
26
|
unit: Optional[str] = None # todo implement later
|
27
27
|
modifier: Optional[str] = None # todo implement later
|
28
|
+
limits: Optional[list[float]] = None # todo implement later
|
28
29
|
|
29
30
|
|
30
31
|
class Signal(BaseModel):
|
31
32
|
"""The configuration of a signal in the 1D waveform widget."""
|
32
33
|
|
33
34
|
source: str
|
34
|
-
x: SignalData
|
35
|
+
x: SignalData # TODO maybe add metadata for config gui later
|
35
36
|
y: SignalData
|
37
|
+
z: Optional[SignalData] = None
|
36
38
|
|
37
39
|
|
38
40
|
class CurveConfig(ConnectionConfig):
|
@@ -48,12 +50,13 @@ class CurveConfig(ConnectionConfig):
|
|
48
50
|
)
|
49
51
|
source: Optional[str] = Field(None, description="The source of the curve.")
|
50
52
|
signals: Optional[Signal] = Field(None, description="The signal of the curve.")
|
53
|
+
colormap: Optional[str] = Field("plasma", description="The colormap of the curves z gradient.")
|
51
54
|
|
52
55
|
|
53
56
|
class Waveform1DConfig(WidgetConfig):
|
54
57
|
color_palette: Literal["plasma", "viridis", "inferno", "magma"] = Field(
|
55
58
|
"plasma", description="The color palette of the figure widget."
|
56
|
-
)
|
59
|
+
) # TODO can be extended to all colormaps from current pyqtgraph session
|
57
60
|
curves: dict[str, CurveConfig] = Field(
|
58
61
|
{}, description="The list of curves to be added to the 1D waveform widget."
|
59
62
|
)
|
@@ -64,6 +67,7 @@ class BECCurve(BECConnector, pg.PlotDataItem):
|
|
64
67
|
"set",
|
65
68
|
"set_data",
|
66
69
|
"set_color",
|
70
|
+
"set_colormap",
|
67
71
|
"set_symbol",
|
68
72
|
"set_symbol_color",
|
69
73
|
"set_symbol_size",
|
@@ -134,6 +138,7 @@ class BECCurve(BECConnector, pg.PlotDataItem):
|
|
134
138
|
# Mapping of keywords to setter methods
|
135
139
|
method_map = {
|
136
140
|
"color": self.set_color,
|
141
|
+
"colormap": self.set_colormap,
|
137
142
|
"symbol": self.set_symbol,
|
138
143
|
"symbol_color": self.set_symbol_color,
|
139
144
|
"symbol_size": self.set_symbol_size,
|
@@ -202,6 +207,14 @@ class BECCurve(BECConnector, pg.PlotDataItem):
|
|
202
207
|
self.config.pen_style = pen_style
|
203
208
|
self.apply_config()
|
204
209
|
|
210
|
+
def set_colormap(self, colormap: str):
|
211
|
+
"""
|
212
|
+
Set the colormap for the scatter plot z gradient.
|
213
|
+
Args:
|
214
|
+
colormap(str): Colormap for the scatter plot.
|
215
|
+
"""
|
216
|
+
self.config.colormap = colormap
|
217
|
+
|
205
218
|
def get_data(self) -> tuple[np.ndarray, np.ndarray]:
|
206
219
|
"""
|
207
220
|
Get the data of the curve.
|
@@ -212,7 +225,7 @@ class BECCurve(BECConnector, pg.PlotDataItem):
|
|
212
225
|
return x_data, y_data
|
213
226
|
|
214
227
|
|
215
|
-
class
|
228
|
+
class BECWaveform(BECPlotBase):
|
216
229
|
USER_ACCESS = [
|
217
230
|
"add_curve_scan",
|
218
231
|
"add_curve_custom",
|
@@ -466,9 +479,12 @@ class BECWaveform1D(BECPlotBase):
|
|
466
479
|
self,
|
467
480
|
x_name: str,
|
468
481
|
y_name: str,
|
482
|
+
z_name: Optional[str] = None,
|
469
483
|
x_entry: Optional[str] = None,
|
470
484
|
y_entry: Optional[str] = None,
|
485
|
+
z_entry: Optional[str] = None,
|
471
486
|
color: Optional[str] = None,
|
487
|
+
color_map_z: Optional[str] = "plasma",
|
472
488
|
label: Optional[str] = None,
|
473
489
|
validate_bec: bool = True,
|
474
490
|
**kwargs,
|
@@ -480,7 +496,10 @@ class BECWaveform1D(BECPlotBase):
|
|
480
496
|
x_entry(str): Entry of the x signal.
|
481
497
|
y_name(str): Name of the y signal.
|
482
498
|
y_entry(str): Entry of the y signal.
|
499
|
+
z_name(str): Name of the z signal.
|
500
|
+
z_entry(str): Entry of the z signal.
|
483
501
|
color(str, optional): Color of the curve. Defaults to None.
|
502
|
+
color_map_z(str): The color map to use for the z-axis.
|
484
503
|
label(str, optional): Label of the curve. Defaults to None.
|
485
504
|
**kwargs: Additional keyword arguments for the curve configuration.
|
486
505
|
|
@@ -491,11 +510,14 @@ class BECWaveform1D(BECPlotBase):
|
|
491
510
|
curve_source = "scan_segment"
|
492
511
|
|
493
512
|
# Get entry if not provided and validate
|
494
|
-
x_entry, y_entry = self._validate_signal_entries(
|
495
|
-
x_name, y_name, x_entry, y_entry, validate_bec
|
513
|
+
x_entry, y_entry, z_entry = self._validate_signal_entries(
|
514
|
+
x_name, y_name, z_name, x_entry, y_entry, z_entry, validate_bec
|
496
515
|
)
|
497
516
|
|
498
|
-
|
517
|
+
if z_name is not None and z_entry is not None:
|
518
|
+
label = label or f"{z_name}-{z_entry}"
|
519
|
+
else:
|
520
|
+
label = label or f"{y_name}-{y_entry}"
|
499
521
|
|
500
522
|
curve_exits = self._check_curve_id(label, self._curves_data)
|
501
523
|
if curve_exits:
|
@@ -514,11 +536,13 @@ class BECWaveform1D(BECPlotBase):
|
|
514
536
|
parent_id=self.gui_id,
|
515
537
|
label=label,
|
516
538
|
color=color,
|
539
|
+
color_map=color_map_z,
|
517
540
|
source=curve_source,
|
518
541
|
signals=Signal(
|
519
542
|
source=curve_source,
|
520
543
|
x=SignalData(name=x_name, entry=x_entry),
|
521
544
|
y=SignalData(name=y_name, entry=y_entry),
|
545
|
+
z=SignalData(name=z_name, entry=z_entry) if z_name else None,
|
522
546
|
),
|
523
547
|
**kwargs,
|
524
548
|
)
|
@@ -529,28 +553,35 @@ class BECWaveform1D(BECPlotBase):
|
|
529
553
|
self,
|
530
554
|
x_name: str,
|
531
555
|
y_name: str,
|
556
|
+
z_name: str | None,
|
532
557
|
x_entry: str | None,
|
533
558
|
y_entry: str | None,
|
559
|
+
z_entry: str | None,
|
534
560
|
validate_bec: bool = True,
|
535
|
-
) -> tuple[str, str]:
|
561
|
+
) -> tuple[str, str, str | None]:
|
536
562
|
"""
|
537
563
|
Validate the signal name and entry.
|
538
564
|
Args:
|
539
565
|
x_name(str): Name of the x signal.
|
540
566
|
y_name(str): Name of the y signal.
|
567
|
+
z_name(str): Name of the z signal.
|
541
568
|
x_entry(str|None): Entry of the x signal.
|
542
569
|
y_entry(str|None): Entry of the y signal.
|
570
|
+
z_entry(str|None): Entry of the z signal.
|
543
571
|
validate_bec(bool, optional): If True, validate the signal with BEC. Defaults to True.
|
544
572
|
Returns:
|
545
|
-
tuple[str,str]: Validated x
|
573
|
+
tuple[str,str,str|None]: Validated x, y, z entries.
|
546
574
|
"""
|
547
575
|
if validate_bec:
|
548
576
|
x_entry = self.entry_validator.validate_signal(x_name, x_entry)
|
549
577
|
y_entry = self.entry_validator.validate_signal(y_name, y_entry)
|
578
|
+
if z_name:
|
579
|
+
z_entry = self.entry_validator.validate_signal(z_name, z_entry)
|
550
580
|
else:
|
551
581
|
x_entry = x_name if x_entry is None else x_entry
|
552
582
|
y_entry = y_name if y_entry is None else y_entry
|
553
|
-
|
583
|
+
z_entry = z_name if z_entry is None else z_entry
|
584
|
+
return x_entry, y_entry, z_entry
|
554
585
|
|
555
586
|
def _check_curve_id(self, val: Any, dict_to_check: dict) -> bool:
|
556
587
|
"""
|
@@ -653,19 +684,54 @@ class BECWaveform1D(BECPlotBase):
|
|
653
684
|
Args:
|
654
685
|
data(ScanData): Data from the scan segment.
|
655
686
|
"""
|
687
|
+
data_x = None
|
688
|
+
data_y = None
|
689
|
+
data_z = None
|
656
690
|
for curve_id, curve in self._curves_data["scan_segment"].items():
|
657
691
|
x_name = curve.config.signals.x.name
|
658
692
|
x_entry = curve.config.signals.x.entry
|
659
693
|
y_name = curve.config.signals.y.name
|
660
694
|
y_entry = curve.config.signals.y.entry
|
695
|
+
if curve.config.signals.z:
|
696
|
+
z_name = curve.config.signals.z.name
|
697
|
+
z_entry = curve.config.signals.z.entry
|
661
698
|
|
662
699
|
try:
|
663
700
|
data_x = data[x_name][x_entry].val
|
664
701
|
data_y = data[y_name][y_entry].val
|
702
|
+
if curve.config.signals.z:
|
703
|
+
data_z = data[z_name][z_entry].val
|
704
|
+
color_z = self._make_z_gradient(
|
705
|
+
data_z, curve.config.colormap
|
706
|
+
) # TODO decide how to implement custom gradient
|
665
707
|
except TypeError:
|
666
708
|
continue
|
667
709
|
|
668
|
-
|
710
|
+
if data_z is not None and color_z is not None:
|
711
|
+
curve.setData(x=data_x, y=data_y, symbolBrush=color_z)
|
712
|
+
else:
|
713
|
+
curve.setData(data_x, data_y)
|
714
|
+
|
715
|
+
def _make_z_gradient(self, data_z: list | np.ndarray, colormap: str) -> list | None:
|
716
|
+
"""
|
717
|
+
Make a gradient color for the z values.
|
718
|
+
Args:
|
719
|
+
data_z(list|np.ndarray): Z values.
|
720
|
+
colormap(str): Colormap for the gradient color.
|
721
|
+
|
722
|
+
Returns:
|
723
|
+
list: List of colors for the z values.
|
724
|
+
"""
|
725
|
+
# Normalize z_values for color mapping
|
726
|
+
z_min, z_max = np.min(data_z), np.max(data_z)
|
727
|
+
|
728
|
+
if z_max != z_min: # Ensure that there is a range in the z values
|
729
|
+
z_values_norm = (data_z - z_min) / (z_max - z_min)
|
730
|
+
colormap = pg.colormap.get(colormap) # using colormap from global settings
|
731
|
+
colors = [colormap.map(z, mode="qcolor") for z in z_values_norm]
|
732
|
+
return colors
|
733
|
+
else:
|
734
|
+
return None
|
669
735
|
|
670
736
|
def scan_history(self, scan_index: int = None, scan_id: str = None):
|
671
737
|
"""
|