bec-ipython-client 3.37.0__tar.gz → 3.88.0__tar.gz
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.
Potentially problematic release.
This version of bec-ipython-client might be problematic. Click here for more details.
- {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/.gitignore +3 -0
- {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/PKG-INFO +2 -2
- {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/bec_ipython_client/bec_startup.py +5 -2
- {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/bec_ipython_client/callbacks/device_progress.py +11 -6
- {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/bec_ipython_client/callbacks/ipython_live_updates.py +47 -19
- {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/bec_ipython_client/callbacks/live_table.py +80 -35
- bec_ipython_client-3.88.0/bec_ipython_client/callbacks/move_device.py +218 -0
- {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/bec_ipython_client/callbacks/utils.py +5 -23
- {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/bec_ipython_client/main.py +85 -8
- {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/bec_ipython_client/signals.py +9 -3
- {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/demo.py +2 -1
- {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/pyproject.toml +2 -2
- bec_ipython_client-3.88.0/tests/client_tests/conftest.py +19 -0
- {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/tests/client_tests/test_bec_client.py +36 -1
- bec_ipython_client-3.88.0/tests/client_tests/test_ipython_live_updates.py +350 -0
- bec_ipython_client-3.88.0/tests/client_tests/test_live_table.py +423 -0
- bec_ipython_client-3.88.0/tests/client_tests/test_move_callback.py +223 -0
- bec_ipython_client-3.88.0/tests/end-2-end/_ensure_requirements_container.py +21 -0
- bec_ipython_client-3.88.0/tests/end-2-end/test_procedures_e2e.py +134 -0
- {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/tests/end-2-end/test_scans_e2e.py +35 -13
- {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/tests/end-2-end/test_scans_lib_e2e.py +36 -32
- bec_ipython_client-3.37.0/bec_ipython_client/callbacks/move_device.py +0 -156
- bec_ipython_client-3.37.0/tests/client_tests/test_ipython_live_updates.py +0 -159
- bec_ipython_client-3.37.0/tests/client_tests/test_live_table.py +0 -322
- bec_ipython_client-3.37.0/tests/client_tests/test_move_callback.py +0 -181
- {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/bec_ipython_client/__init__.py +0 -0
- {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/bec_ipython_client/beamline_mixin.py +0 -0
- {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/bec_ipython_client/bec_magics.py +0 -0
- {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/bec_ipython_client/callbacks/__init__.py +0 -0
- {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/bec_ipython_client/high_level_interfaces/__init__.py +0 -0
- {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/bec_ipython_client/high_level_interfaces/bec_hli.py +0 -0
- {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/bec_ipython_client/high_level_interfaces/spec_hli.py +0 -0
- {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/bec_ipython_client/plugins/SLS/__init__.py +0 -0
- {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/bec_ipython_client/plugins/SLS/sls_info.py +0 -0
- {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/bec_ipython_client/plugins/XTreme/__init__.py +0 -0
- {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/bec_ipython_client/plugins/XTreme/x-treme.py +0 -0
- {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/bec_ipython_client/plugins/__init__.py +0 -0
- {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/bec_ipython_client/plugins/flomni/flomni_config.yaml +0 -0
- {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/bec_ipython_client/prettytable.py +0 -0
- {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/bec_ipython_client/progressbar.py +0 -0
- {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/tests/client_tests/test_beamline_mixins.py +0 -0
- {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/tests/client_tests/test_device_progress.py +0 -0
- {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/tests/client_tests/test_pretty_table.py +0 -0
- {bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/tests/conftest.py +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bec_ipython_client
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.88.0
|
|
4
4
|
Summary: BEC IPython client
|
|
5
5
|
Project-URL: Bug Tracker, https://github.com/bec-project/bec/issues
|
|
6
6
|
Project-URL: Homepage, https://github.com/bec-project/bec
|
|
7
7
|
Classifier: Development Status :: 3 - Alpha
|
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
|
9
9
|
Classifier: Topic :: Scientific/Engineering
|
|
10
|
-
Requires-Python: >=3.
|
|
10
|
+
Requires-Python: >=3.11
|
|
11
11
|
Requires-Dist: bec-lib~=3.0
|
|
12
12
|
Requires-Dist: ipython~=8.22
|
|
13
13
|
Requires-Dist: numpy<3.0,>=1.24
|
|
@@ -32,9 +32,12 @@ except (BECAuthenticationError, KeyboardInterrupt) as exc:
|
|
|
32
32
|
except Exception:
|
|
33
33
|
sys.excepthook(*sys.exc_info())
|
|
34
34
|
else:
|
|
35
|
-
if bec.started and
|
|
35
|
+
if bec.started and BECGuiClient is not None:
|
|
36
36
|
gui = bec.gui = BECGuiClient()
|
|
37
|
-
|
|
37
|
+
if _main_dict["args"].gui_id:
|
|
38
|
+
gui.connect_to_gui_server(_main_dict["args"].gui_id)
|
|
39
|
+
if not _main_dict["args"].nogui:
|
|
40
|
+
gui.show()
|
|
38
41
|
|
|
39
42
|
_available_plugins = plugin_helper.get_ipython_client_startup_plugins(state="post")
|
|
40
43
|
if _available_plugins:
|
|
@@ -14,11 +14,16 @@ class LiveUpdatesDeviceProgress(LiveUpdatesTable):
|
|
|
14
14
|
|
|
15
15
|
REPORT_TYPE = "device_progress"
|
|
16
16
|
|
|
17
|
-
def
|
|
17
|
+
def core(self):
|
|
18
|
+
"""core function to run the live updates for the table"""
|
|
19
|
+
self._wait_for_report_instructions()
|
|
20
|
+
self._run_update(self.report_instruction[self.REPORT_TYPE])
|
|
21
|
+
|
|
22
|
+
def _run_update(self, device_names: list[str]):
|
|
18
23
|
"""Run the update loop for the progress bar.
|
|
19
24
|
|
|
20
25
|
Args:
|
|
21
|
-
device_names (str): The name of the device to monitor.
|
|
26
|
+
device_names (list[str]): The name of the device to monitor.
|
|
22
27
|
"""
|
|
23
28
|
with ScanProgressBar(
|
|
24
29
|
scan_number=self.scan_item.scan_number, clear_on_exit=False
|
|
@@ -29,7 +34,7 @@ class LiveUpdatesDeviceProgress(LiveUpdatesTable):
|
|
|
29
34
|
self._print_client_msgs_asap()
|
|
30
35
|
self._print_client_msgs_all()
|
|
31
36
|
|
|
32
|
-
def _update_progressbar(self, progressbar: ScanProgressBar, device_names: str) -> bool:
|
|
37
|
+
def _update_progressbar(self, progressbar: ScanProgressBar, device_names: list[str]) -> bool:
|
|
33
38
|
"""Update the progressbar based on the device status message
|
|
34
39
|
|
|
35
40
|
Args:
|
|
@@ -41,17 +46,17 @@ class LiveUpdatesDeviceProgress(LiveUpdatesTable):
|
|
|
41
46
|
self.check_alarms()
|
|
42
47
|
status = self.bec.connector.get(MessageEndpoints.device_progress(device_names[0]))
|
|
43
48
|
if not status:
|
|
44
|
-
logger.
|
|
49
|
+
logger.trace("waiting for new data point")
|
|
45
50
|
time.sleep(0.1)
|
|
46
51
|
return False
|
|
47
52
|
if status.metadata.get("scan_id") != self.scan_item.scan_id:
|
|
48
|
-
logger.
|
|
53
|
+
logger.trace("waiting for new data point")
|
|
49
54
|
time.sleep(0.1)
|
|
50
55
|
return False
|
|
51
56
|
|
|
52
57
|
point_id = status.content.get("value")
|
|
53
58
|
if point_id is None:
|
|
54
|
-
logger.
|
|
59
|
+
logger.trace("waiting for new data point")
|
|
55
60
|
time.sleep(0.1)
|
|
56
61
|
return False
|
|
57
62
|
|
|
@@ -31,7 +31,7 @@ class IPythonLiveUpdates:
|
|
|
31
31
|
self._interrupted_request = None
|
|
32
32
|
self._active_callback = None
|
|
33
33
|
self._processed_instructions = 0
|
|
34
|
-
self._active_request = None
|
|
34
|
+
self._active_request: messages.ScanQueueMessage | None = None
|
|
35
35
|
self._user_callback = None
|
|
36
36
|
self._request_block_index = collections.defaultdict(lambda: 0)
|
|
37
37
|
self._request_block_id = None
|
|
@@ -47,9 +47,11 @@ class IPythonLiveUpdates:
|
|
|
47
47
|
Args:
|
|
48
48
|
report_instructions (list): The list of report instructions.
|
|
49
49
|
"""
|
|
50
|
-
|
|
50
|
+
if not self._active_request:
|
|
51
|
+
return
|
|
52
|
+
scan_type = self._active_request.scan_type
|
|
51
53
|
if scan_type in ["open_scan_def", "close_scan_def"]:
|
|
52
|
-
self._process_instruction({"scan_progress": 0})
|
|
54
|
+
self._process_instruction({"scan_progress": {"points": 0, "show_table": True}})
|
|
53
55
|
return
|
|
54
56
|
if scan_type == "close_scan_group":
|
|
55
57
|
return
|
|
@@ -74,6 +76,9 @@ class IPythonLiveUpdates:
|
|
|
74
76
|
scan_report_type = list(instr.keys())[0]
|
|
75
77
|
scan_def_id = self.client.scans._scan_def_id
|
|
76
78
|
interactive_scan = self.client.scans._interactive_scan
|
|
79
|
+
if self._active_request is None:
|
|
80
|
+
# Already checked in caller method. It is just for type checking purposes.
|
|
81
|
+
return
|
|
77
82
|
if scan_def_id is None or interactive_scan:
|
|
78
83
|
if scan_report_type == "readback":
|
|
79
84
|
LiveUpdatesReadbackProgressbar(
|
|
@@ -126,17 +131,22 @@ class IPythonLiveUpdates:
|
|
|
126
131
|
)
|
|
127
132
|
self._active_callback.run()
|
|
128
133
|
|
|
129
|
-
def _available_req_blocks(
|
|
134
|
+
def _available_req_blocks(
|
|
135
|
+
self, queue: QueueItem, request: messages.ScanQueueMessage
|
|
136
|
+
) -> list[messages.RequestBlock]:
|
|
130
137
|
"""Get the available request blocks.
|
|
131
138
|
|
|
132
139
|
Args:
|
|
133
140
|
queue (QueueItem): The queue item.
|
|
134
141
|
request (messages.ScanQueueMessage): The request message.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
list[messages.RequestBlock]: The list of available request blocks.
|
|
135
145
|
"""
|
|
136
146
|
available_blocks = [
|
|
137
147
|
req_block
|
|
138
148
|
for req_block in queue.request_blocks
|
|
139
|
-
if req_block
|
|
149
|
+
if req_block.RID == request.metadata["RID"]
|
|
140
150
|
]
|
|
141
151
|
return available_blocks
|
|
142
152
|
|
|
@@ -151,11 +161,14 @@ class IPythonLiveUpdates:
|
|
|
151
161
|
scan_request = ScanRequestMixin(self.client, request.metadata["RID"])
|
|
152
162
|
scan_request.wait()
|
|
153
163
|
|
|
164
|
+
# After .wait, we can be sure that the queue item is available, so we can
|
|
165
|
+
assert scan_request.scan_queue_request is not None
|
|
166
|
+
|
|
154
167
|
# get the corresponding queue item
|
|
155
|
-
while not scan_request.
|
|
168
|
+
while not scan_request.scan_queue_request.queue:
|
|
156
169
|
time.sleep(0.01)
|
|
157
170
|
|
|
158
|
-
self._current_queue = queue = scan_request.
|
|
171
|
+
self._current_queue = queue = scan_request.scan_queue_request.queue
|
|
159
172
|
self._request_block_id = req_id = self._active_request.metadata.get("RID")
|
|
160
173
|
|
|
161
174
|
while queue.status not in ["COMPLETED", "ABORTED", "HALTED"]:
|
|
@@ -164,7 +177,7 @@ class IPythonLiveUpdates:
|
|
|
164
177
|
|
|
165
178
|
available_blocks = self._available_req_blocks(queue, request)
|
|
166
179
|
req_block = available_blocks[self._request_block_index[req_id]]
|
|
167
|
-
report_instructions = req_block.
|
|
180
|
+
report_instructions = req_block.report_instructions or []
|
|
168
181
|
self._process_report_instructions(report_instructions)
|
|
169
182
|
|
|
170
183
|
self._reset()
|
|
@@ -188,12 +201,19 @@ class IPythonLiveUpdates:
|
|
|
188
201
|
self.client.queue.request_scan_halt()
|
|
189
202
|
|
|
190
203
|
def _element_in_queue(self) -> bool:
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
)
|
|
194
|
-
if not queue:
|
|
204
|
+
if self.client.queue is None:
|
|
205
|
+
return False
|
|
206
|
+
if (csq := self.client.queue.queue_storage.current_scan_queue) is None:
|
|
195
207
|
return False
|
|
196
|
-
|
|
208
|
+
scan_queue_status = csq.get("primary")
|
|
209
|
+
if scan_queue_status is None:
|
|
210
|
+
return False
|
|
211
|
+
queue_info = scan_queue_status.info
|
|
212
|
+
if not queue_info:
|
|
213
|
+
return False
|
|
214
|
+
if self._current_queue is None:
|
|
215
|
+
return False
|
|
216
|
+
return self._current_queue.queue_id == queue_info[0].queue_id
|
|
197
217
|
|
|
198
218
|
def _process_queue(
|
|
199
219
|
self, queue: QueueItem, request: messages.ScanQueueMessage, req_id: str
|
|
@@ -213,9 +233,11 @@ class IPythonLiveUpdates:
|
|
|
213
233
|
if not queue.request_blocks or not queue.status or queue.queue_position is None:
|
|
214
234
|
return False
|
|
215
235
|
if queue.status == "PENDING" and queue.queue_position > 0:
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
236
|
+
primary_queue = self.client.queue.queue_storage.current_scan_queue.get("primary")
|
|
237
|
+
|
|
238
|
+
if primary_queue is None:
|
|
239
|
+
return False
|
|
240
|
+
status = primary_queue.status
|
|
219
241
|
print(
|
|
220
242
|
"Scan is enqueued and is waiting for execution. Current position in queue:"
|
|
221
243
|
f" {queue.queue_position + 1}. Queue status: {status}.",
|
|
@@ -223,14 +245,16 @@ class IPythonLiveUpdates:
|
|
|
223
245
|
flush=True,
|
|
224
246
|
)
|
|
225
247
|
available_blocks = self._available_req_blocks(queue, request)
|
|
248
|
+
if not available_blocks:
|
|
249
|
+
return False
|
|
226
250
|
req_block = available_blocks[self._request_block_index[req_id]]
|
|
227
|
-
if req_block
|
|
251
|
+
if req_block.msg.scan_type in [
|
|
228
252
|
"open_scan_def",
|
|
229
253
|
"mv",
|
|
230
254
|
]: # TODO: make this more general for all scan types that don't have report instructions
|
|
231
255
|
return True
|
|
232
256
|
|
|
233
|
-
report_instructions = req_block[
|
|
257
|
+
report_instructions = req_block.report_instructions or []
|
|
234
258
|
if not report_instructions:
|
|
235
259
|
return False
|
|
236
260
|
self._process_report_instructions(report_instructions)
|
|
@@ -258,7 +282,11 @@ class IPythonLiveUpdates:
|
|
|
258
282
|
self._current_queue = None
|
|
259
283
|
self._user_callback = None
|
|
260
284
|
self._processed_instructions = 0
|
|
261
|
-
scan_closed =
|
|
285
|
+
scan_closed = (
|
|
286
|
+
forced
|
|
287
|
+
or self._active_request is None
|
|
288
|
+
or (self._active_request.scan_type == "close_scan_def")
|
|
289
|
+
)
|
|
262
290
|
self._active_request = None
|
|
263
291
|
|
|
264
292
|
if self.client.scans._scan_def_id and not scan_closed:
|
{bec_ipython_client-3.37.0 → bec_ipython_client-3.88.0}/bec_ipython_client/callbacks/live_table.py
RENAMED
|
@@ -2,7 +2,9 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import time
|
|
4
4
|
from collections.abc import Callable
|
|
5
|
-
from typing import TYPE_CHECKING
|
|
5
|
+
from typing import TYPE_CHECKING, Any, SupportsFloat
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
6
8
|
|
|
7
9
|
from bec_ipython_client.prettytable import PrettyTable
|
|
8
10
|
from bec_ipython_client.progressbar import ScanProgressBar
|
|
@@ -54,7 +56,6 @@ class LiveUpdatesTable(LiveUpdatesBase):
|
|
|
54
56
|
super().__init__(
|
|
55
57
|
bec, report_instruction=report_instruction, request=request, callbacks=callbacks
|
|
56
58
|
)
|
|
57
|
-
self.scan_queue_request = None
|
|
58
59
|
self.scan_item = None
|
|
59
60
|
self.dev_values = None
|
|
60
61
|
self.point_data = None
|
|
@@ -66,16 +67,19 @@ class LiveUpdatesTable(LiveUpdatesBase):
|
|
|
66
67
|
if print_table_data is not None
|
|
67
68
|
else self.REPORT_TYPE == "scan_progress"
|
|
68
69
|
)
|
|
70
|
+
self._devices_with_bad_precision = set()
|
|
69
71
|
|
|
70
72
|
def wait_for_scan_to_start(self):
|
|
71
73
|
"""wait until the scan starts"""
|
|
72
74
|
while True:
|
|
75
|
+
if not self.scan_item or not self.scan_item.queue:
|
|
76
|
+
raise RuntimeError("No scan item or scan queue available.")
|
|
73
77
|
queue_pos = self.scan_item.queue.queue_position
|
|
74
78
|
self.check_alarms()
|
|
75
79
|
if self.scan_item.status == "closed":
|
|
76
80
|
break
|
|
77
81
|
if queue_pos is None:
|
|
78
|
-
logger.
|
|
82
|
+
logger.trace(f"Could not find queue entry for scan_id {self.scan_item.scan_id}")
|
|
79
83
|
continue
|
|
80
84
|
if queue_pos == 0:
|
|
81
85
|
break
|
|
@@ -157,7 +161,20 @@ class LiveUpdatesTable(LiveUpdatesBase):
|
|
|
157
161
|
return header
|
|
158
162
|
|
|
159
163
|
def update_scan_item(self, timeout: float = 15):
|
|
160
|
-
"""
|
|
164
|
+
"""
|
|
165
|
+
Get the current scan item and update self.scan_item
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
timeout (float): timeout in seconds
|
|
169
|
+
|
|
170
|
+
Raises:
|
|
171
|
+
RuntimeError: if no scan queue request is available
|
|
172
|
+
TimeoutError: if no scan item is found before reaching the timeout
|
|
173
|
+
|
|
174
|
+
"""
|
|
175
|
+
if not self.scan_queue_request:
|
|
176
|
+
raise RuntimeError("No scan queue request available.")
|
|
177
|
+
|
|
161
178
|
start = time.time()
|
|
162
179
|
while self.scan_queue_request.scan is None:
|
|
163
180
|
self.check_alarms()
|
|
@@ -168,25 +185,38 @@ class LiveUpdatesTable(LiveUpdatesBase):
|
|
|
168
185
|
|
|
169
186
|
def core(self):
|
|
170
187
|
"""core function to run the live updates for the table"""
|
|
188
|
+
self._wait_for_report_instructions()
|
|
189
|
+
show_table = self.report_instruction[self.REPORT_TYPE].get("show_table", True)
|
|
190
|
+
self._print_table_data = show_table
|
|
191
|
+
self._run_update(self.report_instruction[self.REPORT_TYPE]["points"])
|
|
192
|
+
|
|
193
|
+
def _wait_for_report_instructions(self):
|
|
194
|
+
"""wait until the report instructions are available"""
|
|
195
|
+
if not self.scan_queue_request or not self.scan_item or not self.scan_item.queue:
|
|
196
|
+
logger.warning(
|
|
197
|
+
f"Cannot wait for report instructions. scan_queue_request: {self.scan_queue_request}, scan_item: {self.scan_item}, scan_item.queue: {getattr(self.scan_item, 'queue', None)}"
|
|
198
|
+
)
|
|
199
|
+
return
|
|
171
200
|
req_ID = self.scan_queue_request.requestID
|
|
172
201
|
while True:
|
|
173
202
|
request_block = [
|
|
174
|
-
req for req in self.scan_item.queue.request_blocks if req
|
|
203
|
+
req for req in self.scan_item.queue.request_blocks if req.RID == req_ID
|
|
175
204
|
][0]
|
|
176
|
-
if not request_block
|
|
205
|
+
if not request_block.is_scan:
|
|
177
206
|
break
|
|
178
|
-
if request_block
|
|
207
|
+
if request_block.report_instructions:
|
|
179
208
|
break
|
|
180
209
|
self.check_alarms()
|
|
181
210
|
|
|
182
|
-
self._run_update(self.report_instruction[self.REPORT_TYPE])
|
|
183
|
-
|
|
184
211
|
def _run_update(self, target_num_points: int):
|
|
185
212
|
"""run the update loop with the progress bar
|
|
186
213
|
|
|
187
214
|
Args:
|
|
188
215
|
target_num_points (int): number of points to be collected
|
|
189
216
|
"""
|
|
217
|
+
if not self.scan_item:
|
|
218
|
+
logger.warning("No scan item available for live updates.")
|
|
219
|
+
return
|
|
190
220
|
with ScanProgressBar(
|
|
191
221
|
scan_number=self.scan_item.scan_number, clear_on_exit=self._print_table_data
|
|
192
222
|
) as progressbar:
|
|
@@ -208,7 +238,7 @@ class LiveUpdatesTable(LiveUpdatesBase):
|
|
|
208
238
|
self.bec.callbacks.poll()
|
|
209
239
|
self.scan_item.poll_callbacks()
|
|
210
240
|
else:
|
|
211
|
-
logger.
|
|
241
|
+
logger.trace("waiting for new data point")
|
|
212
242
|
time.sleep(0.1)
|
|
213
243
|
|
|
214
244
|
if not self.scan_item.num_points:
|
|
@@ -218,12 +248,25 @@ class LiveUpdatesTable(LiveUpdatesBase):
|
|
|
218
248
|
break
|
|
219
249
|
if self.point_id > self.scan_item.num_points:
|
|
220
250
|
raise RuntimeError("Received more points than expected.")
|
|
251
|
+
|
|
221
252
|
if len(self.scan_item.live_data) == 0 and self.scan_item.status == "closed":
|
|
253
|
+
msg = self.scan_item.status_message
|
|
254
|
+
if not msg:
|
|
255
|
+
continue
|
|
256
|
+
if msg.readout_priority.get("monitored", []):
|
|
257
|
+
continue
|
|
258
|
+
|
|
222
259
|
logger.warning(
|
|
223
260
|
f"\n Scan {self.scan_item.scan_number} finished. No monitored devices enabled, please check your config."
|
|
224
261
|
)
|
|
225
262
|
break
|
|
226
263
|
|
|
264
|
+
def _warn_bad_precisions(self):
|
|
265
|
+
if self._devices_with_bad_precision != set():
|
|
266
|
+
for dev, prec in self._devices_with_bad_precision:
|
|
267
|
+
logger.warning(f"Device {dev} reported malformed precision of {prec}!")
|
|
268
|
+
self._devices_with_bad_precision = set()
|
|
269
|
+
|
|
227
270
|
@property
|
|
228
271
|
def _print_table_data(self) -> bool:
|
|
229
272
|
"""Checks if the table should be printed or not.
|
|
@@ -266,40 +309,41 @@ class LiveUpdatesTable(LiveUpdatesBase):
|
|
|
266
309
|
|
|
267
310
|
if self.point_id % 100 == 0:
|
|
268
311
|
print(self.table.get_header_lines())
|
|
269
|
-
|
|
312
|
+
|
|
313
|
+
signals_precisions = []
|
|
270
314
|
for dev in self.devices:
|
|
271
315
|
if dev in self.bec.device_manager.devices:
|
|
272
316
|
obj = self.bec.device_manager.devices[dev]
|
|
273
317
|
for hint in obj._hints:
|
|
274
318
|
signal = self.point_data.content["data"].get(obj.root.name, {}).get(hint)
|
|
275
|
-
if signal is None
|
|
276
|
-
|
|
319
|
+
if signal is None:
|
|
320
|
+
signals_precisions.append((None, None))
|
|
277
321
|
else:
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
if isinstance(value, (int, float)):
|
|
284
|
-
print_value = f"{value:.{precision}f}"
|
|
285
|
-
else:
|
|
286
|
-
print_value = str(value)
|
|
287
|
-
self.dev_values[ind] = print_value
|
|
288
|
-
ind += 1
|
|
322
|
+
prec = getattr(obj, "precision", 2)
|
|
323
|
+
if not isinstance(prec, int):
|
|
324
|
+
self._devices_with_bad_precision.add((dev, prec))
|
|
325
|
+
prec = 2
|
|
326
|
+
signals_precisions.append((signal, prec))
|
|
289
327
|
else:
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
else:
|
|
296
|
-
print_value = str(value)
|
|
297
|
-
else:
|
|
298
|
-
print_value = "N/A"
|
|
299
|
-
self.dev_values[ind] = print_value
|
|
300
|
-
ind += 1
|
|
328
|
+
signals_precisions.append((self.point_data.content["data"].get(dev, {}), 2))
|
|
329
|
+
|
|
330
|
+
for i, (signal, precision) in enumerate(signals_precisions):
|
|
331
|
+
self.dev_values[i] = self._format_value(signal, precision)
|
|
332
|
+
|
|
301
333
|
print(self.table.get_row(str(self.point_id), *self.dev_values))
|
|
302
334
|
|
|
335
|
+
def _format_value(self, signal: dict | None, precision: int = 2):
|
|
336
|
+
if signal is None:
|
|
337
|
+
return "N/A"
|
|
338
|
+
val = signal.get("value")
|
|
339
|
+
if isinstance(val, SupportsFloat) and not isinstance(val, np.ndarray):
|
|
340
|
+
if precision < 0:
|
|
341
|
+
# This is to cover the special case when EPICS returns a negative precision.
|
|
342
|
+
# More info: https://epics.anl.gov/tech-talk/2004/msg00434.php
|
|
343
|
+
return f"{float(val):.{-precision}g}"
|
|
344
|
+
return f"{float(val):.{precision}f}"
|
|
345
|
+
return str(val)
|
|
346
|
+
|
|
303
347
|
def close_table(self):
|
|
304
348
|
"""close the table and print the footer"""
|
|
305
349
|
if not self.table:
|
|
@@ -310,6 +354,7 @@ class LiveUpdatesTable(LiveUpdatesBase):
|
|
|
310
354
|
f"Scan {self.scan_item.scan_number} finished. Scan ID {self.scan_item.scan_id}. Elapsed time: {elapsed_time:.2f} s"
|
|
311
355
|
)
|
|
312
356
|
)
|
|
357
|
+
self._warn_bad_precisions()
|
|
313
358
|
|
|
314
359
|
def process_request(self):
|
|
315
360
|
"""process the request and start the core loop for live updates"""
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from typing import TYPE_CHECKING, cast
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from bec_ipython_client.progressbar import DeviceProgressBar
|
|
10
|
+
from bec_lib import messages
|
|
11
|
+
from bec_lib.endpoints import MessageEndpoints
|
|
12
|
+
from bec_lib.redis_connector import MessageObject
|
|
13
|
+
|
|
14
|
+
from .utils import LiveUpdatesBase, check_alarms
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from bec_lib.client import BECClient
|
|
18
|
+
from bec_lib.devicemanager import DeviceManagerBase
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ReadbackDataHandler:
|
|
22
|
+
"""Helper class to get the current device values and request-done messages."""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self, device_manager: DeviceManagerBase, devices: list[str], request_id: str
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Helper class to get the current device values and request-done messages.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
device_manager (DeviceManagerBase): device manager
|
|
31
|
+
devices (list): list of devices to monitor
|
|
32
|
+
request_id (str): request ID
|
|
33
|
+
"""
|
|
34
|
+
self.device_manager = device_manager
|
|
35
|
+
self.devices = devices
|
|
36
|
+
self.connector = device_manager.connector
|
|
37
|
+
self.request_id = request_id
|
|
38
|
+
self._devices_received = {dev: False for dev in devices}
|
|
39
|
+
self.data: dict[str, messages.DeviceMessage] = {}
|
|
40
|
+
self._devices_done_state: dict[str, tuple[bool, bool]] = {
|
|
41
|
+
dev: (False, False) for dev in devices
|
|
42
|
+
}
|
|
43
|
+
self.requests_done = threading.Event()
|
|
44
|
+
self._register_callbacks()
|
|
45
|
+
|
|
46
|
+
def _register_callbacks(self):
|
|
47
|
+
"""register callbacks for device readback messages."""
|
|
48
|
+
for dev in self.devices:
|
|
49
|
+
self.connector.register(
|
|
50
|
+
MessageEndpoints.device_readback(dev), cb=self.on_readback, parent=self, device=dev
|
|
51
|
+
)
|
|
52
|
+
self.connector.register(
|
|
53
|
+
MessageEndpoints.device_req_status(self.request_id),
|
|
54
|
+
cb=self.on_req_status,
|
|
55
|
+
from_start=True,
|
|
56
|
+
parent=self,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def _unregister_callbacks(self):
|
|
60
|
+
"""unregister callbacks for device readback messages."""
|
|
61
|
+
for dev in self.devices:
|
|
62
|
+
self.connector.unregister(MessageEndpoints.device_readback(dev), cb=self.on_readback)
|
|
63
|
+
self.connector.unregister(
|
|
64
|
+
MessageEndpoints.device_req_status(self.request_id), cb=self.on_req_status
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def on_req_status(
|
|
69
|
+
msg_obj: dict[str, messages.DeviceReqStatusMessage], parent: ReadbackDataHandler
|
|
70
|
+
):
|
|
71
|
+
"""Callback for device request status messages to track which devices are done.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
msg_obj (dict[str, messages.DeviceReqStatusMessage]): message object or device request status message
|
|
75
|
+
parent (ReadbackDataHandler): parent instance
|
|
76
|
+
"""
|
|
77
|
+
# pylint: disable=protected-access
|
|
78
|
+
msg = msg_obj["data"]
|
|
79
|
+
if msg.request_id != parent.request_id:
|
|
80
|
+
return
|
|
81
|
+
device = msg.device
|
|
82
|
+
parent._devices_done_state[device] = (True, msg.success)
|
|
83
|
+
|
|
84
|
+
if (
|
|
85
|
+
all(done for done, _ in parent._devices_done_state.values())
|
|
86
|
+
and not parent.requests_done.is_set()
|
|
87
|
+
):
|
|
88
|
+
parent._on_request_done()
|
|
89
|
+
|
|
90
|
+
@staticmethod
|
|
91
|
+
def on_readback(msg_obj: MessageObject, parent: ReadbackDataHandler, device: str):
|
|
92
|
+
"""Callback for updating device readback data.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
msg_obj (MessageObject): message object
|
|
96
|
+
parent (ReadbackDataHandler): parent instance
|
|
97
|
+
device (str): device name
|
|
98
|
+
"""
|
|
99
|
+
# pylint: disable=protected-access
|
|
100
|
+
msg: messages.DeviceMessage = cast(messages.DeviceMessage, msg_obj.value)
|
|
101
|
+
parent._devices_received[device] = True
|
|
102
|
+
parent.data[device] = msg
|
|
103
|
+
|
|
104
|
+
def _on_request_done(self):
|
|
105
|
+
"""Callback for when all requests are done."""
|
|
106
|
+
self.requests_done.set()
|
|
107
|
+
self._unregister_callbacks()
|
|
108
|
+
|
|
109
|
+
def get_device_values(self) -> list:
|
|
110
|
+
"""get the current device values
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
list: list of device values
|
|
114
|
+
"""
|
|
115
|
+
values = []
|
|
116
|
+
for dev in self.devices:
|
|
117
|
+
val = self.data.get(dev)
|
|
118
|
+
if val is None:
|
|
119
|
+
signal_data = self.device_manager.devices[dev].read(cached=True)
|
|
120
|
+
else:
|
|
121
|
+
signal_data = val.signals
|
|
122
|
+
# pylint: disable=protected-access
|
|
123
|
+
hints = self.device_manager.devices[dev]._hints
|
|
124
|
+
# if we have hints, use them to get the value, otherwise just use the first value
|
|
125
|
+
if hints:
|
|
126
|
+
values.append(signal_data.get(hints[0]).get("value"))
|
|
127
|
+
else:
|
|
128
|
+
values.append(signal_data.get(list(signal_data.keys())[0]).get("value"))
|
|
129
|
+
return values
|
|
130
|
+
|
|
131
|
+
def done(self) -> bool:
|
|
132
|
+
"""check if all devices are done
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
bool: True if all devices are done, False otherwise
|
|
136
|
+
"""
|
|
137
|
+
return self.requests_done.is_set()
|
|
138
|
+
|
|
139
|
+
def device_states(self) -> dict[str, tuple[bool, bool]]:
|
|
140
|
+
"""
|
|
141
|
+
Return the current device done states.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
dict: dictionary with device names as keys and tuples of (done, success) as values
|
|
145
|
+
"""
|
|
146
|
+
return self._devices_done_state
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class LiveUpdatesReadbackProgressbar(LiveUpdatesBase):
|
|
150
|
+
"""Live feedback on motor movements using a progressbar.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
bec (BECClient): BECClient instance
|
|
154
|
+
report_instruction (list, optional): report instruction for the scan. Defaults to None.
|
|
155
|
+
request (messages.ScanQueueMessage, optional): scan queue request message. Defaults to None.
|
|
156
|
+
callbacks (list[Callable], optional): list of callbacks to register. Defaults to None.
|
|
157
|
+
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
def __init__(
|
|
161
|
+
self,
|
|
162
|
+
bec: BECClient,
|
|
163
|
+
report_instruction: list | None = None,
|
|
164
|
+
request: messages.ScanQueueMessage | None = None,
|
|
165
|
+
callbacks: list[Callable] | None = None,
|
|
166
|
+
) -> None:
|
|
167
|
+
super().__init__(
|
|
168
|
+
bec, report_instruction=report_instruction, request=request, callbacks=callbacks
|
|
169
|
+
)
|
|
170
|
+
if report_instruction:
|
|
171
|
+
self.devices = report_instruction["readback"]["devices"]
|
|
172
|
+
else:
|
|
173
|
+
self.devices = list(request.content["parameter"]["args"].keys())
|
|
174
|
+
|
|
175
|
+
def core(self):
|
|
176
|
+
"""core function to monitor the device values and update the progressbar accordingly."""
|
|
177
|
+
request_id = self.request.metadata["RID"]
|
|
178
|
+
if self.report_instruction:
|
|
179
|
+
self.devices = self.report_instruction["readback"]["devices"]
|
|
180
|
+
request_id = self.report_instruction["readback"]["RID"]
|
|
181
|
+
data_source = ReadbackDataHandler(self.bec.device_manager, self.devices, request_id)
|
|
182
|
+
start_values = data_source.get_device_values()
|
|
183
|
+
self.wait_for_request_acceptance()
|
|
184
|
+
|
|
185
|
+
if self.report_instruction:
|
|
186
|
+
target_values = self.report_instruction["readback"]["end"]
|
|
187
|
+
|
|
188
|
+
start_instr = self.report_instruction["readback"].get("start")
|
|
189
|
+
if start_instr:
|
|
190
|
+
start_values = self.report_instruction["readback"]["start"]
|
|
191
|
+
else:
|
|
192
|
+
target_values = [
|
|
193
|
+
x for xs in self.request.content["parameter"]["args"].values() for x in xs
|
|
194
|
+
]
|
|
195
|
+
if self.request.content["parameter"]["kwargs"].get("relative"):
|
|
196
|
+
target_values = np.asarray(target_values) + np.asarray(start_values)
|
|
197
|
+
|
|
198
|
+
with DeviceProgressBar(
|
|
199
|
+
self.devices, start_values=start_values, target_values=target_values
|
|
200
|
+
) as progress:
|
|
201
|
+
|
|
202
|
+
while not progress.finished or not data_source.done():
|
|
203
|
+
check_alarms(self.bec)
|
|
204
|
+
|
|
205
|
+
values = data_source.get_device_values()
|
|
206
|
+
progress.update(values=values)
|
|
207
|
+
self._print_client_msgs_asap()
|
|
208
|
+
|
|
209
|
+
for dev, (done, success) in data_source.device_states().items():
|
|
210
|
+
if done and success:
|
|
211
|
+
progress.set_finished(dev)
|
|
212
|
+
# pylint: disable=protected-access
|
|
213
|
+
progress._progress.refresh()
|
|
214
|
+
self._print_client_msgs_all()
|
|
215
|
+
|
|
216
|
+
def run(self):
|
|
217
|
+
"""run the progressbar."""
|
|
218
|
+
self.core()
|