bec-widgets 0.53.3__py3-none-any.whl → 0.55.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.
- CHANGELOG.md +24 -26
- PKG-INFO +1 -1
- bec_widgets/cli/client.py +265 -13
- bec_widgets/cli/client_utils.py +0 -3
- bec_widgets/cli/generate_cli.py +10 -5
- bec_widgets/cli/rpc_wigdet_handler.py +2 -1
- bec_widgets/cli/server.py +5 -7
- bec_widgets/examples/jupyter_console/jupyter_console_window.py +11 -5
- bec_widgets/examples/motor_movement/motor_control_compilations.py +17 -16
- bec_widgets/widgets/__init__.py +1 -10
- bec_widgets/widgets/figure/figure.py +40 -23
- bec_widgets/widgets/figure/plots/__init__.py +0 -0
- bec_widgets/widgets/figure/plots/image/__init__.py +0 -0
- bec_widgets/widgets/{plots → figure/plots/image}/image.py +6 -416
- bec_widgets/widgets/figure/plots/image/image_item.py +277 -0
- bec_widgets/widgets/figure/plots/image/image_processor.py +152 -0
- bec_widgets/widgets/figure/plots/motor_map/__init__.py +0 -0
- bec_widgets/widgets/{plots → figure/plots/motor_map}/motor_map.py +2 -2
- bec_widgets/widgets/figure/plots/waveform/__init__.py +0 -0
- bec_widgets/widgets/{plots → figure/plots/waveform}/waveform.py +9 -222
- bec_widgets/widgets/figure/plots/waveform/waveform_curve.py +227 -0
- bec_widgets/widgets/motor_control/__init__.py +0 -7
- bec_widgets/widgets/motor_control/motor_control.py +2 -948
- bec_widgets/widgets/motor_control/motor_table/__init__.py +0 -0
- bec_widgets/widgets/motor_control/motor_table/motor_table.py +483 -0
- bec_widgets/widgets/motor_control/movement_absolute/__init__.py +0 -0
- bec_widgets/widgets/motor_control/movement_absolute/movement_absolute.py +157 -0
- bec_widgets/widgets/motor_control/movement_relative/__init__.py +0 -0
- bec_widgets/widgets/motor_control/movement_relative/movement_relative.py +227 -0
- bec_widgets/widgets/motor_control/selection/__init__.py +0 -0
- bec_widgets/widgets/motor_control/selection/selection.py +110 -0
- bec_widgets/widgets/spiral_progress_bar/__init__.py +1 -0
- bec_widgets/widgets/spiral_progress_bar/ring.py +184 -0
- bec_widgets/widgets/spiral_progress_bar/spiral_progress_bar.py +594 -0
- {bec_widgets-0.53.3.dist-info → bec_widgets-0.55.0.dist-info}/METADATA +1 -1
- {bec_widgets-0.53.3.dist-info → bec_widgets-0.55.0.dist-info}/RECORD +56 -53
- docs/requirements.txt +1 -0
- pyproject.toml +1 -1
- tests/end-2-end/test_bec_dock_rpc_e2e.py +82 -1
- tests/end-2-end/test_bec_figure_rpc_e2e.py +4 -4
- tests/end-2-end/test_rpc_register_e2e.py +1 -1
- tests/unit_tests/test_bec_dock.py +1 -1
- tests/unit_tests/test_bec_figure.py +6 -4
- tests/unit_tests/test_bec_motor_map.py +2 -3
- tests/unit_tests/test_motor_control.py +6 -5
- tests/unit_tests/test_spiral_progress_bar.py +338 -0
- tests/unit_tests/test_waveform1d.py +13 -1
- bec_widgets/validation/__init__.py +0 -2
- bec_widgets/validation/monitor_config_validator.py +0 -258
- bec_widgets/widgets/monitor/__init__.py +0 -1
- bec_widgets/widgets/monitor/config_dialog.py +0 -574
- bec_widgets/widgets/monitor/config_dialog.ui +0 -210
- bec_widgets/widgets/monitor/example_configs/config_device.yaml +0 -60
- bec_widgets/widgets/monitor/example_configs/config_scans.yaml +0 -92
- bec_widgets/widgets/monitor/monitor.py +0 -845
- bec_widgets/widgets/monitor/tab_template.ui +0 -180
- bec_widgets/widgets/motor_map/__init__.py +0 -1
- bec_widgets/widgets/motor_map/motor_map.py +0 -594
- bec_widgets/widgets/plots/__init__.py +0 -4
- tests/unit_tests/test_bec_monitor.py +0 -220
- tests/unit_tests/test_config_dialog.py +0 -178
- tests/unit_tests/test_motor_map.py +0 -171
- tests/unit_tests/test_validator_errors.py +0 -110
- /bec_widgets/{cli → assets}/bec_widgets_icon.png +0 -0
- /bec_widgets/{examples/jupyter_console → assets}/terminal_icon.png +0 -0
- /bec_widgets/widgets/{plots → figure/plots}/plot_base.py +0 -0
- /bec_widgets/widgets/motor_control/{motor_control_table.ui → motor_table/motor_table.ui} +0 -0
- /bec_widgets/widgets/motor_control/{motor_control_absolute.ui → movement_absolute/movement_absolute.ui} +0 -0
- /bec_widgets/widgets/motor_control/{motor_control_relative.ui → movement_relative/movement_relative.ui} +0 -0
- /bec_widgets/widgets/motor_control/{motor_control_selection.ui → selection/selection.ui} +0 -0
- {bec_widgets-0.53.3.dist-info → bec_widgets-0.55.0.dist-info}/WHEEL +0 -0
- {bec_widgets-0.53.3.dist-info → bec_widgets-0.55.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,845 +0,0 @@
|
|
1
|
-
# pylint: disable = no-name-in-module,missing-module-docstring
|
2
|
-
import time
|
3
|
-
|
4
|
-
import pyqtgraph as pg
|
5
|
-
from bec_lib.endpoints import MessageEndpoints
|
6
|
-
from pydantic import ValidationError
|
7
|
-
from pyqtgraph import mkBrush, mkPen
|
8
|
-
from qtpy import QtCore
|
9
|
-
from qtpy.QtCore import Signal as pyqtSignal
|
10
|
-
from qtpy.QtCore import Slot as pyqtSlot
|
11
|
-
from qtpy.QtWidgets import QApplication, QMessageBox
|
12
|
-
|
13
|
-
from bec_widgets.utils import Colors, Crosshair, yaml_dialog
|
14
|
-
from bec_widgets.utils.bec_dispatcher import BECDispatcher
|
15
|
-
from bec_widgets.validation import MonitorConfigValidator
|
16
|
-
from bec_widgets.widgets.monitor.config_dialog import ConfigDialog
|
17
|
-
|
18
|
-
# just for demonstration purposes if script run directly
|
19
|
-
CONFIG_SCAN_MODE = {
|
20
|
-
"plot_settings": {
|
21
|
-
"background_color": "white",
|
22
|
-
"num_columns": 3,
|
23
|
-
"colormap": "plasma",
|
24
|
-
"scan_types": True,
|
25
|
-
},
|
26
|
-
"plot_data": {
|
27
|
-
"grid_scan": [
|
28
|
-
{
|
29
|
-
"plot_name": "Grid plot 1",
|
30
|
-
"x_label": "Motor X",
|
31
|
-
"y_label": "BPM",
|
32
|
-
"sources": [
|
33
|
-
{
|
34
|
-
"type": "scan_segment",
|
35
|
-
"signals": {
|
36
|
-
"x": [{"name": "samx", "entry": "samx"}],
|
37
|
-
"y": [{"name": "bpm4i"}],
|
38
|
-
},
|
39
|
-
}
|
40
|
-
],
|
41
|
-
},
|
42
|
-
{
|
43
|
-
"plot_name": "Grid plot 2",
|
44
|
-
"x_label": "Motor X",
|
45
|
-
"y_label": "BPM",
|
46
|
-
"sources": [
|
47
|
-
{
|
48
|
-
"type": "scan_segment",
|
49
|
-
"signals": {
|
50
|
-
"x": [{"name": "samx", "entry": "samx"}],
|
51
|
-
"y": [{"name": "bpm4i"}],
|
52
|
-
},
|
53
|
-
}
|
54
|
-
],
|
55
|
-
},
|
56
|
-
{
|
57
|
-
"plot_name": "Grid plot 3",
|
58
|
-
"x_label": "Motor X",
|
59
|
-
"y_label": "BPM",
|
60
|
-
"sources": [
|
61
|
-
{
|
62
|
-
"type": "scan_segment",
|
63
|
-
"signals": {"x": [{"name": "samy"}], "y": [{"name": "bpm4i"}]},
|
64
|
-
}
|
65
|
-
],
|
66
|
-
},
|
67
|
-
{
|
68
|
-
"plot_name": "Grid plot 4",
|
69
|
-
"x_label": "Motor X",
|
70
|
-
"y_label": "BPM",
|
71
|
-
"sources": [
|
72
|
-
{
|
73
|
-
"type": "scan_segment",
|
74
|
-
"signals": {
|
75
|
-
"x": [{"name": "samy", "entry": "samy"}],
|
76
|
-
"y": [{"name": "bpm4i"}],
|
77
|
-
},
|
78
|
-
}
|
79
|
-
],
|
80
|
-
},
|
81
|
-
],
|
82
|
-
"line_scan": [
|
83
|
-
{
|
84
|
-
"plot_name": "BPM plots vs samx",
|
85
|
-
"x_label": "Motor X",
|
86
|
-
"y_label": "Gauss",
|
87
|
-
"sources": [
|
88
|
-
{
|
89
|
-
"type": "scan_segment",
|
90
|
-
"signals": {
|
91
|
-
"x": [{"name": "samx", "entry": "samx"}],
|
92
|
-
"y": [{"name": "bpm4i"}],
|
93
|
-
},
|
94
|
-
}
|
95
|
-
],
|
96
|
-
},
|
97
|
-
{
|
98
|
-
"plot_name": "Gauss plots vs samx",
|
99
|
-
"x_label": "Motor X",
|
100
|
-
"y_label": "Gauss",
|
101
|
-
"sources": [
|
102
|
-
{
|
103
|
-
"type": "scan_segment",
|
104
|
-
"signals": {
|
105
|
-
"x": [{"name": "samx", "entry": "samx"}],
|
106
|
-
"y": [{"name": "bpm4i"}, {"name": "bpm4i"}],
|
107
|
-
},
|
108
|
-
}
|
109
|
-
],
|
110
|
-
},
|
111
|
-
],
|
112
|
-
},
|
113
|
-
}
|
114
|
-
|
115
|
-
|
116
|
-
CONFIG_WRONG = {
|
117
|
-
"plot_settings": {
|
118
|
-
"background_color": "black",
|
119
|
-
"num_columns": 2,
|
120
|
-
"colormap": "plasma",
|
121
|
-
"scan_types": False,
|
122
|
-
},
|
123
|
-
"plot_data": [
|
124
|
-
{
|
125
|
-
"plot_name": "BPM4i plots vs samx",
|
126
|
-
"x_label": "Motor Y",
|
127
|
-
"y_label": "bpm4i",
|
128
|
-
"sources": [
|
129
|
-
{
|
130
|
-
"type": "non_existing_source",
|
131
|
-
"signals": {
|
132
|
-
"x": [{"name": "samy"}],
|
133
|
-
"y": [{"name": "bpm4i", "entry": "bpm4i"}],
|
134
|
-
},
|
135
|
-
},
|
136
|
-
{
|
137
|
-
"type": "history",
|
138
|
-
"scan_id": "<scan_id>",
|
139
|
-
"signals": {
|
140
|
-
"x": [{"name": "samy"}],
|
141
|
-
"y": [{"name": "bpm4i", "entry": "bpm4i"}],
|
142
|
-
},
|
143
|
-
},
|
144
|
-
],
|
145
|
-
},
|
146
|
-
{
|
147
|
-
"plot_name": "Gauss plots vs samx",
|
148
|
-
"x_label": "Motor X",
|
149
|
-
"y_label": "Gauss",
|
150
|
-
"sources": [
|
151
|
-
{
|
152
|
-
"type": "scan_segment",
|
153
|
-
"signals": {
|
154
|
-
"x": [{"name": "samx", "entry": "non_sense_entry"}],
|
155
|
-
"y": [
|
156
|
-
{"name": "non_existing_name"},
|
157
|
-
{"name": "samy", "entry": "non_existing_entry"},
|
158
|
-
],
|
159
|
-
},
|
160
|
-
}
|
161
|
-
],
|
162
|
-
},
|
163
|
-
{
|
164
|
-
"plot_name": "Gauss plots vs samx",
|
165
|
-
"x_label": "Motor X",
|
166
|
-
"y_label": "Gauss",
|
167
|
-
"sources": [
|
168
|
-
{
|
169
|
-
"signals": {
|
170
|
-
"x": [{"name": "samx", "entry": "samx"}],
|
171
|
-
"y": [{"name": "samx"}, {"name": "samy", "entry": "samx"}],
|
172
|
-
}
|
173
|
-
}
|
174
|
-
],
|
175
|
-
},
|
176
|
-
],
|
177
|
-
}
|
178
|
-
|
179
|
-
|
180
|
-
CONFIG_SIMPLE = {
|
181
|
-
"plot_settings": {
|
182
|
-
"background_color": "black",
|
183
|
-
"num_columns": 2,
|
184
|
-
"colormap": "plasma",
|
185
|
-
"scan_types": False,
|
186
|
-
},
|
187
|
-
"plot_data": [
|
188
|
-
{
|
189
|
-
"plot_name": "BPM4i plots vs samx",
|
190
|
-
"x_label": "Motor X",
|
191
|
-
"y_label": "bpm4i",
|
192
|
-
"sources": [
|
193
|
-
{
|
194
|
-
"type": "scan_segment",
|
195
|
-
"signals": {
|
196
|
-
"x": [{"name": "samx"}],
|
197
|
-
"y": [{"name": "bpm4i", "entry": "bpm4i"}],
|
198
|
-
},
|
199
|
-
},
|
200
|
-
# {
|
201
|
-
# "type": "history",
|
202
|
-
# "signals": {
|
203
|
-
# "x": [{"name": "samx"}],
|
204
|
-
# "y": [{"name": "bpm4i", "entry": "bpm4i"}],
|
205
|
-
# },
|
206
|
-
# },
|
207
|
-
# {
|
208
|
-
# "type": "dap",
|
209
|
-
# 'worker':'some_worker',
|
210
|
-
# "signals": {
|
211
|
-
# "x": [{"name": "samx"}],
|
212
|
-
# "y": [{"name": "bpm4i", "entry": "bpm4i"}],
|
213
|
-
# },
|
214
|
-
# },
|
215
|
-
],
|
216
|
-
},
|
217
|
-
{
|
218
|
-
"plot_name": "Gauss plots vs samx",
|
219
|
-
"x_label": "Motor X",
|
220
|
-
"y_label": "Gauss",
|
221
|
-
"sources": [
|
222
|
-
{
|
223
|
-
"type": "scan_segment",
|
224
|
-
"signals": {
|
225
|
-
"x": [{"name": "samx", "entry": "samx"}],
|
226
|
-
"y": [{"name": "bpm4i"}, {"name": "bpm4i"}],
|
227
|
-
},
|
228
|
-
}
|
229
|
-
],
|
230
|
-
},
|
231
|
-
],
|
232
|
-
}
|
233
|
-
|
234
|
-
CONFIG_REDIS = {
|
235
|
-
"plot_settings": {
|
236
|
-
"background_color": "white",
|
237
|
-
"axis_width": 2,
|
238
|
-
"num_columns": 5,
|
239
|
-
"colormap": "plasma",
|
240
|
-
"scan_types": False,
|
241
|
-
},
|
242
|
-
"plot_data": [
|
243
|
-
{
|
244
|
-
"plot_name": "BPM4i plots vs samx",
|
245
|
-
"x_label": "Motor Y",
|
246
|
-
"y_label": "bpm4i",
|
247
|
-
"sources": [
|
248
|
-
{
|
249
|
-
"type": "scan_segment",
|
250
|
-
"signals": {"x": [{"name": "samx"}], "y": [{"name": "gauss_bpm"}]},
|
251
|
-
},
|
252
|
-
{
|
253
|
-
"type": "redis",
|
254
|
-
"endpoint": "public/gui/data/6cd5ea3f-a9a9-4736-b4ed-74ab9edfb996",
|
255
|
-
"update": "append",
|
256
|
-
"signals": {"x": [{"name": "x_default_tag"}], "y": [{"name": "y_default_tag"}]},
|
257
|
-
},
|
258
|
-
],
|
259
|
-
}
|
260
|
-
],
|
261
|
-
}
|
262
|
-
|
263
|
-
|
264
|
-
class BECMonitor(pg.GraphicsLayoutWidget):
|
265
|
-
update_signal = pyqtSignal()
|
266
|
-
|
267
|
-
def __init__(
|
268
|
-
self,
|
269
|
-
parent=None,
|
270
|
-
client=None,
|
271
|
-
config: dict = None,
|
272
|
-
enable_crosshair: bool = True,
|
273
|
-
gui_id=None,
|
274
|
-
skip_validation: bool = False,
|
275
|
-
):
|
276
|
-
super().__init__(parent=parent)
|
277
|
-
|
278
|
-
# Client and device manager from BEC
|
279
|
-
self.plot_data = None
|
280
|
-
bec_dispatcher = BECDispatcher()
|
281
|
-
self.client = bec_dispatcher.client if client is None else client
|
282
|
-
self.dev = self.client.device_manager.devices
|
283
|
-
self.queue = self.client.queue
|
284
|
-
|
285
|
-
self.validator = MonitorConfigValidator(self.dev)
|
286
|
-
self.gui_id = gui_id
|
287
|
-
|
288
|
-
if self.gui_id is None:
|
289
|
-
self.gui_id = self.__class__.__name__ + str(time.time())
|
290
|
-
|
291
|
-
# Connect slots dispatcher
|
292
|
-
bec_dispatcher.connect_slot(self.on_scan_segment, MessageEndpoints.scan_segment())
|
293
|
-
bec_dispatcher.connect_slot(self.on_config_update, MessageEndpoints.gui_config(self.gui_id))
|
294
|
-
bec_dispatcher.connect_slot(
|
295
|
-
self.on_instruction, MessageEndpoints.gui_instructions(self.gui_id)
|
296
|
-
)
|
297
|
-
bec_dispatcher.connect_slot(self.on_data_from_redis, MessageEndpoints.gui_data(self.gui_id))
|
298
|
-
|
299
|
-
# Current configuration
|
300
|
-
self.config = config
|
301
|
-
self.skip_validation = skip_validation
|
302
|
-
|
303
|
-
# Enable crosshair
|
304
|
-
self.enable_crosshair = enable_crosshair
|
305
|
-
|
306
|
-
# Displayed Data
|
307
|
-
self.database = None
|
308
|
-
|
309
|
-
self.crosshairs = None
|
310
|
-
self.plots = None
|
311
|
-
self.curves_data = None
|
312
|
-
self.grid_coordinates = None
|
313
|
-
self.scan_id = None
|
314
|
-
|
315
|
-
# TODO make colors accessible to users
|
316
|
-
self.user_colors = {} # key: (plot_name, y_name, y_entry), value: color
|
317
|
-
|
318
|
-
# Connect the update signal to the update plot method
|
319
|
-
self.proxy_update_plot = pg.SignalProxy(
|
320
|
-
self.update_signal, rateLimit=25, slot=self.update_scan_segment_plot
|
321
|
-
)
|
322
|
-
|
323
|
-
# Init UI
|
324
|
-
if self.config is None:
|
325
|
-
print("No initial config found for BECDeviceMonitor")
|
326
|
-
else:
|
327
|
-
self.on_config_update(self.config)
|
328
|
-
|
329
|
-
def _init_config(self):
|
330
|
-
"""
|
331
|
-
Initializes or update the configuration settings for the PlotApp.
|
332
|
-
"""
|
333
|
-
|
334
|
-
# Separate configs
|
335
|
-
self.plot_settings = self.config.get("plot_settings", {})
|
336
|
-
self.plot_data_config = self.config.get("plot_data", {})
|
337
|
-
self.scan_types = self.plot_settings.get("scan_types", False)
|
338
|
-
|
339
|
-
if self.scan_types is False: # Device tracking mode
|
340
|
-
self.plot_data = self.plot_data_config # TODO logic has to be improved
|
341
|
-
else: # without incoming data setup the first configuration to the first scan type sorted alphabetically by name
|
342
|
-
self.plot_data = self.plot_data_config[min(list(self.plot_data_config.keys()))]
|
343
|
-
|
344
|
-
# Initialize the database
|
345
|
-
self.database = self._init_database(self.plot_data)
|
346
|
-
|
347
|
-
# Initialize the UI
|
348
|
-
self._init_ui(self.plot_settings["num_columns"])
|
349
|
-
|
350
|
-
if self.scan_id is not None:
|
351
|
-
self.replot_last_scan()
|
352
|
-
|
353
|
-
def _init_database(self, plot_data_config: dict, source_type_to_init=None) -> dict:
|
354
|
-
"""
|
355
|
-
Initializes or updates the database for the PlotApp.
|
356
|
-
Args:
|
357
|
-
plot_data_config(dict): Configuration settings for plots.
|
358
|
-
source_type_to_init(str, optional): Specific source type to initialize. If None, initialize all.
|
359
|
-
Returns:
|
360
|
-
dict: Updated or new database dictionary.
|
361
|
-
"""
|
362
|
-
database = {} if source_type_to_init is None else self.database.copy()
|
363
|
-
|
364
|
-
for plot in plot_data_config:
|
365
|
-
for source in plot["sources"]:
|
366
|
-
source_type = source["type"]
|
367
|
-
if source_type_to_init and source_type != source_type_to_init:
|
368
|
-
continue # Skip if not the specified source type
|
369
|
-
|
370
|
-
if source_type not in database:
|
371
|
-
database[source_type] = {}
|
372
|
-
|
373
|
-
for axis, signals in source["signals"].items():
|
374
|
-
for signal in signals:
|
375
|
-
name = signal["name"]
|
376
|
-
entry = signal.get("entry", name)
|
377
|
-
if name not in database[source_type]:
|
378
|
-
database[source_type][name] = {}
|
379
|
-
if entry not in database[source_type][name]:
|
380
|
-
database[source_type][name][entry] = []
|
381
|
-
|
382
|
-
return database
|
383
|
-
|
384
|
-
def _init_ui(self, num_columns: int = 3) -> None:
|
385
|
-
"""
|
386
|
-
Initialize the UI components, create plots and store their grid positions.
|
387
|
-
|
388
|
-
Args:
|
389
|
-
num_columns (int): Number of columns to wrap the layout.
|
390
|
-
|
391
|
-
This method initializes a dictionary `self.plots` to store the plot objects
|
392
|
-
along with their corresponding x and y signal names. It dynamically arranges
|
393
|
-
the plots in a grid layout based on the given number of columns and dynamically
|
394
|
-
stretches the last plots to fit the remaining space.
|
395
|
-
"""
|
396
|
-
self.clear()
|
397
|
-
self.plots = {}
|
398
|
-
self.grid_coordinates = []
|
399
|
-
|
400
|
-
num_plots = len(self.plot_data)
|
401
|
-
|
402
|
-
# Check if num_columns exceeds the number of plots
|
403
|
-
if num_columns >= num_plots:
|
404
|
-
num_columns = num_plots
|
405
|
-
self.plot_settings["num_columns"] = num_columns # Update the settings
|
406
|
-
print(
|
407
|
-
"Warning: num_columns in the YAML file was greater than the number of plots."
|
408
|
-
f" Resetting num_columns to number of plots:{num_columns}."
|
409
|
-
)
|
410
|
-
else:
|
411
|
-
self.plot_settings["num_columns"] = num_columns # Update the settings
|
412
|
-
|
413
|
-
num_rows = num_plots // num_columns
|
414
|
-
last_row_cols = num_plots % num_columns
|
415
|
-
remaining_space = num_columns - last_row_cols
|
416
|
-
|
417
|
-
for i, plot_config in enumerate(self.plot_data):
|
418
|
-
row, col = i // num_columns, i % num_columns
|
419
|
-
colspan = 1
|
420
|
-
|
421
|
-
if row == num_rows and remaining_space > 0:
|
422
|
-
if last_row_cols == 1:
|
423
|
-
colspan = num_columns
|
424
|
-
else:
|
425
|
-
colspan = remaining_space // last_row_cols + 1
|
426
|
-
remaining_space -= colspan - 1
|
427
|
-
last_row_cols -= 1
|
428
|
-
|
429
|
-
plot_name = plot_config.get("plot_name", "")
|
430
|
-
|
431
|
-
x_label = plot_config.get("x_label", "")
|
432
|
-
y_label = plot_config.get("y_label", "")
|
433
|
-
|
434
|
-
plot = self.addPlot(row=row, col=col, colspan=colspan, title=plot_name)
|
435
|
-
plot.setLabel("bottom", x_label)
|
436
|
-
plot.setLabel("left", y_label)
|
437
|
-
plot.addLegend()
|
438
|
-
self._set_plot_colors(plot, self.plot_settings)
|
439
|
-
|
440
|
-
self.plots[plot_name] = plot
|
441
|
-
self.grid_coordinates.append((row, col))
|
442
|
-
|
443
|
-
# Initialize curves
|
444
|
-
self.init_curves()
|
445
|
-
|
446
|
-
def _set_plot_colors(self, plot: pg.PlotItem, plot_settings: dict) -> None:
|
447
|
-
"""
|
448
|
-
Set the plot colors based on the plot config.
|
449
|
-
|
450
|
-
Args:
|
451
|
-
plot (pg.PlotItem): Plot object to set the colors.
|
452
|
-
plot_settings (dict): Plot settings dictionary.
|
453
|
-
"""
|
454
|
-
if plot_settings.get("show_grid", False):
|
455
|
-
plot.showGrid(x=True, y=True, alpha=0.5)
|
456
|
-
pen_width = plot_settings.get("axis_width")
|
457
|
-
color = plot_settings.get("axis_color")
|
458
|
-
if color is None:
|
459
|
-
if plot_settings["background_color"].lower() == "black":
|
460
|
-
color = "w"
|
461
|
-
self.setBackground("k")
|
462
|
-
elif plot_settings["background_color"].lower() == "white":
|
463
|
-
color = "k"
|
464
|
-
self.setBackground("w")
|
465
|
-
else:
|
466
|
-
raise ValueError(
|
467
|
-
f"Invalid background color {plot_settings['background_color']}. Allowed values"
|
468
|
-
" are 'white' or 'black'."
|
469
|
-
)
|
470
|
-
pen = pg.mkPen(color=color, width=pen_width)
|
471
|
-
x_axis = plot.getAxis("bottom") # 'bottom' corresponds to the x-axis
|
472
|
-
x_axis.setPen(pen)
|
473
|
-
x_axis.setTextPen(pen)
|
474
|
-
x_axis.setTickPen(pen)
|
475
|
-
|
476
|
-
y_axis = plot.getAxis("left") # 'left' corresponds to the y-axis
|
477
|
-
y_axis.setPen(pen)
|
478
|
-
y_axis.setTextPen(pen)
|
479
|
-
y_axis.setTickPen(pen)
|
480
|
-
|
481
|
-
def init_curves(self) -> None:
|
482
|
-
"""
|
483
|
-
Initialize curve data and properties for each plot and data source.
|
484
|
-
"""
|
485
|
-
self.curves_data = {}
|
486
|
-
|
487
|
-
for idx, plot_config in enumerate(self.plot_data):
|
488
|
-
plot_name = plot_config.get("plot_name", "")
|
489
|
-
plot = self.plots[plot_name]
|
490
|
-
plot.clear()
|
491
|
-
|
492
|
-
for source in plot_config["sources"]:
|
493
|
-
source_type = source["type"]
|
494
|
-
y_signals = source["signals"].get("y", [])
|
495
|
-
colors_ys = Colors.golden_angle_color(
|
496
|
-
colormap=self.plot_settings["colormap"], num=len(y_signals)
|
497
|
-
)
|
498
|
-
|
499
|
-
if source_type not in self.curves_data:
|
500
|
-
self.curves_data[source_type] = {}
|
501
|
-
if plot_name not in self.curves_data[source_type]:
|
502
|
-
self.curves_data[source_type][plot_name] = []
|
503
|
-
|
504
|
-
for i, (y_signal, color) in enumerate(zip(y_signals, colors_ys)):
|
505
|
-
y_name = y_signal["name"]
|
506
|
-
y_entry = y_signal.get("entry", y_name)
|
507
|
-
curve_name = f"{y_name} ({y_entry})-{source_type[0].upper()}"
|
508
|
-
curve_data = self.create_curve(curve_name, color)
|
509
|
-
plot.addItem(curve_data)
|
510
|
-
self.curves_data[source_type][plot_name].append((y_name, y_entry, curve_data))
|
511
|
-
|
512
|
-
# Render static plot elements
|
513
|
-
self.update_plot()
|
514
|
-
# # Hook Crosshair #TODO enable later, currently not working
|
515
|
-
if self.enable_crosshair is True:
|
516
|
-
self.hook_crosshair()
|
517
|
-
|
518
|
-
def create_curve(self, curve_name: str, color: str) -> pg.PlotDataItem:
|
519
|
-
"""
|
520
|
-
Create
|
521
|
-
Args:
|
522
|
-
curve_name: Name of the curve
|
523
|
-
color(str): Color of the curve
|
524
|
-
|
525
|
-
Returns:
|
526
|
-
pg.PlotDataItem: Assigned curve object
|
527
|
-
"""
|
528
|
-
user_color = self.user_colors.get(curve_name, None)
|
529
|
-
color_to_use = user_color if user_color else color
|
530
|
-
pen_curve = mkPen(color=color_to_use, width=2, style=QtCore.Qt.DashLine)
|
531
|
-
brush_curve = mkBrush(color=color_to_use)
|
532
|
-
|
533
|
-
return pg.PlotDataItem(
|
534
|
-
symbolSize=5,
|
535
|
-
symbolBrush=brush_curve,
|
536
|
-
pen=pen_curve,
|
537
|
-
skipFiniteCheck=True,
|
538
|
-
name=curve_name,
|
539
|
-
)
|
540
|
-
|
541
|
-
def hook_crosshair(self) -> None:
|
542
|
-
"""Hook the crosshair to all plots."""
|
543
|
-
# TODO can be extended to hook crosshair signal for mouse move/clicked
|
544
|
-
self.crosshairs = {}
|
545
|
-
for plot_name, plot in self.plots.items():
|
546
|
-
crosshair = Crosshair(plot, precision=3)
|
547
|
-
self.crosshairs[plot_name] = crosshair
|
548
|
-
|
549
|
-
def update_scan_segment_plot(self):
|
550
|
-
"""
|
551
|
-
Update the plot with the latest scan segment data.
|
552
|
-
"""
|
553
|
-
self.update_plot(source_type="scan_segment")
|
554
|
-
|
555
|
-
def update_plot(self, source_type=None) -> None:
|
556
|
-
"""
|
557
|
-
Update the plot data based on the stored data dictionary.
|
558
|
-
Only updates data for the specified source_type if provided.
|
559
|
-
"""
|
560
|
-
for src_type, plots in self.curves_data.items():
|
561
|
-
if source_type and src_type != source_type:
|
562
|
-
continue
|
563
|
-
|
564
|
-
for plot_name, curve_list in plots.items():
|
565
|
-
plot_config = next(
|
566
|
-
(pc for pc in self.plot_data if pc.get("plot_name") == plot_name), None
|
567
|
-
)
|
568
|
-
if not plot_config:
|
569
|
-
continue
|
570
|
-
|
571
|
-
x_name, x_entry = self.extract_x_config(plot_config, src_type)
|
572
|
-
|
573
|
-
for y_name, y_entry, curve in curve_list:
|
574
|
-
data_x = self.database.get(src_type, {}).get(x_name, {}).get(x_entry, [])
|
575
|
-
data_y = self.database.get(src_type, {}).get(y_name, {}).get(y_entry, [])
|
576
|
-
curve.setData(data_x, data_y)
|
577
|
-
|
578
|
-
def extract_x_config(self, plot_config: dict, source_type: str) -> tuple:
|
579
|
-
"""Extract the signal configurations for x and y axes from plot_config.
|
580
|
-
Args:
|
581
|
-
plot_config (dict): Plot configuration.
|
582
|
-
Returns:
|
583
|
-
tuple: Tuple containing the x name and x entry.
|
584
|
-
"""
|
585
|
-
x_name, x_entry = None, None
|
586
|
-
|
587
|
-
for source in plot_config["sources"]:
|
588
|
-
if source["type"] == source_type and "x" in source["signals"]:
|
589
|
-
x_signal = source["signals"]["x"][0]
|
590
|
-
x_name = x_signal.get("name")
|
591
|
-
x_entry = x_signal.get("entry", x_name)
|
592
|
-
return x_name, x_entry
|
593
|
-
|
594
|
-
def get_config(self):
|
595
|
-
"""Return the current configuration settings."""
|
596
|
-
return self.config
|
597
|
-
|
598
|
-
def show_config_dialog(self):
|
599
|
-
"""Show the configuration dialog."""
|
600
|
-
|
601
|
-
dialog = ConfigDialog(
|
602
|
-
client=self.client, default_config=self.config, skip_validation=self.skip_validation
|
603
|
-
)
|
604
|
-
dialog.config_updated.connect(self.on_config_update)
|
605
|
-
dialog.show()
|
606
|
-
|
607
|
-
def update_client(self, client) -> None:
|
608
|
-
"""Update the client and device manager from BEC.
|
609
|
-
Args:
|
610
|
-
client: BEC client
|
611
|
-
"""
|
612
|
-
self.client = client
|
613
|
-
self.dev = self.client.device_manager.devices
|
614
|
-
|
615
|
-
def _close_all_plots(self):
|
616
|
-
"""Close all plots."""
|
617
|
-
for plot in self.plots.values():
|
618
|
-
plot.clear()
|
619
|
-
|
620
|
-
@pyqtSlot(dict)
|
621
|
-
def on_instruction(self, msg_content: dict) -> None:
|
622
|
-
"""
|
623
|
-
Handle instructions sent to the GUI.
|
624
|
-
Possible actions are:
|
625
|
-
- clear: Clear the plots
|
626
|
-
- close: Close the GUI
|
627
|
-
- config_dialog: Open the configuration dialog
|
628
|
-
|
629
|
-
Args:
|
630
|
-
msg_content (dict): Message content with the instruction and parameters.
|
631
|
-
"""
|
632
|
-
action = msg_content.get("action", None)
|
633
|
-
parameters = msg_content.get("parameters", None)
|
634
|
-
|
635
|
-
if action == "clear":
|
636
|
-
self.flush()
|
637
|
-
self._close_all_plots()
|
638
|
-
elif action == "close":
|
639
|
-
self.close()
|
640
|
-
elif action == "config_dialog":
|
641
|
-
self.show_config_dialog()
|
642
|
-
else:
|
643
|
-
print(f"Unknown instruction received: {msg_content}")
|
644
|
-
|
645
|
-
@pyqtSlot(dict)
|
646
|
-
def on_config_update(self, config: dict) -> None:
|
647
|
-
"""
|
648
|
-
Validate and update the configuration settings for the PlotApp.
|
649
|
-
Args:
|
650
|
-
config(dict): Configuration settings
|
651
|
-
"""
|
652
|
-
# convert config from BEC CLI to correct formatting
|
653
|
-
config_tag = config.get("config", None)
|
654
|
-
if config_tag is not None:
|
655
|
-
config = config["config"]
|
656
|
-
|
657
|
-
if self.skip_validation is True:
|
658
|
-
self.config = config
|
659
|
-
self._init_config()
|
660
|
-
else:
|
661
|
-
try:
|
662
|
-
validated_config = self.validator.validate_monitor_config(config)
|
663
|
-
self.config = validated_config.model_dump()
|
664
|
-
self._init_config()
|
665
|
-
except ValidationError as e:
|
666
|
-
error_str = str(e)
|
667
|
-
formatted_error_message = BECMonitor.format_validation_error(error_str)
|
668
|
-
|
669
|
-
# Display the formatted error message in a popup
|
670
|
-
QMessageBox.critical(self, "Configuration Error", formatted_error_message)
|
671
|
-
|
672
|
-
@staticmethod
|
673
|
-
def format_validation_error(error_str: str) -> str:
|
674
|
-
"""
|
675
|
-
Format the validation error string to be displayed in a popup.
|
676
|
-
Args:
|
677
|
-
error_str(str): Error string from the validation error.
|
678
|
-
"""
|
679
|
-
error_lines = error_str.split("\n")
|
680
|
-
# The first line contains the number of errors.
|
681
|
-
error_header = f"<p><b>{error_lines[0]}</b></p><hr>"
|
682
|
-
|
683
|
-
formatted_error_message = error_header
|
684
|
-
# Skip the first line as it's the header.
|
685
|
-
error_details = error_lines[1:]
|
686
|
-
|
687
|
-
# Iterate through pairs of lines (each error's two lines).
|
688
|
-
for i in range(0, len(error_details), 2):
|
689
|
-
location = error_details[i]
|
690
|
-
message = error_details[i + 1] if i + 1 < len(error_details) else ""
|
691
|
-
|
692
|
-
formatted_error_message += f"<p><b>{location}</b><br>{message}</p><hr>"
|
693
|
-
|
694
|
-
return formatted_error_message
|
695
|
-
|
696
|
-
def flush(self, flush_all=False, source_type_to_flush=None) -> None:
|
697
|
-
"""Update or reset the database to match the current configuration.
|
698
|
-
|
699
|
-
Args:
|
700
|
-
flush_all (bool): If True, reset the entire database.
|
701
|
-
source_type_to_flush (str): Specific source type to reset. Ignored if flush_all is True.
|
702
|
-
"""
|
703
|
-
if flush_all:
|
704
|
-
self.database = self._init_database(self.plot_data)
|
705
|
-
self.init_curves()
|
706
|
-
else:
|
707
|
-
if source_type_to_flush in self.database:
|
708
|
-
# TODO maybe reinit the database from config again instead of cycle through names/entries
|
709
|
-
# Reset only the specified source type
|
710
|
-
for name in self.database[source_type_to_flush]:
|
711
|
-
for entry in self.database[source_type_to_flush][name]:
|
712
|
-
self.database[source_type_to_flush][name][entry] = []
|
713
|
-
# Reset curves for the specified source type
|
714
|
-
if source_type_to_flush in self.curves_data:
|
715
|
-
self.init_curves()
|
716
|
-
|
717
|
-
@pyqtSlot(dict, dict)
|
718
|
-
def on_scan_segment(self, msg: dict, metadata: dict):
|
719
|
-
"""
|
720
|
-
Handle new scan segments and saves data to a dictionary. Linked through bec_dispatcher.
|
721
|
-
|
722
|
-
Args:
|
723
|
-
msg (dict): Message received with scan data.
|
724
|
-
metadata (dict): Metadata of the scan.
|
725
|
-
"""
|
726
|
-
current_scan_id = msg.get("scan_id", None)
|
727
|
-
if current_scan_id is None:
|
728
|
-
return
|
729
|
-
|
730
|
-
if current_scan_id != self.scan_id:
|
731
|
-
if self.scan_types is False:
|
732
|
-
self.plot_data = self.plot_data_config
|
733
|
-
elif self.scan_types is True:
|
734
|
-
current_name = metadata.get("scan_name")
|
735
|
-
if current_name is None:
|
736
|
-
raise ValueError(
|
737
|
-
"Scan name not found in metadata. Please check the scan_name in the YAML"
|
738
|
-
" config or in bec configuration."
|
739
|
-
)
|
740
|
-
self.plot_data = self.plot_data_config.get(current_name, None)
|
741
|
-
if not self.plot_data:
|
742
|
-
raise ValueError(
|
743
|
-
f"Scan name {current_name} not found in the YAML config. Please check the scan_name in the "
|
744
|
-
"YAML config or in bec configuration."
|
745
|
-
)
|
746
|
-
|
747
|
-
# Init UI
|
748
|
-
self._init_ui(self.plot_settings["num_columns"])
|
749
|
-
|
750
|
-
self.scan_id = current_scan_id
|
751
|
-
self.scan_data = self.queue.scan_storage.find_scan_by_ID(self.scan_id)
|
752
|
-
if not self.scan_data:
|
753
|
-
print(f"No data found for scan_id: {self.scan_id}") # TODO better error
|
754
|
-
return
|
755
|
-
self.flush(source_type_to_flush="scan_segment")
|
756
|
-
|
757
|
-
self.scan_segment_update()
|
758
|
-
|
759
|
-
self.update_signal.emit()
|
760
|
-
|
761
|
-
def scan_segment_update(self):
|
762
|
-
"""
|
763
|
-
Update the database with data from scan storage based on the provided scan_id.
|
764
|
-
"""
|
765
|
-
scan_data = self.scan_data.data
|
766
|
-
for device_name, device_entries in self.database.get("scan_segment", {}).items():
|
767
|
-
for entry in device_entries.keys():
|
768
|
-
dataset = scan_data[device_name][entry].val
|
769
|
-
if dataset:
|
770
|
-
self.database["scan_segment"][device_name][entry] = dataset
|
771
|
-
else:
|
772
|
-
print(f"No data found for {device_name} {entry}")
|
773
|
-
|
774
|
-
def replot_last_scan(self):
|
775
|
-
"""
|
776
|
-
Replot the last scan.
|
777
|
-
"""
|
778
|
-
self.scan_segment_update()
|
779
|
-
self.update_plot(source_type="scan_segment")
|
780
|
-
|
781
|
-
@pyqtSlot(dict)
|
782
|
-
def on_data_from_redis(self, msg) -> None:
|
783
|
-
"""
|
784
|
-
Handle new data sent from redis.
|
785
|
-
Args:
|
786
|
-
msg (dict): Message received with data.
|
787
|
-
"""
|
788
|
-
|
789
|
-
# wait until new config is loaded
|
790
|
-
while "redis" not in self.database:
|
791
|
-
time.sleep(0.1)
|
792
|
-
self._init_database(
|
793
|
-
self.plot_data, source_type_to_init="redis"
|
794
|
-
) # add database entry for redis dataset
|
795
|
-
|
796
|
-
data = msg.get("data", {})
|
797
|
-
x_data = data.get("x", {})
|
798
|
-
y_data = data.get("y", {})
|
799
|
-
|
800
|
-
# Update x data
|
801
|
-
if x_data:
|
802
|
-
x_tag = x_data.get("tag")
|
803
|
-
self.database["redis"][x_tag][x_tag] = x_data["data"]
|
804
|
-
|
805
|
-
# Update y data
|
806
|
-
for y_tag, y_info in y_data.items():
|
807
|
-
self.database["redis"][y_tag][y_tag] = y_info["data"]
|
808
|
-
|
809
|
-
# Trigger plot update
|
810
|
-
self.update_plot(source_type="redis")
|
811
|
-
print(f"database after: {self.database}")
|
812
|
-
|
813
|
-
|
814
|
-
if __name__ == "__main__": # pragma: no cover
|
815
|
-
import argparse
|
816
|
-
import json
|
817
|
-
import sys
|
818
|
-
|
819
|
-
parser = argparse.ArgumentParser()
|
820
|
-
parser.add_argument("--config_file", help="Path to the config file.")
|
821
|
-
parser.add_argument("--config", help="Path to the config file.")
|
822
|
-
parser.add_argument("--id", help="GUI ID.")
|
823
|
-
args = parser.parse_args()
|
824
|
-
|
825
|
-
if args.config is not None:
|
826
|
-
# Load config from file
|
827
|
-
config = json.loads(args.config)
|
828
|
-
elif args.config_file is not None:
|
829
|
-
# Load config from file
|
830
|
-
config = yaml_dialog.load_yaml(args.config_file)
|
831
|
-
else:
|
832
|
-
config = CONFIG_SIMPLE
|
833
|
-
|
834
|
-
client = BECDispatcher().client
|
835
|
-
client.start()
|
836
|
-
app = QApplication(sys.argv)
|
837
|
-
monitor = BECMonitor(config=config, gui_id=args.id, skip_validation=False)
|
838
|
-
monitor.show()
|
839
|
-
# just to test redis data
|
840
|
-
# redis_data = {
|
841
|
-
# "x": {"data": [1, 2, 3], "tag": "x_default_tag"},
|
842
|
-
# "y": {"y_default_tag": {"data": [1, 2, 3]}},
|
843
|
-
# }
|
844
|
-
# monitor.on_data_from_redis({"data": redis_data})
|
845
|
-
sys.exit(app.exec())
|