lima2.client 2.0rc2__tar.gz → 2.1.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.
- {lima2_client-2.0rc2/src/lima2.client.egg-info → lima2_client-2.1.0}/PKG-INFO +2 -1
- {lima2_client-2.0rc2 → lima2_client-2.1.0}/setup.cfg +2 -1
- {lima2_client-2.0rc2 → lima2_client-2.1.0}/src/lima2/client/client.py +79 -16
- lima2_client-2.1.0/src/lima2/client/convert.py +36 -0
- {lima2_client-2.0rc2 → lima2_client-2.1.0}/src/lima2/client/detector.py +37 -53
- {lima2_client-2.0rc2 → lima2_client-2.1.0}/src/lima2/client/devencoded/dense_frame.py +4 -8
- {lima2_client-2.0rc2 → lima2_client-2.1.0}/src/lima2/client/devencoded/sparse_frame.py +1 -1
- {lima2_client-2.0rc2 → lima2_client-2.1.0}/src/lima2/client/pipeline.py +88 -15
- {lima2_client-2.0rc2 → lima2_client-2.1.0}/src/lima2/client/shell.py +4 -2
- {lima2_client-2.0rc2 → lima2_client-2.1.0}/src/lima2/client/state_machine.py +53 -8
- {lima2_client-2.0rc2 → lima2_client-2.1.0}/src/lima2/client/utils.py +30 -0
- lima2_client-2.1.0/src/lima2/master_file/generate.py +222 -0
- {lima2_client-2.0rc2 → lima2_client-2.1.0}/src/lima2/server/receiver.py +4 -0
- {lima2_client-2.0rc2 → lima2_client-2.1.0/src/lima2.client.egg-info}/PKG-INFO +2 -1
- {lima2_client-2.0rc2 → lima2_client-2.1.0}/src/lima2.client.egg-info/SOURCES.txt +4 -3
- {lima2_client-2.0rc2 → lima2_client-2.1.0}/src/lima2.client.egg-info/requires.txt +1 -0
- {lima2_client-2.0rc2 → lima2_client-2.1.0}/tests/test_client.py +118 -0
- lima2_client-2.1.0/tests/test_convert.py +39 -0
- lima2_client-2.1.0/tests/test_dense_frame.py +155 -0
- {lima2_client-2.0rc2 → lima2_client-2.1.0}/tests/test_detector.py +1 -0
- {lima2_client-2.0rc2 → lima2_client-2.1.0}/tests/test_pipeline.py +152 -0
- {lima2_client-2.0rc2 → lima2_client-2.1.0}/tests/test_progress_counter.py +5 -0
- lima2_client-2.1.0/tests/test_sparse_frame.py +109 -0
- {lima2_client-2.0rc2 → lima2_client-2.1.0}/tests/test_state_machine.py +19 -3
- {lima2_client-2.0rc2 → lima2_client-2.1.0}/tests/test_timer.py +2 -0
- {lima2_client-2.0rc2 → lima2_client-2.1.0}/tests/test_topology.py +5 -0
- lima2_client-2.1.0/tests/test_utils.py +62 -0
- lima2_client-2.0rc2/src/lima2/client/channel.py +0 -30
- lima2_client-2.0rc2/src/lima2/client/convert.py +0 -46
- lima2_client-2.0rc2/src/lima2/client/devencoded/sparse_frame_with_background.py +0 -120
- lima2_client-2.0rc2/tests/test_import.py +0 -10
- lima2_client-2.0rc2/tests/test_utils.py +0 -30
- {lima2_client-2.0rc2 → lima2_client-2.1.0}/LICENSE +0 -0
- {lima2_client-2.0rc2 → lima2_client-2.1.0}/README.md +0 -0
- {lima2_client-2.0rc2 → lima2_client-2.1.0}/pyproject.toml +0 -0
- {lima2_client-2.0rc2 → lima2_client-2.1.0}/src/lima2/client/__init__.py +0 -0
- {lima2_client-2.0rc2 → lima2_client-2.1.0}/src/lima2/client/devencoded/__init__.py +0 -0
- {lima2_client-2.0rc2 → lima2_client-2.1.0}/src/lima2/client/devencoded/exception.py +0 -0
- {lima2_client-2.0rc2 → lima2_client-2.1.0}/src/lima2/client/devencoded/structured_array.py +0 -0
- {lima2_client-2.0rc2 → lima2_client-2.1.0}/src/lima2/client/pipelines/__init__.py +0 -0
- {lima2_client-2.0rc2 → lima2_client-2.1.0}/src/lima2/client/pipelines/legacy.py +0 -0
- {lima2_client-2.0rc2 → lima2_client-2.1.0}/src/lima2/client/pipelines/smx.py +0 -0
- {lima2_client-2.0rc2 → lima2_client-2.1.0}/src/lima2/client/pipelines/xpcs.py +0 -0
- {lima2_client-2.0rc2 → lima2_client-2.1.0}/src/lima2/client/progress_counter.py +0 -0
- {lima2_client-2.0rc2 → lima2_client-2.1.0}/src/lima2/client/py.typed +0 -0
- {lima2_client-2.0rc2 → lima2_client-2.1.0}/src/lima2/client/topology.py +0 -0
- {lima2_client-2.0rc2 → lima2_client-2.1.0}/src/lima2/server/__init__.py +0 -0
- {lima2_client-2.0rc2 → lima2_client-2.1.0}/src/lima2/server/control.py +0 -0
- {lima2_client-2.0rc2 → lima2_client-2.1.0}/src/lima2/server/timer.py +0 -0
- {lima2_client-2.0rc2 → lima2_client-2.1.0}/src/lima2.client.egg-info/dependency_links.txt +0 -0
- {lima2_client-2.0rc2 → lima2_client-2.1.0}/src/lima2.client.egg-info/entry_points.txt +0 -0
- {lima2_client-2.0rc2 → lima2_client-2.1.0}/src/lima2.client.egg-info/top_level.txt +0 -0
- {lima2_client-2.0rc2 → lima2_client-2.1.0}/tests/test_pipelines.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: lima2.client
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: Python client library for Lima2 detector
|
|
5
5
|
Home-page: https://gitlab.esrf.fr/limagroup/lima2-client
|
|
6
6
|
Author: BCU (ESRF)
|
|
@@ -19,6 +19,7 @@ Requires-Dist: beartype
|
|
|
19
19
|
Requires-Dist: numpy
|
|
20
20
|
Requires-Dist: pyyaml
|
|
21
21
|
Requires-Dist: typing-extensions
|
|
22
|
+
Requires-Dist: h5py
|
|
22
23
|
Provides-Extra: shell
|
|
23
24
|
Requires-Dist: ipython; extra == "shell"
|
|
24
25
|
|
|
@@ -22,6 +22,7 @@ install_requires =
|
|
|
22
22
|
numpy
|
|
23
23
|
pyyaml
|
|
24
24
|
typing-extensions
|
|
25
|
+
h5py
|
|
25
26
|
|
|
26
27
|
[options.entry_points]
|
|
27
28
|
console_scripts =
|
|
@@ -40,7 +41,7 @@ per-file-ignores =
|
|
|
40
41
|
required_plugins =
|
|
41
42
|
pytest-cov>=6.0.0
|
|
42
43
|
pytest-forked>=1.6.0
|
|
43
|
-
addopts = -v
|
|
44
|
+
addopts = -v
|
|
44
45
|
pythonpath = src
|
|
45
46
|
|
|
46
47
|
[coverage:run]
|
|
@@ -18,7 +18,10 @@ individual receivers, for instance if one of them is stuck on a frame while the
|
|
|
18
18
|
processing data.
|
|
19
19
|
"""
|
|
20
20
|
|
|
21
|
+
import logging
|
|
21
22
|
import os
|
|
23
|
+
import traceback
|
|
24
|
+
from concurrent import futures
|
|
22
25
|
from uuid import UUID
|
|
23
26
|
|
|
24
27
|
import tango as tg
|
|
@@ -33,6 +36,8 @@ from lima2.client.progress_counter import ProgressCounter, SingleCounter
|
|
|
33
36
|
from lima2.client.topology import TopologyKind
|
|
34
37
|
from lima2.client.utils import docstring_from
|
|
35
38
|
|
|
39
|
+
logger = logging.getLogger(__name__)
|
|
40
|
+
|
|
36
41
|
|
|
37
42
|
@beartype
|
|
38
43
|
class Client:
|
|
@@ -67,12 +72,10 @@ class Client:
|
|
|
67
72
|
f"No matching topology type for {repr(topology_name)}"
|
|
68
73
|
) from e
|
|
69
74
|
|
|
70
|
-
self.recv_devs = rcv_devs
|
|
71
|
-
|
|
72
75
|
# Cache of processing pipelines by uuid (see method `pipeline`)
|
|
73
76
|
self.__pipelines: dict[UUID, Pipeline] = {}
|
|
74
77
|
|
|
75
|
-
self.
|
|
78
|
+
self._tango_db = tg.Database()
|
|
76
79
|
|
|
77
80
|
@classmethod
|
|
78
81
|
def from_yaml(
|
|
@@ -91,6 +94,16 @@ class Client:
|
|
|
91
94
|
|
|
92
95
|
return Client(ctl_dev=ctl_dev, rcv_devs=rcv_devs)
|
|
93
96
|
|
|
97
|
+
@property
|
|
98
|
+
def control(self) -> tg.DeviceProxy:
|
|
99
|
+
"""Lima2 control tango device."""
|
|
100
|
+
return self.detector.ctrl
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def receivers(self) -> list[tg.DeviceProxy]:
|
|
104
|
+
"""Lima2 receiver tango devices."""
|
|
105
|
+
return self.detector.recvs
|
|
106
|
+
|
|
94
107
|
@property
|
|
95
108
|
def detector(self) -> Detector:
|
|
96
109
|
return self._detector
|
|
@@ -124,6 +137,11 @@ class Client:
|
|
|
124
137
|
def params_default(self) -> dict[str, Any]:
|
|
125
138
|
return self.detector.params_default
|
|
126
139
|
|
|
140
|
+
@property
|
|
141
|
+
@docstring_from(Detector.params_schema)
|
|
142
|
+
def params_schema(self) -> dict[str, Any]:
|
|
143
|
+
return self.detector.params_schema
|
|
144
|
+
|
|
127
145
|
####################################################################################
|
|
128
146
|
# Control
|
|
129
147
|
####################################################################################
|
|
@@ -138,7 +156,7 @@ class Client:
|
|
|
138
156
|
ctl_params=ctrl_params,
|
|
139
157
|
acq_params=acq_params,
|
|
140
158
|
proc_params=proc_params,
|
|
141
|
-
num_receivers=len(self.
|
|
159
|
+
num_receivers=len(self.receivers),
|
|
142
160
|
)
|
|
143
161
|
|
|
144
162
|
self.detector.prepare_acq(uuid, ctl, acq, proc)
|
|
@@ -170,7 +188,7 @@ class Client:
|
|
|
170
188
|
SingleCounter(
|
|
171
189
|
name="nb_frames_acquired",
|
|
172
190
|
value=self.detector.nb_frames_acquired,
|
|
173
|
-
source=self.
|
|
191
|
+
source=self.control.name(),
|
|
174
192
|
)
|
|
175
193
|
)
|
|
176
194
|
|
|
@@ -184,7 +202,7 @@ class Client:
|
|
|
184
202
|
value=recv.nb_frames_xferred,
|
|
185
203
|
source=recv.name(),
|
|
186
204
|
)
|
|
187
|
-
for recv in self.
|
|
205
|
+
for recv in self.receivers
|
|
188
206
|
]
|
|
189
207
|
)
|
|
190
208
|
|
|
@@ -195,7 +213,7 @@ class Client:
|
|
|
195
213
|
@property
|
|
196
214
|
def pipelines(self) -> list[UUID]:
|
|
197
215
|
"""Get list of available pipelines UUID"""
|
|
198
|
-
uuids = [recv.pipelines for recv in self.
|
|
216
|
+
uuids = [recv.pipelines for recv in self.receivers]
|
|
199
217
|
if all(uuid is None for uuid in uuids):
|
|
200
218
|
return []
|
|
201
219
|
return [UUID(u) for u in {y for x in uuids for y in x}]
|
|
@@ -209,12 +227,13 @@ class Client:
|
|
|
209
227
|
return self.__pipelines[uuid]
|
|
210
228
|
|
|
211
229
|
# uuid isn't in the local cache of pipelines: get it from Tango
|
|
212
|
-
instances = self.
|
|
230
|
+
instances = self._tango_db.get_device_exported(f"*/limaprocessing/{uuid}*")
|
|
213
231
|
if not instances:
|
|
214
232
|
raise ValueError(f"Pipeline not found in tango database: {uuid=}")
|
|
215
233
|
|
|
216
234
|
class_names = [
|
|
217
|
-
self.
|
|
235
|
+
self._tango_db.get_device_info(instance).class_name
|
|
236
|
+
for instance in instances
|
|
218
237
|
]
|
|
219
238
|
|
|
220
239
|
# Select the pipeline according to the Tango class (Homogeneous processing for now)
|
|
@@ -237,7 +256,7 @@ class Client:
|
|
|
237
256
|
Raises if no pipeline has been created, or if receivers disagree on the
|
|
238
257
|
current pipeline's uuid.
|
|
239
258
|
"""
|
|
240
|
-
uuids: list[str] = [recv.current_pipeline for recv in self.
|
|
259
|
+
uuids: list[str] = [recv.current_pipeline for recv in self.receivers]
|
|
241
260
|
|
|
242
261
|
if all([uuid == "" for uuid in uuids]):
|
|
243
262
|
raise ValueError("No pipeline present in the tango database")
|
|
@@ -250,10 +269,54 @@ class Client:
|
|
|
250
269
|
return self.pipeline(uuids[0])
|
|
251
270
|
|
|
252
271
|
def erase_pipeline(self, uuid: UUID) -> None:
|
|
253
|
-
"""Erase a pipeline instance
|
|
254
|
-
|
|
255
|
-
|
|
272
|
+
"""Erase a pipeline instance.
|
|
273
|
+
|
|
274
|
+
Raises if the pipeline is absent from a receiver.
|
|
275
|
+
"""
|
|
276
|
+
for recv in self.receivers:
|
|
256
277
|
recv.erasePipeline(str(uuid))
|
|
278
|
+
self.__pipelines.pop(uuid, None)
|
|
279
|
+
|
|
280
|
+
def clear_previous_pipelines(self) -> None:
|
|
281
|
+
"""Erase all pipelines except the current one."""
|
|
282
|
+
try:
|
|
283
|
+
current = self.current_pipeline.uuid
|
|
284
|
+
except ValueError:
|
|
285
|
+
current = None
|
|
286
|
+
|
|
287
|
+
def clear(receiver: tg.DeviceProxy):
|
|
288
|
+
"""Clear previous pipelines from a single receiver device."""
|
|
289
|
+
if receiver.pipelines is None:
|
|
290
|
+
# No pipelines
|
|
291
|
+
return []
|
|
292
|
+
|
|
293
|
+
cleared = []
|
|
294
|
+
for uuid in receiver.pipelines:
|
|
295
|
+
if uuid != str(current):
|
|
296
|
+
logger.debug(
|
|
297
|
+
f"Erasing pipeline {uuid} from recv {receiver.dev_name()}"
|
|
298
|
+
)
|
|
299
|
+
receiver.erasePipeline(uuid)
|
|
300
|
+
cleared.append(uuid)
|
|
301
|
+
return cleared
|
|
302
|
+
|
|
303
|
+
cleared: set[str] = set()
|
|
304
|
+
with futures.ThreadPoolExecutor(max_workers=len(self.receivers)) as executor:
|
|
305
|
+
fs = [executor.submit(clear, receiver) for receiver in self.receivers]
|
|
306
|
+
for receiver, future in zip(self.receivers, fs):
|
|
307
|
+
try:
|
|
308
|
+
cleared = cleared.union(future.result())
|
|
309
|
+
except Exception:
|
|
310
|
+
logger.warning(
|
|
311
|
+
f"Exception while clearing pipelines from {receiver.dev_name()}: "
|
|
312
|
+
f"{traceback.format_exc()}"
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
logger.debug(f"Cleared pipelines {[uuid for uuid in set(cleared)]}")
|
|
316
|
+
|
|
317
|
+
# Reset local cache
|
|
318
|
+
if current is not None:
|
|
319
|
+
self.__pipelines = {current: self.__pipelines[current]}
|
|
257
320
|
|
|
258
321
|
####################################################################################
|
|
259
322
|
# Utility
|
|
@@ -264,10 +327,10 @@ class Client:
|
|
|
264
327
|
[
|
|
265
328
|
"Lima2 client",
|
|
266
329
|
f"State: {self.detector.state}",
|
|
267
|
-
"
|
|
268
|
-
f" {self.
|
|
330
|
+
"Control:",
|
|
331
|
+
f" {self.control}",
|
|
269
332
|
f"Receivers ({self.topology_kind}):",
|
|
270
|
-
*[f" - {recv}" for recv in self.
|
|
333
|
+
*[f" - {recv}" for recv in self.receivers],
|
|
271
334
|
"Progress counters:",
|
|
272
335
|
f" - {self.nb_frames_acquired}",
|
|
273
336
|
f" - {self.nb_frames_xferred}",
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# This file is part of the Lima2 project
|
|
4
|
+
#
|
|
5
|
+
# Copyright (c) 2020-2024 Beamline Control Unit, ESRF
|
|
6
|
+
# Distributed under the MIT license. See LICENSE for more info.
|
|
7
|
+
|
|
8
|
+
"""Convertion routines.
|
|
9
|
+
|
|
10
|
+
Frame info is serialized as {'dimensions': {'x': 2068, 'y': 512}, 'nb_channels': 2, 'pixel_type': 'gray16'}
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
# convert from pixel enum type to numpy type
|
|
16
|
+
pixel_type_to_np_dtype = {
|
|
17
|
+
"gray8s": np.int8,
|
|
18
|
+
"gray8": np.uint8,
|
|
19
|
+
"gray16s": np.int16,
|
|
20
|
+
"gray16": np.uint16,
|
|
21
|
+
"gray32s": np.int32,
|
|
22
|
+
"gray32": np.uint32,
|
|
23
|
+
"gray32f": np.float32,
|
|
24
|
+
"gray64f": np.float64,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def frame_info_to_shape_dtype(frame_info):
|
|
29
|
+
return dict(
|
|
30
|
+
shape=(
|
|
31
|
+
frame_info["nb_channels"],
|
|
32
|
+
frame_info["dimensions"]["y"],
|
|
33
|
+
frame_info["dimensions"]["x"],
|
|
34
|
+
),
|
|
35
|
+
dtype=pixel_type_to_np_dtype[frame_info["pixel_type"]],
|
|
36
|
+
)
|
|
@@ -24,13 +24,15 @@ import json
|
|
|
24
24
|
import logging
|
|
25
25
|
import uuid
|
|
26
26
|
|
|
27
|
-
import jsonschema
|
|
28
27
|
import tango
|
|
29
28
|
from beartype import beartype
|
|
30
29
|
from jsonschema_default import create_from
|
|
30
|
+
from typing_extensions import Optional
|
|
31
31
|
|
|
32
|
+
from lima2.client import pipelines
|
|
32
33
|
from lima2.client import state_machine as fsm
|
|
33
34
|
from lima2.client.state_machine import State
|
|
35
|
+
from lima2.client.utils import validate
|
|
34
36
|
|
|
35
37
|
# Create a logger
|
|
36
38
|
_logger = logging.getLogger(__name__)
|
|
@@ -45,7 +47,7 @@ class CommError(RuntimeError):
|
|
|
45
47
|
class Detector:
|
|
46
48
|
"""The main Detector interface."""
|
|
47
49
|
|
|
48
|
-
DEFAULT_TANGO_TIMEOUT =
|
|
50
|
+
DEFAULT_TANGO_TIMEOUT = 5
|
|
49
51
|
|
|
50
52
|
def __init__(
|
|
51
53
|
self,
|
|
@@ -68,24 +70,19 @@ class Detector:
|
|
|
68
70
|
# Preconditions
|
|
69
71
|
if not recv_devs:
|
|
70
72
|
raise ValueError("Must provide at least one receiver")
|
|
71
|
-
elif len(recv_devs) > 1:
|
|
72
|
-
rank_order = [d.recv_rank for d in recv_devs]
|
|
73
|
-
if not all([r == i for i, r in enumerate(rank_order)]):
|
|
74
|
-
raise ValueError(
|
|
75
|
-
f"Receiver MPI rank order does not match client order: {rank_order}"
|
|
76
|
-
)
|
|
77
73
|
|
|
78
74
|
self.__ctrl = ctrl_dev
|
|
79
75
|
self.__recvs = list(recv_devs)
|
|
80
76
|
|
|
81
|
-
|
|
82
|
-
self.ping()
|
|
83
|
-
except tango.DevFailed as e:
|
|
84
|
-
raise CommError(
|
|
85
|
-
"Failed to ping Tango servers (are the devices running?)"
|
|
86
|
-
) from e
|
|
77
|
+
self.ping()
|
|
87
78
|
|
|
88
|
-
|
|
79
|
+
rank_order = [d.recv_rank for d in recv_devs]
|
|
80
|
+
if not all([r == i for i, r in enumerate(rank_order)]):
|
|
81
|
+
raise ValueError(
|
|
82
|
+
f"Receiver MPI rank order does not match client order: {rank_order}"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
self.__params_schema: Optional[dict] = None
|
|
89
86
|
self.__fsm = fsm.StateMachine(self.__ctrl, self.__recvs)
|
|
90
87
|
self.__try_sync_hard()
|
|
91
88
|
|
|
@@ -100,8 +97,6 @@ class Detector:
|
|
|
100
97
|
d.set_green_mode(tango.GreenMode.Gevent)
|
|
101
98
|
d.set_timeout_millis(timeout * 1000)
|
|
102
99
|
|
|
103
|
-
self.__try_params_schema()
|
|
104
|
-
|
|
105
100
|
@property
|
|
106
101
|
def det_info(self) -> dict:
|
|
107
102
|
"""
|
|
@@ -166,26 +161,19 @@ class Detector:
|
|
|
166
161
|
"""
|
|
167
162
|
return json.loads(self.__ctrl.det_capabilities) # type: ignore
|
|
168
163
|
|
|
169
|
-
def __try_params_schema(self):
|
|
170
|
-
"""
|
|
171
|
-
Try to init the parameters from a lazy constructed device Proxy
|
|
172
|
-
(to be used when the DS is not running when Detector is constructed)
|
|
173
|
-
"""
|
|
174
|
-
try:
|
|
175
|
-
if not self.__params_schema:
|
|
176
|
-
self.__params_schema = self.__get_params_schema()
|
|
177
|
-
return True
|
|
178
|
-
except (tango.DevFailed, RuntimeError) as e:
|
|
179
|
-
print(f"Failed to get params schema: {e}")
|
|
180
|
-
return False
|
|
181
|
-
|
|
182
164
|
def __get_params_schema(self) -> dict:
|
|
183
165
|
"""
|
|
184
166
|
Get the parameters's schema for each devices from the Tango class attribute
|
|
167
|
+
|
|
168
|
+
Raises tango.DevFailed if we cannot connect to database at TANGO_HOST.
|
|
169
|
+
|
|
170
|
+
If the lima2 server hasn't been started yet, returns a dict of the form:
|
|
171
|
+
{
|
|
172
|
+
d.dev_name(): {"init_params": None, "acq_params": None}
|
|
173
|
+
for d in self.devs
|
|
174
|
+
}
|
|
185
175
|
"""
|
|
186
176
|
tango_db = tango.Database()
|
|
187
|
-
if tango_db is None:
|
|
188
|
-
return {}
|
|
189
177
|
|
|
190
178
|
res = {}
|
|
191
179
|
for d in self.devs:
|
|
@@ -207,7 +195,7 @@ class Detector:
|
|
|
207
195
|
return res
|
|
208
196
|
|
|
209
197
|
@property
|
|
210
|
-
def params_schema(self):
|
|
198
|
+
def params_schema(self) -> dict:
|
|
211
199
|
"""Returns JSON schemas of init, acquisition and processing parameters.
|
|
212
200
|
|
|
213
201
|
Example:
|
|
@@ -235,10 +223,9 @@ class Detector:
|
|
|
235
223
|
Group:
|
|
236
224
|
Parameters
|
|
237
225
|
"""
|
|
238
|
-
if self.__params_schema:
|
|
239
|
-
|
|
226
|
+
if self.__params_schema is None:
|
|
227
|
+
self.__params_schema = self.__get_params_schema()
|
|
240
228
|
|
|
241
|
-
self.__try_params_schema()
|
|
242
229
|
return self.__params_schema
|
|
243
230
|
|
|
244
231
|
@property
|
|
@@ -281,13 +268,16 @@ class Detector:
|
|
|
281
268
|
Ping all the devices of the system.
|
|
282
269
|
|
|
283
270
|
Raises:
|
|
284
|
-
|
|
271
|
+
CommError if the connection failed.
|
|
285
272
|
|
|
286
273
|
Group:
|
|
287
274
|
Status
|
|
288
275
|
"""
|
|
289
276
|
for d in self.devs:
|
|
290
|
-
|
|
277
|
+
try:
|
|
278
|
+
d.ping()
|
|
279
|
+
except tango.DevFailed as e:
|
|
280
|
+
raise CommError(f"Failed to ping device {d.dev_name()}") from e
|
|
291
281
|
|
|
292
282
|
@property
|
|
293
283
|
def devs(self) -> list[tango.DeviceProxy]:
|
|
@@ -347,18 +337,11 @@ class Detector:
|
|
|
347
337
|
Group:
|
|
348
338
|
State
|
|
349
339
|
"""
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
map = {
|
|
356
|
-
ServerState.idle: State.IDLE,
|
|
357
|
-
ServerState.prepared: State.PREPARED,
|
|
358
|
-
ServerState.running: State.RUNNING,
|
|
359
|
-
ServerState.stopped: State.IDLE,
|
|
360
|
-
}
|
|
361
|
-
self.__fsm.state = map[server_state]
|
|
340
|
+
try:
|
|
341
|
+
self.__fsm.sync()
|
|
342
|
+
except Exception as e:
|
|
343
|
+
_logger.warning(f"Unable to sync with server: {e}")
|
|
344
|
+
|
|
362
345
|
return self.__fsm.state
|
|
363
346
|
|
|
364
347
|
@property
|
|
@@ -421,18 +404,19 @@ class Detector:
|
|
|
421
404
|
f"prepare_acq({uuid}, {ctrl_params}, {acq_params}, {proc_params})"
|
|
422
405
|
)
|
|
423
406
|
|
|
424
|
-
|
|
407
|
+
validate(
|
|
425
408
|
instance=ctrl_params,
|
|
426
409
|
schema=self.params_schema[self.ctrl.name()]["acq_params"],
|
|
427
410
|
)
|
|
428
411
|
|
|
429
412
|
for i, recv in enumerate(self.__recvs):
|
|
430
|
-
|
|
413
|
+
validate(
|
|
431
414
|
instance=acq_params[i],
|
|
432
415
|
schema=self.params_schema[recv.name()]["acq_params"],
|
|
433
416
|
)
|
|
434
417
|
|
|
435
|
-
|
|
418
|
+
pipeline_class = pipelines.get_class(proc_params[i]["class_name"])
|
|
419
|
+
validate(instance=proc_params[i], schema=pipeline_class.params_schema)
|
|
436
420
|
|
|
437
421
|
# Update parameters on the device servers
|
|
438
422
|
# TODO: Rename acq_params to ctrl_params
|
|
@@ -10,11 +10,13 @@
|
|
|
10
10
|
|
|
11
11
|
from dataclasses import dataclass
|
|
12
12
|
import struct
|
|
13
|
-
import numpy
|
|
14
13
|
import typing
|
|
15
14
|
from typing_extensions import Union
|
|
16
15
|
import enum
|
|
17
16
|
|
|
17
|
+
import numpy
|
|
18
|
+
from typing_extensions import Union
|
|
19
|
+
|
|
18
20
|
from .exception import DevEncodedFormatNotSupported
|
|
19
21
|
|
|
20
22
|
DATA_HEADER_FORMAT = "<IHHIIHHHHHHHHIIIIIIQ"
|
|
@@ -61,15 +63,9 @@ class Frame:
|
|
|
61
63
|
data: numpy.ndarray
|
|
62
64
|
"""Data of the frame"""
|
|
63
65
|
|
|
64
|
-
idx:
|
|
66
|
+
idx: int
|
|
65
67
|
"""Index of the frame where 0 is the first frame"""
|
|
66
68
|
|
|
67
|
-
def __bool__(self):
|
|
68
|
-
return not self.isEmpty()
|
|
69
|
-
|
|
70
|
-
def isEmpty(self):
|
|
71
|
-
return self.data is None
|
|
72
|
-
|
|
73
69
|
|
|
74
70
|
def decode(raw_data: EncodedFrame) -> Frame:
|
|
75
71
|
"""Decode data provided by Lima device image attribute
|
|
@@ -14,6 +14,7 @@ frame index, and provide aggregated progress counters during/after an acquisitio
|
|
|
14
14
|
|
|
15
15
|
import json
|
|
16
16
|
import logging
|
|
17
|
+
import traceback
|
|
17
18
|
from collections.abc import Callable
|
|
18
19
|
from concurrent.futures import ThreadPoolExecutor
|
|
19
20
|
from dataclasses import dataclass
|
|
@@ -36,7 +37,6 @@ from lima2.client.topology import (
|
|
|
36
37
|
)
|
|
37
38
|
from lima2.client.utils import classproperty
|
|
38
39
|
|
|
39
|
-
# Create a logger
|
|
40
40
|
logger = logging.getLogger(__name__)
|
|
41
41
|
|
|
42
42
|
|
|
@@ -231,14 +231,29 @@ class Pipeline:
|
|
|
231
231
|
# The most naive way to get the latest frame is to try all receivers.
|
|
232
232
|
# For monitoring purposes (~1 Hz), this is OK, but would be very wasteful otherwise.
|
|
233
233
|
num_rcvs = len(self._devs)
|
|
234
|
+
|
|
235
|
+
def fetch_and_decode_last(rcv_idx):
|
|
236
|
+
"""Return the decoded last frame from receiver rcv_idx.
|
|
237
|
+
|
|
238
|
+
Returns None if the receiver hasn't yet processed any frames.
|
|
239
|
+
"""
|
|
240
|
+
try:
|
|
241
|
+
return frame_source.decoder(getters[rcv_idx](-1))
|
|
242
|
+
except tango.DevFailed:
|
|
243
|
+
return None
|
|
244
|
+
|
|
234
245
|
with ThreadPoolExecutor(max_workers=num_rcvs) as pool:
|
|
235
246
|
frames = list(
|
|
236
247
|
pool.map(
|
|
237
|
-
|
|
248
|
+
fetch_and_decode_last,
|
|
238
249
|
range(num_rcvs),
|
|
239
250
|
)
|
|
240
251
|
)
|
|
241
|
-
indices = [frame.idx for frame in frames]
|
|
252
|
+
indices = [frame.idx for frame in frames if frame is not None]
|
|
253
|
+
if len(indices) == 0:
|
|
254
|
+
raise ValueError(
|
|
255
|
+
"Cannot get the latest frame: no frames available from any receiver"
|
|
256
|
+
)
|
|
242
257
|
latest_idx = indices.index(max(indices))
|
|
243
258
|
|
|
244
259
|
logger.debug(
|
|
@@ -505,22 +520,58 @@ class Pipeline:
|
|
|
505
520
|
"""A list of `is_finished` for each devices."""
|
|
506
521
|
return [dev.is_finished for dev in self._devs]
|
|
507
522
|
|
|
508
|
-
def register_on_finished(self,
|
|
523
|
+
def register_on_finished(self, on_finished: Callable[[], None]) -> None:
|
|
509
524
|
"""
|
|
510
|
-
Register a callback function to be notified
|
|
525
|
+
Register a callback function to be notified when all pipelines are finished.
|
|
511
526
|
|
|
512
527
|
Arg:
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
Returns:
|
|
516
|
-
A dict mapping the pipeline instance name with the event id
|
|
528
|
+
on_finished: A callback called when all receivers are done
|
|
529
|
+
processing frames.
|
|
517
530
|
"""
|
|
518
531
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
532
|
+
self.finished_devs: set[tango.DeviceProxy] = set()
|
|
533
|
+
"""Set of processing devices that have finished."""
|
|
534
|
+
|
|
535
|
+
event_ids: dict[tango.DeviceProxy, int] = {}
|
|
536
|
+
|
|
537
|
+
def device_finished_callback(evt: tango.DataReadyEventData):
|
|
538
|
+
nonlocal event_ids
|
|
539
|
+
|
|
540
|
+
if evt.err:
|
|
541
|
+
logger.error(
|
|
542
|
+
f"Error on {evt.device.dev_name()} 'is_finished' event:\n"
|
|
543
|
+
+ "\n".join([f"- {err.desc}" for err in evt.errors])
|
|
544
|
+
+ "\n"
|
|
545
|
+
)
|
|
546
|
+
for r, e in event_ids.items():
|
|
547
|
+
r.unsubscribe_event(e)
|
|
548
|
+
return
|
|
549
|
+
|
|
550
|
+
logger.debug(f"{evt.device.dev_name()} is_finished event received")
|
|
551
|
+
|
|
552
|
+
self.finished_devs.add(evt.device)
|
|
553
|
+
if self.finished_devs == {dev for dev in self._devs}:
|
|
554
|
+
# All done. Call on_finished and report any exceptions.
|
|
555
|
+
try:
|
|
556
|
+
on_finished()
|
|
557
|
+
except Exception:
|
|
558
|
+
logger.error(
|
|
559
|
+
"Exception raised in pipeline on_finished callback:\n"
|
|
560
|
+
f"{traceback.format_exc()}"
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
for dev, event_id in event_ids.items():
|
|
564
|
+
# Unsubscribe from the event to stop trying in case of error
|
|
565
|
+
logger.debug(f"Unsubscribing from {event_id} on {dev.dev_name()}")
|
|
566
|
+
dev.unsubscribe_event(event_id)
|
|
567
|
+
|
|
568
|
+
event_ids = {
|
|
569
|
+
dev: dev.subscribe_event(
|
|
570
|
+
"is_finished",
|
|
571
|
+
tango.EventType.DATA_READY_EVENT,
|
|
572
|
+
device_finished_callback,
|
|
522
573
|
)
|
|
523
|
-
for
|
|
574
|
+
for dev in self._devs
|
|
524
575
|
}
|
|
525
576
|
|
|
526
577
|
@property
|
|
@@ -539,9 +590,31 @@ class Pipeline:
|
|
|
539
590
|
A dict mapping the pipeline instance name with the event id
|
|
540
591
|
"""
|
|
541
592
|
|
|
542
|
-
|
|
593
|
+
event_ids: dict[tango.DeviceProxy, int] = {}
|
|
594
|
+
|
|
595
|
+
def on_error(evt: tango.DataReadyEventData):
|
|
596
|
+
nonlocal event_ids
|
|
597
|
+
|
|
598
|
+
if evt.err:
|
|
599
|
+
logger.error(
|
|
600
|
+
f"Error on {evt.device.dev_name()} 'last_error' event:\n"
|
|
601
|
+
+ "\n".join([f"- {err.desc}" for err in evt.errors])
|
|
602
|
+
+ "\n"
|
|
603
|
+
)
|
|
604
|
+
else:
|
|
605
|
+
try:
|
|
606
|
+
cbk(evt)
|
|
607
|
+
except Exception:
|
|
608
|
+
logger.error(
|
|
609
|
+
"Exception raised in pipeline on_error callback:\n"
|
|
610
|
+
f"{traceback.format_exc()}"
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
evt.device.unsubscribe_event(event_ids[evt.device])
|
|
614
|
+
|
|
615
|
+
event_ids = {
|
|
543
616
|
proc: proc.subscribe_event(
|
|
544
|
-
"last_error", tango.EventType.DATA_READY_EVENT,
|
|
617
|
+
"last_error", tango.EventType.DATA_READY_EVENT, on_error
|
|
545
618
|
)
|
|
546
619
|
for proc in self._devs
|
|
547
620
|
}
|
|
@@ -33,7 +33,6 @@ def get_proc_params(pipeline: Literal["Legacy", "Smx", "Xpcs"]) -> dict:
|
|
|
33
33
|
|
|
34
34
|
proc_class = pipelines.get_class(f"LimaProcessing{pipeline}")
|
|
35
35
|
proc_params: dict = proc_class.params_default
|
|
36
|
-
proc_params["class_name"] = proc_class.tango_class
|
|
37
36
|
|
|
38
37
|
return proc_params
|
|
39
38
|
|
|
@@ -64,6 +63,7 @@ def run_acquisition(
|
|
|
64
63
|
|
|
65
64
|
def main():
|
|
66
65
|
import copy
|
|
66
|
+
import logging
|
|
67
67
|
import os
|
|
68
68
|
|
|
69
69
|
import tango as tg
|
|
@@ -83,6 +83,8 @@ def main():
|
|
|
83
83
|
if not os.getenv("TANGO_HOST"):
|
|
84
84
|
raise ValueError("TANGO_HOST must be exported")
|
|
85
85
|
|
|
86
|
+
logging.basicConfig(level=logging.INFO)
|
|
87
|
+
|
|
86
88
|
#############
|
|
87
89
|
# Populate user namespace
|
|
88
90
|
|
|
@@ -103,7 +105,7 @@ def main():
|
|
|
103
105
|
proc_params = get_proc_params("Legacy")
|
|
104
106
|
proc_params["saving"]["file_exists_policy"] = "overwrite"
|
|
105
107
|
|
|
106
|
-
ctl_params = c.params_default[c.
|
|
108
|
+
ctl_params = c.params_default[c.control.name()]["acq_params"]
|
|
107
109
|
|
|
108
110
|
rcv_params = copy.deepcopy(ctl_params)
|
|
109
111
|
|