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,594 +0,0 @@
|
|
1
|
-
# pylint: disable = no-name-in-module,missing-module-docstring
|
2
|
-
from __future__ import annotations
|
3
|
-
|
4
|
-
import time
|
5
|
-
from typing import Any, Union
|
6
|
-
|
7
|
-
import numpy as np
|
8
|
-
import pyqtgraph as pg
|
9
|
-
from bec_lib.endpoints import MessageEndpoints
|
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 QApplication
|
14
|
-
|
15
|
-
from bec_widgets.utils.bec_dispatcher import BECDispatcher
|
16
|
-
from bec_widgets.utils.yaml_dialog import load_yaml
|
17
|
-
|
18
|
-
CONFIG_DEFAULT = {
|
19
|
-
"plot_settings": {
|
20
|
-
"colormap": "Greys",
|
21
|
-
"scatter_size": 5,
|
22
|
-
"max_points": 1000,
|
23
|
-
"num_dim_points": 100,
|
24
|
-
"precision": 2,
|
25
|
-
"num_columns": 1,
|
26
|
-
"background_value": 25,
|
27
|
-
},
|
28
|
-
"motors": [
|
29
|
-
{
|
30
|
-
"plot_name": "Motor Map",
|
31
|
-
"x_label": "Motor X",
|
32
|
-
"y_label": "Motor Y",
|
33
|
-
"signals": {
|
34
|
-
"x": [{"name": "samx", "entry": "samx"}],
|
35
|
-
"y": [{"name": "samy", "entry": "samy"}],
|
36
|
-
},
|
37
|
-
},
|
38
|
-
{
|
39
|
-
"plot_name": "Motor Map 2 ",
|
40
|
-
"x_label": "Motor X",
|
41
|
-
"y_label": "Motor Y",
|
42
|
-
"signals": {
|
43
|
-
"x": [{"name": "aptrx", "entry": "aptrx"}],
|
44
|
-
"y": [{"name": "aptry", "entry": "aptry"}],
|
45
|
-
},
|
46
|
-
},
|
47
|
-
],
|
48
|
-
}
|
49
|
-
|
50
|
-
|
51
|
-
class MotorMap(pg.GraphicsLayoutWidget):
|
52
|
-
update_signal = pyqtSignal()
|
53
|
-
|
54
|
-
def __init__(
|
55
|
-
self,
|
56
|
-
parent=None,
|
57
|
-
client=None,
|
58
|
-
config: dict = None,
|
59
|
-
gui_id=None,
|
60
|
-
skip_validation: bool = True,
|
61
|
-
):
|
62
|
-
super().__init__(parent=parent)
|
63
|
-
|
64
|
-
# Import BEC related stuff
|
65
|
-
bec_dispatcher = BECDispatcher()
|
66
|
-
self.client = bec_dispatcher.client if client is None else client
|
67
|
-
self.dev = self.client.device_manager.devices
|
68
|
-
|
69
|
-
# TODO import validator when prepared
|
70
|
-
self.gui_id = gui_id
|
71
|
-
|
72
|
-
if self.gui_id is None:
|
73
|
-
self.gui_id = self.__class__.__name__ + str(time.time())
|
74
|
-
|
75
|
-
# Current configuration
|
76
|
-
self.config = config
|
77
|
-
self.skip_validation = skip_validation # TODO implement validation when validator is ready
|
78
|
-
|
79
|
-
# Connect the update signal to the update plot method
|
80
|
-
self.proxy_update_plot = pg.SignalProxy(
|
81
|
-
self.update_signal, rateLimit=25, slot=self._update_plots
|
82
|
-
)
|
83
|
-
|
84
|
-
# Config related variables
|
85
|
-
self.plot_data = None
|
86
|
-
self.plot_settings = None
|
87
|
-
self.max_points = None
|
88
|
-
self.num_dim_points = None
|
89
|
-
self.scatter_size = None
|
90
|
-
self.precision = None
|
91
|
-
self.background_value = None
|
92
|
-
self.database = {}
|
93
|
-
self.device_mapping = {}
|
94
|
-
self.plots = {}
|
95
|
-
self.grid_coordinates = []
|
96
|
-
self.curves_data = {}
|
97
|
-
|
98
|
-
# Init UI with config
|
99
|
-
if self.config is None:
|
100
|
-
print("No initial config found for MotorMap. Using default config.")
|
101
|
-
else:
|
102
|
-
self.on_config_update(self.config)
|
103
|
-
|
104
|
-
@pyqtSlot(dict)
|
105
|
-
def on_config_update(self, config: dict) -> None:
|
106
|
-
"""
|
107
|
-
Validate and update the configuration settings for the PlotApp.
|
108
|
-
Args:
|
109
|
-
config(dict): Configuration settings
|
110
|
-
"""
|
111
|
-
# TODO implement BEC CLI commands similar to BECPlotter
|
112
|
-
# convert config from BEC CLI to correct formatting
|
113
|
-
config_tag = config.get("config", None)
|
114
|
-
if config_tag is not None:
|
115
|
-
config = config["config"]
|
116
|
-
|
117
|
-
if self.skip_validation is True:
|
118
|
-
self.config = config
|
119
|
-
self._init_config()
|
120
|
-
|
121
|
-
else: # TODO implement validator
|
122
|
-
print("Do validation")
|
123
|
-
|
124
|
-
@pyqtSlot(str, str, int)
|
125
|
-
def change_motors(self, motor_x: str, motor_y: str, subplot: int = 0) -> None:
|
126
|
-
"""
|
127
|
-
Change the active motors for the plot.
|
128
|
-
Args:
|
129
|
-
motor_x(str): Motor name for the X axis.
|
130
|
-
motor_y(str): Motor name for the Y axis.
|
131
|
-
subplot(int): Subplot number.
|
132
|
-
"""
|
133
|
-
if subplot >= len(self.plot_data):
|
134
|
-
print(f"Invalid subplot index: {subplot}. Available subplots: {len(self.plot_data)}")
|
135
|
-
return
|
136
|
-
|
137
|
-
# Update the motor names in the plot configuration
|
138
|
-
self.config["motors"][subplot]["signals"]["x"][0]["name"] = motor_x
|
139
|
-
self.config["motors"][subplot]["signals"]["x"][0]["entry"] = motor_x
|
140
|
-
self.config["motors"][subplot]["signals"]["y"][0]["name"] = motor_y
|
141
|
-
self.config["motors"][subplot]["signals"]["y"][0]["entry"] = motor_y
|
142
|
-
|
143
|
-
# reinitialise the config and UI
|
144
|
-
self._init_config()
|
145
|
-
|
146
|
-
def _init_config(self):
|
147
|
-
"""Initiate the configuration."""
|
148
|
-
|
149
|
-
# Global widget settings
|
150
|
-
self._get_global_settings()
|
151
|
-
|
152
|
-
# Motor settings
|
153
|
-
self.plot_data = self.config.get("motors", {})
|
154
|
-
|
155
|
-
# Include motor limits into the config
|
156
|
-
self._add_limits_to_plot_data()
|
157
|
-
|
158
|
-
# Initialize the database
|
159
|
-
self.database = self._init_database()
|
160
|
-
|
161
|
-
# Create device mapping for x/y motor pairs
|
162
|
-
self.device_mapping = self._create_device_mapping()
|
163
|
-
|
164
|
-
# Initialize the plot UI
|
165
|
-
self._init_ui()
|
166
|
-
|
167
|
-
# Connect motors to slots
|
168
|
-
self._connect_motors_to_slots()
|
169
|
-
|
170
|
-
# Render init position of selected motors
|
171
|
-
self._update_plots()
|
172
|
-
|
173
|
-
def _get_global_settings(self):
|
174
|
-
"""Get global settings from the config."""
|
175
|
-
self.plot_settings = self.config.get("plot_settings", {})
|
176
|
-
|
177
|
-
self.max_points = self.plot_settings.get("max_points", 5000)
|
178
|
-
self.num_dim_points = self.plot_settings.get("num_dim_points", 100)
|
179
|
-
self.scatter_size = self.plot_settings.get("scatter_size", 5)
|
180
|
-
self.precision = self.plot_settings.get("precision", 2)
|
181
|
-
self.background_value = self.plot_settings.get("background_value", 25)
|
182
|
-
|
183
|
-
def _create_device_mapping(self):
|
184
|
-
"""
|
185
|
-
Create a mapping of device names to their corresponding x/y devices.
|
186
|
-
"""
|
187
|
-
mapping = {}
|
188
|
-
for motor in self.config.get("motors", []):
|
189
|
-
for axis in ["x", "y"]:
|
190
|
-
for signal in motor["signals"][axis]:
|
191
|
-
other_axis = "y" if axis == "x" else "x"
|
192
|
-
corresponding_device = motor["signals"][other_axis][0]["name"]
|
193
|
-
mapping[signal["name"]] = corresponding_device
|
194
|
-
return mapping
|
195
|
-
|
196
|
-
def _connect_motors_to_slots(self):
|
197
|
-
"""Connect motors to slots."""
|
198
|
-
|
199
|
-
# Disconnect all slots before connecting a new ones
|
200
|
-
bec_dispatcher = BECDispatcher()
|
201
|
-
bec_dispatcher.disconnect_all()
|
202
|
-
|
203
|
-
# Get list of all unique motors
|
204
|
-
unique_motors = []
|
205
|
-
for motor_config in self.plot_data:
|
206
|
-
for axis in ["x", "y"]:
|
207
|
-
for signal in motor_config["signals"][axis]:
|
208
|
-
unique_motors.append(signal["name"])
|
209
|
-
unique_motors = list(set(unique_motors))
|
210
|
-
|
211
|
-
# Create list of endpoint
|
212
|
-
endpoints = []
|
213
|
-
for motor in unique_motors:
|
214
|
-
endpoints.append(MessageEndpoints.device_readback(motor))
|
215
|
-
|
216
|
-
# Connect all topics to a single slot
|
217
|
-
bec_dispatcher.connect_slot(self.on_device_readback, endpoints)
|
218
|
-
|
219
|
-
def _add_limits_to_plot_data(self):
|
220
|
-
"""
|
221
|
-
Add limits to each motor signal in the plot_data.
|
222
|
-
"""
|
223
|
-
for motor_config in self.plot_data:
|
224
|
-
for axis in ["x", "y"]:
|
225
|
-
for signal in motor_config["signals"][axis]:
|
226
|
-
motor_name = signal["name"]
|
227
|
-
motor_limits = self._get_motor_limit(motor_name)
|
228
|
-
signal["limits"] = motor_limits
|
229
|
-
|
230
|
-
def _get_motor_limit(self, motor: str) -> Union[list | None]:
|
231
|
-
"""
|
232
|
-
Get the motor limit from the config.
|
233
|
-
Args:
|
234
|
-
motor(str): Motor name.
|
235
|
-
|
236
|
-
Returns:
|
237
|
-
float: Motor limit.
|
238
|
-
"""
|
239
|
-
try:
|
240
|
-
limits = self.dev[motor].limits
|
241
|
-
if limits == [0, 0]:
|
242
|
-
return None
|
243
|
-
return limits
|
244
|
-
except AttributeError: # TODO maybe not needed, if no limits it returns [0,0]
|
245
|
-
# If the motor doesn't have a 'limits' attribute, return a default value or raise a custom exception
|
246
|
-
print(f"The device '{motor}' does not have defined limits.")
|
247
|
-
return None
|
248
|
-
|
249
|
-
def _init_database(self):
|
250
|
-
"""Initiate the database according the config."""
|
251
|
-
database = {}
|
252
|
-
|
253
|
-
for plot in self.plot_data:
|
254
|
-
for axis, signals in plot["signals"].items():
|
255
|
-
for signal in signals:
|
256
|
-
name = signal["name"]
|
257
|
-
entry = signal.get("entry", name)
|
258
|
-
if name not in database:
|
259
|
-
database[name] = {}
|
260
|
-
if entry not in database[name]:
|
261
|
-
database[name][entry] = [self.get_coordinate(name, entry)]
|
262
|
-
return database
|
263
|
-
|
264
|
-
def get_coordinate(self, name, entry):
|
265
|
-
"""Get the initial coordinate value for a motor."""
|
266
|
-
try:
|
267
|
-
return self.dev[name].read()[entry]["value"]
|
268
|
-
except Exception as e:
|
269
|
-
print(f"Error getting initial value for {name}: {e}")
|
270
|
-
return None
|
271
|
-
|
272
|
-
def _init_ui(self, num_columns: int = 3) -> None:
|
273
|
-
"""
|
274
|
-
Initialize the UI components, create plots and store their grid positions.
|
275
|
-
|
276
|
-
Args:
|
277
|
-
num_columns (int): Number of columns to wrap the layout.
|
278
|
-
|
279
|
-
This method initializes a dictionary `self.plots` to store the plot objects
|
280
|
-
along with their corresponding x and y signal names. It dynamically arranges
|
281
|
-
the plots in a grid layout based on the given number of columns and dynamically
|
282
|
-
stretches the last plots to fit the remaining space.
|
283
|
-
"""
|
284
|
-
self.clear()
|
285
|
-
self.plots = {}
|
286
|
-
self.grid_coordinates = []
|
287
|
-
self.curves_data = {} # TODO moved from init_curves
|
288
|
-
|
289
|
-
num_plots = len(self.plot_data)
|
290
|
-
|
291
|
-
# Check if num_columns exceeds the number of plots
|
292
|
-
if num_columns >= num_plots:
|
293
|
-
num_columns = num_plots
|
294
|
-
self.plot_settings["num_columns"] = num_columns # Update the settings
|
295
|
-
print(
|
296
|
-
"Warning: num_columns in the YAML file was greater than the number of plots."
|
297
|
-
f" Resetting num_columns to number of plots:{num_columns}."
|
298
|
-
)
|
299
|
-
else:
|
300
|
-
self.plot_settings["num_columns"] = num_columns # Update the settings
|
301
|
-
|
302
|
-
num_rows = num_plots // num_columns
|
303
|
-
last_row_cols = num_plots % num_columns
|
304
|
-
remaining_space = num_columns - last_row_cols
|
305
|
-
|
306
|
-
for i, plot_config in enumerate(self.plot_data):
|
307
|
-
row, col = i // num_columns, i % num_columns
|
308
|
-
colspan = 1
|
309
|
-
|
310
|
-
if row == num_rows and remaining_space > 0:
|
311
|
-
if last_row_cols == 1:
|
312
|
-
colspan = num_columns
|
313
|
-
else:
|
314
|
-
colspan = remaining_space // last_row_cols + 1
|
315
|
-
remaining_space -= colspan - 1
|
316
|
-
last_row_cols -= 1
|
317
|
-
|
318
|
-
if "plot_name" not in plot_config:
|
319
|
-
plot_name = f"Plot ({row}, {col})"
|
320
|
-
plot_config["plot_name"] = plot_name
|
321
|
-
else:
|
322
|
-
plot_name = plot_config["plot_name"]
|
323
|
-
|
324
|
-
x_label = plot_config.get("x_label", "")
|
325
|
-
y_label = plot_config.get("y_label", "")
|
326
|
-
|
327
|
-
plot = self.addPlot(row=row, col=col, colspan=colspan, title="Motor position: (X, Y)")
|
328
|
-
plot.setLabel("bottom", f"{x_label} ({plot_config['signals']['x'][0]['name']})")
|
329
|
-
plot.setLabel("left", f"{y_label} ({plot_config['signals']['y'][0]['name']})")
|
330
|
-
plot.addLegend()
|
331
|
-
# self._set_plot_colors(plot, self.plot_settings) #TODO implement colors
|
332
|
-
|
333
|
-
self.plots[plot_name] = plot
|
334
|
-
self.grid_coordinates.append((row, col))
|
335
|
-
|
336
|
-
self._init_motor_map(plot_config)
|
337
|
-
|
338
|
-
def _init_motor_map(self, plot_config: dict) -> None:
|
339
|
-
"""
|
340
|
-
Initialize the motor map.
|
341
|
-
Args:
|
342
|
-
plot_config(dict): Plot configuration.
|
343
|
-
"""
|
344
|
-
|
345
|
-
# Get plot name to find appropriate plot
|
346
|
-
plot_name = plot_config.get("plot_name", "")
|
347
|
-
|
348
|
-
# Reset the curves data
|
349
|
-
plot = self.plots[plot_name]
|
350
|
-
plot.clear()
|
351
|
-
|
352
|
-
limits_x, limits_y = plot_config["signals"]["x"][0].get("limits", None), plot_config[
|
353
|
-
"signals"
|
354
|
-
]["y"][0].get("limits", None)
|
355
|
-
if limits_x is not None and limits_y is not None:
|
356
|
-
self._make_limit_map(plot, [limits_x, limits_y])
|
357
|
-
|
358
|
-
# Initiate ScatterPlotItem for motor coordinates
|
359
|
-
self.curves_data[plot_name] = {
|
360
|
-
"pos": pg.ScatterPlotItem(
|
361
|
-
size=self.scatter_size, pen=pg.mkPen(None), brush=pg.mkBrush(255, 255, 255, 255)
|
362
|
-
)
|
363
|
-
}
|
364
|
-
|
365
|
-
# Add the scatter plot to the plot
|
366
|
-
plot.addItem(self.curves_data[plot_name]["pos"])
|
367
|
-
# Set the point map to be always on the top
|
368
|
-
self.curves_data[plot_name]["pos"].setZValue(0)
|
369
|
-
|
370
|
-
# Add all layers to the plot
|
371
|
-
plot.showGrid(x=True, y=True)
|
372
|
-
|
373
|
-
# Add the crosshair for motor coordinates
|
374
|
-
init_position_x = self._get_motor_init_position(
|
375
|
-
plot_config["signals"]["x"][0]["name"], plot_config["signals"]["x"][0]["entry"]
|
376
|
-
)
|
377
|
-
init_position_y = self._get_motor_init_position(
|
378
|
-
plot_config["signals"]["y"][0]["name"], plot_config["signals"]["y"][0]["entry"]
|
379
|
-
)
|
380
|
-
self._add_coordinantes_crosshair(plot_name, init_position_x, init_position_y)
|
381
|
-
|
382
|
-
def _add_coordinantes_crosshair(self, plot_name: str, x: float, y: float) -> None:
|
383
|
-
"""
|
384
|
-
Add crosshair to the plot to highlight the current position.
|
385
|
-
Args:
|
386
|
-
plot_name(str): Name of the plot.
|
387
|
-
x(float): X coordinate.
|
388
|
-
y(float): Y coordinate.
|
389
|
-
"""
|
390
|
-
# find the current plot
|
391
|
-
plot = self.plots[plot_name]
|
392
|
-
|
393
|
-
# Crosshair to highlight the current position
|
394
|
-
highlight_H = pg.InfiniteLine(
|
395
|
-
angle=0, movable=False, pen=pg.mkPen(color="r", width=1, style=QtCore.Qt.DashLine)
|
396
|
-
)
|
397
|
-
highlight_V = pg.InfiniteLine(
|
398
|
-
angle=90, movable=False, pen=pg.mkPen(color="r", width=1, style=QtCore.Qt.DashLine)
|
399
|
-
)
|
400
|
-
|
401
|
-
# Add crosshair to the curve list for future referencing
|
402
|
-
self.curves_data[plot_name]["highlight_H"] = highlight_H
|
403
|
-
self.curves_data[plot_name]["highlight_V"] = highlight_V
|
404
|
-
|
405
|
-
# Add crosshair to the plot
|
406
|
-
plot.addItem(highlight_H)
|
407
|
-
plot.addItem(highlight_V)
|
408
|
-
|
409
|
-
highlight_H.setPos(x)
|
410
|
-
highlight_V.setPos(y)
|
411
|
-
|
412
|
-
def _make_limit_map(self, plot: pg.PlotItem, limits: list):
|
413
|
-
"""
|
414
|
-
Make a limit map from the limits list.
|
415
|
-
|
416
|
-
Args:
|
417
|
-
plot(pg.PlotItem): Plot to add the limit map to.
|
418
|
-
limits(list): List of limits.
|
419
|
-
"""
|
420
|
-
# Define the size of the image map based on the motor's limits
|
421
|
-
limit_x_min, limit_x_max = limits[0]
|
422
|
-
limit_y_min, limit_y_max = limits[1]
|
423
|
-
|
424
|
-
map_width = int(limit_x_max - limit_x_min + 1)
|
425
|
-
map_height = int(limit_y_max - limit_y_min + 1)
|
426
|
-
|
427
|
-
limit_map_data = np.full((map_width, map_height), self.background_value, dtype=np.float32)
|
428
|
-
|
429
|
-
# Create the image map
|
430
|
-
limit_map = pg.ImageItem()
|
431
|
-
limit_map.setImage(limit_map_data)
|
432
|
-
plot.addItem(limit_map)
|
433
|
-
|
434
|
-
# Translate and scale the image item to match the motor coordinates
|
435
|
-
tr = QtGui.QTransform()
|
436
|
-
tr.translate(limit_x_min, limit_y_min)
|
437
|
-
limit_map.setTransform(tr)
|
438
|
-
|
439
|
-
def _get_motor_init_position(self, name: str, entry: str) -> float:
|
440
|
-
"""
|
441
|
-
Get the motor initial position from the config.
|
442
|
-
Args:
|
443
|
-
name(str): Motor name.
|
444
|
-
entry(str): Motor entry.
|
445
|
-
Returns:
|
446
|
-
float: Motor initial position.
|
447
|
-
"""
|
448
|
-
init_position = round(self.dev[name].read()[entry]["value"], self.precision)
|
449
|
-
return init_position
|
450
|
-
|
451
|
-
def _update_plots(self):
|
452
|
-
"""Update the motor position on plots."""
|
453
|
-
for plot_name, curve_list in self.curves_data.items():
|
454
|
-
plot_config = next(
|
455
|
-
(pc for pc in self.plot_data if pc.get("plot_name") == plot_name), None
|
456
|
-
)
|
457
|
-
if not plot_config:
|
458
|
-
continue
|
459
|
-
|
460
|
-
# Get the motor coordinates
|
461
|
-
x_motor_name = plot_config["signals"]["x"][0]["name"]
|
462
|
-
x_motor_entry = plot_config["signals"]["x"][0]["entry"]
|
463
|
-
y_motor_name = plot_config["signals"]["y"][0]["name"]
|
464
|
-
y_motor_entry = plot_config["signals"]["y"][0]["entry"]
|
465
|
-
|
466
|
-
# update motor position only if there is data
|
467
|
-
if (
|
468
|
-
len(self.database[x_motor_name][x_motor_entry]) >= 1
|
469
|
-
and len(self.database[y_motor_name][y_motor_entry]) >= 1
|
470
|
-
):
|
471
|
-
# Relevant data for the plot
|
472
|
-
motor_x_data = self.database[x_motor_name][x_motor_entry]
|
473
|
-
motor_y_data = self.database[y_motor_name][y_motor_entry]
|
474
|
-
|
475
|
-
# Setup gradient brush for history
|
476
|
-
brushes = [pg.mkBrush(50, 50, 50, 255)] * len(motor_x_data)
|
477
|
-
|
478
|
-
# Calculate the decrement step based on self.num_dim_points
|
479
|
-
decrement_step = (255 - 50) / self.num_dim_points
|
480
|
-
|
481
|
-
for i in range(1, min(self.num_dim_points + 1, len(motor_x_data) + 1)):
|
482
|
-
brightness = max(60, 255 - decrement_step * (i - 1))
|
483
|
-
brushes[-i] = pg.mkBrush(brightness, brightness, brightness, 255)
|
484
|
-
|
485
|
-
brushes[-1] = pg.mkBrush(
|
486
|
-
255, 255, 255, 255
|
487
|
-
) # Newest point is always full brightness
|
488
|
-
|
489
|
-
# Update the scatter plot
|
490
|
-
self.curves_data[plot_name]["pos"].setData(
|
491
|
-
x=motor_x_data, y=motor_y_data, brush=brushes, pen=None, size=self.scatter_size
|
492
|
-
)
|
493
|
-
|
494
|
-
# Get last know position for crosshair
|
495
|
-
current_x = motor_x_data[-1]
|
496
|
-
current_y = motor_y_data[-1]
|
497
|
-
|
498
|
-
# Update plot title
|
499
|
-
self.plots[plot_name].setTitle(
|
500
|
-
f"Motor position: ({round(current_x,self.precision)}, {round(current_y,self.precision)})"
|
501
|
-
)
|
502
|
-
|
503
|
-
# Update the crosshair
|
504
|
-
self.curves_data[plot_name]["highlight_V"].setPos(current_x)
|
505
|
-
self.curves_data[plot_name]["highlight_H"].setPos(current_y)
|
506
|
-
|
507
|
-
@pyqtSlot(list, str, str)
|
508
|
-
def plot_saved_coordinates(self, coordinates: list, tag: str, color: str):
|
509
|
-
"""
|
510
|
-
Plot saved coordinates on the map.
|
511
|
-
Args:
|
512
|
-
coordinates(list): List of coordinates to be plotted.
|
513
|
-
tag(str): Tag for the coordinates for future reference.
|
514
|
-
color(str): Color to plot coordinates in.
|
515
|
-
"""
|
516
|
-
for plot_name in self.plots:
|
517
|
-
plot = self.plots[plot_name]
|
518
|
-
|
519
|
-
# Clear previous saved points
|
520
|
-
if tag in self.curves_data[plot_name]:
|
521
|
-
plot.removeItem(self.curves_data[plot_name][tag])
|
522
|
-
|
523
|
-
# Filter coordinates to be shown
|
524
|
-
visible_coords = [coord[:2] for coord in coordinates if coord[2]]
|
525
|
-
|
526
|
-
if visible_coords:
|
527
|
-
saved_points = pg.ScatterPlotItem(
|
528
|
-
pos=np.array(visible_coords), brush=pg.mkBrush(color)
|
529
|
-
)
|
530
|
-
plot.addItem(saved_points)
|
531
|
-
self.curves_data[plot_name][tag] = saved_points
|
532
|
-
|
533
|
-
@pyqtSlot(dict)
|
534
|
-
def on_device_readback(self, msg: dict):
|
535
|
-
"""
|
536
|
-
Update the motor coordinates on the plots.
|
537
|
-
Args:
|
538
|
-
msg (dict): Message received with device readback data.
|
539
|
-
"""
|
540
|
-
|
541
|
-
for device_name, device_info in msg["signals"].items():
|
542
|
-
# Check if the device is relevant to our current context
|
543
|
-
if device_name in self.device_mapping:
|
544
|
-
self._update_device_data(device_name, device_info["value"])
|
545
|
-
|
546
|
-
self.update_signal.emit()
|
547
|
-
|
548
|
-
def _update_device_data(self, device_name: str, value: float):
|
549
|
-
"""
|
550
|
-
Update the device data.
|
551
|
-
Args:
|
552
|
-
device_name (str): Device name.
|
553
|
-
value (float): Device value.
|
554
|
-
"""
|
555
|
-
if device_name in self.database:
|
556
|
-
self.database[device_name][device_name].append(value)
|
557
|
-
|
558
|
-
corresponding_device = self.device_mapping.get(device_name)
|
559
|
-
if corresponding_device and corresponding_device in self.database:
|
560
|
-
last_value = (
|
561
|
-
self.database[corresponding_device][corresponding_device][-1]
|
562
|
-
if self.database[corresponding_device][corresponding_device]
|
563
|
-
else None
|
564
|
-
)
|
565
|
-
self.database[corresponding_device][corresponding_device].append(last_value)
|
566
|
-
|
567
|
-
|
568
|
-
if __name__ == "__main__": # pragma: no cover
|
569
|
-
import argparse
|
570
|
-
import json
|
571
|
-
import sys
|
572
|
-
|
573
|
-
parser = argparse.ArgumentParser()
|
574
|
-
parser.add_argument("--config_file", help="Path to the config file.")
|
575
|
-
parser.add_argument("--config", help="Path to the config file.")
|
576
|
-
parser.add_argument("--id", help="GUI ID.")
|
577
|
-
args = parser.parse_args()
|
578
|
-
|
579
|
-
if args.config is not None:
|
580
|
-
# Load config from file
|
581
|
-
config = json.loads(args.config)
|
582
|
-
elif args.config_file is not None:
|
583
|
-
# Load config from file
|
584
|
-
config = load_yaml(args.config_file)
|
585
|
-
else:
|
586
|
-
config = CONFIG_DEFAULT
|
587
|
-
|
588
|
-
client = BECDispatcher().client
|
589
|
-
client.start()
|
590
|
-
app = QApplication(sys.argv)
|
591
|
-
motor_map = MotorMap(config=config, gui_id=args.id, skip_validation=True)
|
592
|
-
motor_map.show()
|
593
|
-
|
594
|
-
sys.exit(app.exec())
|