lima2.client 2.1.0__tar.gz → 2.1.2__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.1.0/src/lima2.client.egg-info → lima2_client-2.1.2}/PKG-INFO +1 -1
- lima2_client-2.1.2/src/lima2/client/devencoded/smx_sparse_frame.py +128 -0
- {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2/client/pipeline.py +43 -17
- {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2/client/pipelines/legacy.py +9 -7
- {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2/client/pipelines/smx.py +27 -14
- {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2/client/pipelines/xpcs.py +13 -11
- {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2/client/state_machine.py +39 -14
- {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2/client/topology.py +3 -2
- {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2/master_file/generate.py +54 -27
- {lima2_client-2.1.0 → lima2_client-2.1.2/src/lima2.client.egg-info}/PKG-INFO +1 -1
- {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2.client.egg-info/SOURCES.txt +1 -0
- {lima2_client-2.1.0 → lima2_client-2.1.2}/tests/test_pipeline.py +35 -5
- {lima2_client-2.1.0 → lima2_client-2.1.2}/LICENSE +0 -0
- {lima2_client-2.1.0 → lima2_client-2.1.2}/README.md +0 -0
- {lima2_client-2.1.0 → lima2_client-2.1.2}/pyproject.toml +0 -0
- {lima2_client-2.1.0 → lima2_client-2.1.2}/setup.cfg +0 -0
- {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2/client/__init__.py +0 -0
- {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2/client/client.py +0 -0
- {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2/client/convert.py +0 -0
- {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2/client/detector.py +0 -0
- {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2/client/devencoded/__init__.py +0 -0
- {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2/client/devencoded/dense_frame.py +0 -0
- {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2/client/devencoded/exception.py +0 -0
- {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2/client/devencoded/sparse_frame.py +0 -0
- {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2/client/devencoded/structured_array.py +0 -0
- {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2/client/pipelines/__init__.py +0 -0
- {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2/client/progress_counter.py +0 -0
- {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2/client/py.typed +0 -0
- {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2/client/shell.py +0 -0
- {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2/client/utils.py +0 -0
- {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2/server/__init__.py +0 -0
- {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2/server/control.py +0 -0
- {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2/server/receiver.py +0 -0
- {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2/server/timer.py +0 -0
- {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2.client.egg-info/dependency_links.txt +0 -0
- {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2.client.egg-info/entry_points.txt +0 -0
- {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2.client.egg-info/requires.txt +0 -0
- {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2.client.egg-info/top_level.txt +0 -0
- {lima2_client-2.1.0 → lima2_client-2.1.2}/tests/test_client.py +0 -0
- {lima2_client-2.1.0 → lima2_client-2.1.2}/tests/test_convert.py +0 -0
- {lima2_client-2.1.0 → lima2_client-2.1.2}/tests/test_dense_frame.py +0 -0
- {lima2_client-2.1.0 → lima2_client-2.1.2}/tests/test_detector.py +0 -0
- {lima2_client-2.1.0 → lima2_client-2.1.2}/tests/test_pipelines.py +0 -0
- {lima2_client-2.1.0 → lima2_client-2.1.2}/tests/test_progress_counter.py +0 -0
- {lima2_client-2.1.0 → lima2_client-2.1.2}/tests/test_sparse_frame.py +0 -0
- {lima2_client-2.1.0 → lima2_client-2.1.2}/tests/test_state_machine.py +0 -0
- {lima2_client-2.1.0 → lima2_client-2.1.2}/tests/test_timer.py +0 -0
- {lima2_client-2.1.0 → lima2_client-2.1.2}/tests/test_topology.py +0 -0
- {lima2_client-2.1.0 → lima2_client-2.1.2}/tests/test_utils.py +0 -0
|
@@ -0,0 +1,128 @@
|
|
|
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
|
+
"""Utility functions to decode Lima data from device server.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import struct
|
|
12
|
+
import numpy as np
|
|
13
|
+
import typing
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
|
|
16
|
+
from .exception import DevEncodedFormatNotSupported
|
|
17
|
+
|
|
18
|
+
DATA_HEADER_FORMAT = "<HHIII"
|
|
19
|
+
DATA_HEADER_SIZE = struct.calcsize(DATA_HEADER_FORMAT)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class SmxSparseFrame:
|
|
24
|
+
"""
|
|
25
|
+
Provide sparse data frame from Lima including few metadata
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
index: np.ndarray
|
|
29
|
+
intensity: np.ndarray
|
|
30
|
+
background_avg: np.ndarray
|
|
31
|
+
background_std: np.ndarray
|
|
32
|
+
|
|
33
|
+
idx: typing.Optional[int]
|
|
34
|
+
"""Index of the frame where 0 is the first frame"""
|
|
35
|
+
|
|
36
|
+
shape: tuple[int, ...]
|
|
37
|
+
"""Shape of the frame"""
|
|
38
|
+
|
|
39
|
+
def __bool__(self):
|
|
40
|
+
return not self.isEmpty()
|
|
41
|
+
|
|
42
|
+
def isEmpty(self):
|
|
43
|
+
return self.index is None
|
|
44
|
+
|
|
45
|
+
def densify(self, mask, radius, dummy, normalization=None):
|
|
46
|
+
"""Generate a dense image of its sparse representation with background
|
|
47
|
+
|
|
48
|
+
:param mask: 2D array with NaNs for mask and pixel radius for the valid pixels
|
|
49
|
+
:param radius: 1D array with the radial distance
|
|
50
|
+
:param dummy: numerical value for masked-out pixels in dense image
|
|
51
|
+
:param normalization: flat*solidangle*polarization*... array
|
|
52
|
+
:return dense array
|
|
53
|
+
"""
|
|
54
|
+
dense = np.interp(mask, radius, self.background_avg)
|
|
55
|
+
if self.background_std is not None:
|
|
56
|
+
std = np.interp(mask, radius, self.background_std)
|
|
57
|
+
np.maximum(0.0, np.random.normal(dense, std), out=dense)
|
|
58
|
+
if normalization is not None:
|
|
59
|
+
dense *= normalization
|
|
60
|
+
|
|
61
|
+
flat = dense.ravel()
|
|
62
|
+
flat[self.index] = self.intensity
|
|
63
|
+
dtype = self.intensity.dtype
|
|
64
|
+
if np.issubdtype(dtype, np.integer):
|
|
65
|
+
dense = np.round(dense)
|
|
66
|
+
dense = np.ascontiguousarray(dense, dtype=dtype)
|
|
67
|
+
dense[np.logical_not(np.isfinite(mask))] = dummy
|
|
68
|
+
return dense
|
|
69
|
+
|
|
70
|
+
def densify_peaks(self):
|
|
71
|
+
"""Generate a dense image of its sparse representation with peaks only
|
|
72
|
+
|
|
73
|
+
:param mask: 2D array with NaNs for mask and pixel radius for the valid pixels
|
|
74
|
+
:return dense array
|
|
75
|
+
"""
|
|
76
|
+
dense = np.zeros(self.shape, self.intensity.dtype)
|
|
77
|
+
flat = dense.ravel()
|
|
78
|
+
flat[self.index] = self.intensity
|
|
79
|
+
return dense
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def data(self):
|
|
83
|
+
return self.densify_peaks()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def decode_sparse_frame(raw_data: bytes) -> SmxSparseFrame:
|
|
87
|
+
"""Decode data provided by Lima2
|
|
88
|
+
|
|
89
|
+
Argument:
|
|
90
|
+
raw_data: DevEncoded data
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
A SmxSparseFrame.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
if isinstance(raw_data, tuple):
|
|
97
|
+
# Support the direct output from proxy.readImage
|
|
98
|
+
if raw_data[0] != "SMX_SPARSE_FRAME":
|
|
99
|
+
raise DevEncodedFormatNotSupported(
|
|
100
|
+
"Data type SMX_SPARSE_FRAME expected (found %s)." % raw_data[0]
|
|
101
|
+
)
|
|
102
|
+
raw_data = raw_data[1]
|
|
103
|
+
|
|
104
|
+
(
|
|
105
|
+
width,
|
|
106
|
+
height,
|
|
107
|
+
nb_bins,
|
|
108
|
+
nb_peaks,
|
|
109
|
+
frame_idx,
|
|
110
|
+
) = struct.unpack_from(DATA_HEADER_FORMAT, raw_data)
|
|
111
|
+
|
|
112
|
+
offset = DATA_HEADER_SIZE
|
|
113
|
+
index = np.frombuffer(raw_data, count=nb_peaks, offset=offset, dtype=np.int32)
|
|
114
|
+
offset += index.nbytes
|
|
115
|
+
intensity = np.frombuffer(raw_data, count=nb_peaks, offset=offset, dtype=np.float32)
|
|
116
|
+
offset += intensity.nbytes
|
|
117
|
+
background_avg = np.frombuffer(
|
|
118
|
+
raw_data, count=nb_bins, offset=offset, dtype=np.float32
|
|
119
|
+
)
|
|
120
|
+
offset += background_avg.nbytes
|
|
121
|
+
background_std = np.frombuffer(
|
|
122
|
+
raw_data, count=nb_bins, offset=offset, dtype=np.float32
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
shape = (1, height, width)
|
|
126
|
+
return SmxSparseFrame(
|
|
127
|
+
index, intensity, background_avg, background_std, frame_idx, shape
|
|
128
|
+
)
|
|
@@ -18,15 +18,22 @@ import traceback
|
|
|
18
18
|
from collections.abc import Callable
|
|
19
19
|
from concurrent.futures import ThreadPoolExecutor
|
|
20
20
|
from dataclasses import dataclass
|
|
21
|
+
from enum import Enum, auto
|
|
21
22
|
from uuid import UUID
|
|
22
23
|
|
|
23
24
|
import numpy as np
|
|
24
25
|
import tango
|
|
25
26
|
from jsonschema_default import create_from
|
|
27
|
+
from typing_extensions import Optional
|
|
26
28
|
|
|
27
29
|
from lima2.client import progress_counter, utils
|
|
28
30
|
from lima2.client.convert import frame_info_to_shape_dtype
|
|
29
|
-
from lima2.client.devencoded import
|
|
31
|
+
from lima2.client.devencoded import (
|
|
32
|
+
dense_frame,
|
|
33
|
+
sparse_frame,
|
|
34
|
+
smx_sparse_frame,
|
|
35
|
+
structured_array,
|
|
36
|
+
)
|
|
30
37
|
from lima2.client.progress_counter import ProgressCounter, SingleCounter
|
|
31
38
|
from lima2.client.topology import (
|
|
32
39
|
RoundRobinTopology,
|
|
@@ -40,29 +47,46 @@ from lima2.client.utils import classproperty
|
|
|
40
47
|
logger = logging.getLogger(__name__)
|
|
41
48
|
|
|
42
49
|
|
|
50
|
+
class FrameType(Enum):
|
|
51
|
+
DENSE = auto()
|
|
52
|
+
SPARSE = auto()
|
|
53
|
+
SMX_SPARSE = auto()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
decoder_by_type: dict[FrameType, Callable] = {
|
|
57
|
+
FrameType.DENSE: dense_frame.decode,
|
|
58
|
+
FrameType.SPARSE: sparse_frame.decode,
|
|
59
|
+
FrameType.SMX_SPARSE: smx_sparse_frame.decode_sparse_frame,
|
|
60
|
+
}
|
|
61
|
+
"""Mapping from frame type to associated decode function"""
|
|
62
|
+
|
|
63
|
+
|
|
43
64
|
@dataclass
|
|
44
65
|
class FrameSource:
|
|
45
|
-
"""Specifies the name of a getter and a
|
|
66
|
+
"""Specifies the name of a getter and a frame type for a given frame source.
|
|
46
67
|
|
|
47
68
|
From these two attributes, it is possible to fetch a frame from a processing tango
|
|
48
69
|
device and then decode it.
|
|
49
70
|
"""
|
|
50
71
|
|
|
51
72
|
getter_name: str
|
|
52
|
-
|
|
73
|
+
"""Name of the getter method to call on the tango device to fetch the data"""
|
|
53
74
|
|
|
75
|
+
frame_type: FrameType
|
|
76
|
+
"""Frame type"""
|
|
54
77
|
|
|
55
|
-
|
|
56
|
-
"""
|
|
57
|
-
|
|
58
|
-
SAVING_PARAMS_NAMES: list[str]
|
|
59
|
-
"""Names of members of type "saving_params" in the corresponding lima2 "proc_params" struct.
|
|
78
|
+
saving_channel: Optional[str]
|
|
79
|
+
"""Name of the associated saving_params object in the proc_params struct.
|
|
60
80
|
|
|
61
|
-
|
|
81
|
+
Can be None for sources without persistency.
|
|
62
82
|
"""
|
|
63
83
|
|
|
84
|
+
|
|
85
|
+
class Pipeline:
|
|
86
|
+
"""A base class for all processing pipelines."""
|
|
87
|
+
|
|
64
88
|
FRAME_SOURCES: dict[str, FrameSource]
|
|
65
|
-
"""Map of available frame source names to a corresponding
|
|
89
|
+
"""Map of available frame source names to a corresponding FrameSource descriptor.
|
|
66
90
|
|
|
67
91
|
Definition in child classes is enforced by __init_subclass__().
|
|
68
92
|
"""
|
|
@@ -128,11 +152,6 @@ class Pipeline:
|
|
|
128
152
|
@classmethod
|
|
129
153
|
def __init_subclass__(cls) -> None:
|
|
130
154
|
"""Initialize a pipeline subclass."""
|
|
131
|
-
if not hasattr(cls, "SAVING_PARAMS_NAMES"):
|
|
132
|
-
raise ValueError(
|
|
133
|
-
f"Pipeline subclass {cls} must define a SAVING_PARAMS_NAMES class member"
|
|
134
|
-
)
|
|
135
|
-
|
|
136
155
|
if not hasattr(cls, "FRAME_SOURCES"):
|
|
137
156
|
raise ValueError(
|
|
138
157
|
f"Pipeline subclass {cls} must define a FRAME_SOURCES class member"
|
|
@@ -225,6 +244,13 @@ class Pipeline:
|
|
|
225
244
|
f"Available ones are: {list(self.FRAME_SOURCES.keys())}"
|
|
226
245
|
)
|
|
227
246
|
|
|
247
|
+
try:
|
|
248
|
+
decode = decoder_by_type[frame_source.frame_type]
|
|
249
|
+
except KeyError as e:
|
|
250
|
+
raise NotImplementedError(
|
|
251
|
+
f"Frame type {frame_source.frame_type} decoder missing"
|
|
252
|
+
) from e
|
|
253
|
+
|
|
228
254
|
getters = [getattr(dev, frame_source.getter_name) for dev in self._devs]
|
|
229
255
|
|
|
230
256
|
if frame_idx == -1:
|
|
@@ -238,7 +264,7 @@ class Pipeline:
|
|
|
238
264
|
Returns None if the receiver hasn't yet processed any frames.
|
|
239
265
|
"""
|
|
240
266
|
try:
|
|
241
|
-
return
|
|
267
|
+
return decode(getters[rcv_idx](-1))
|
|
242
268
|
except tango.DevFailed:
|
|
243
269
|
return None
|
|
244
270
|
|
|
@@ -264,7 +290,7 @@ class Pipeline:
|
|
|
264
290
|
# Get a frame by absolute index -> use topology-specific lookup()
|
|
265
291
|
frame = self.topology.lookup(frame_idx=frame_idx, getters=getters)
|
|
266
292
|
|
|
267
|
-
return
|
|
293
|
+
return decode(frame)
|
|
268
294
|
|
|
269
295
|
def cache_byproduct(self, key: str, data: np.ndarray) -> None:
|
|
270
296
|
"""Store `data` into `self.byproduct_cache[key]`.
|
|
@@ -13,8 +13,7 @@ from uuid import UUID
|
|
|
13
13
|
import tango
|
|
14
14
|
from beartype import beartype
|
|
15
15
|
|
|
16
|
-
from lima2.client.
|
|
17
|
-
from lima2.client.pipeline import FrameSource, Pipeline
|
|
16
|
+
from lima2.client.pipeline import FrameSource, FrameType, Pipeline
|
|
18
17
|
from lima2.client.topology import TopologyKind
|
|
19
18
|
|
|
20
19
|
# Create a logger
|
|
@@ -25,14 +24,17 @@ _logger = logging.getLogger(__name__)
|
|
|
25
24
|
class Legacy(Pipeline):
|
|
26
25
|
tango_class = "LimaProcessingLegacy"
|
|
27
26
|
|
|
28
|
-
SAVING_PARAMS_NAMES = ["saving"]
|
|
29
|
-
"""Members of type "saving_params" in the corresponding lima2 "proc_params" struct."""
|
|
30
|
-
|
|
31
27
|
FRAME_SOURCES = {
|
|
32
28
|
"input_frame": FrameSource(
|
|
33
|
-
getter_name="getInputFrame",
|
|
29
|
+
getter_name="getInputFrame",
|
|
30
|
+
frame_type=FrameType.DENSE,
|
|
31
|
+
saving_channel=None,
|
|
32
|
+
),
|
|
33
|
+
"frame": FrameSource(
|
|
34
|
+
getter_name="getFrame",
|
|
35
|
+
frame_type=FrameType.DENSE,
|
|
36
|
+
saving_channel="saving",
|
|
34
37
|
),
|
|
35
|
-
"frame": FrameSource(getter_name="getFrame", decoder=dense_frame.decode),
|
|
36
38
|
}
|
|
37
39
|
"""Available frame sources."""
|
|
38
40
|
|
|
@@ -16,10 +16,9 @@ import tango
|
|
|
16
16
|
from lima2.client import utils
|
|
17
17
|
from lima2.client.devencoded import (
|
|
18
18
|
dense_frame,
|
|
19
|
-
sparse_frame,
|
|
20
19
|
structured_array,
|
|
21
20
|
)
|
|
22
|
-
from lima2.client.pipeline import FrameSource, Pipeline
|
|
21
|
+
from lima2.client.pipeline import FrameSource, FrameType, Pipeline
|
|
23
22
|
from lima2.client.topology import TopologyKind
|
|
24
23
|
|
|
25
24
|
# Create a logger
|
|
@@ -29,23 +28,27 @@ logger = logging.getLogger(__name__)
|
|
|
29
28
|
class Smx(Pipeline):
|
|
30
29
|
tango_class = "LimaProcessingSmx"
|
|
31
30
|
|
|
32
|
-
# Members of type "saving_params" in the corresponding lima2 "proc_params" struct.
|
|
33
|
-
SAVING_PARAMS_NAMES = [
|
|
34
|
-
"saving_dense",
|
|
35
|
-
"saving_sparse",
|
|
36
|
-
"saving_accumulation_corrected",
|
|
37
|
-
"saving_accumulation_peak",
|
|
38
|
-
]
|
|
39
|
-
|
|
40
31
|
FRAME_SOURCES = {
|
|
41
|
-
"frame": FrameSource(
|
|
32
|
+
"frame": FrameSource(
|
|
33
|
+
getter_name="getFrame",
|
|
34
|
+
frame_type=FrameType.DENSE,
|
|
35
|
+
saving_channel="saving_dense",
|
|
36
|
+
),
|
|
42
37
|
"sparse_frame": FrameSource(
|
|
43
|
-
getter_name="getSparseFrame",
|
|
38
|
+
getter_name="getSparseFrame",
|
|
39
|
+
frame_type=FrameType.SMX_SPARSE,
|
|
40
|
+
saving_channel="saving_sparse",
|
|
44
41
|
),
|
|
45
42
|
"acc_corrected": FrameSource(
|
|
46
|
-
getter_name="getAccCorrected",
|
|
43
|
+
getter_name="getAccCorrected",
|
|
44
|
+
frame_type=FrameType.DENSE,
|
|
45
|
+
saving_channel="saving_accumulation_corrected",
|
|
46
|
+
),
|
|
47
|
+
"acc_peaks": FrameSource(
|
|
48
|
+
getter_name="getAccPeaks",
|
|
49
|
+
frame_type=FrameType.DENSE,
|
|
50
|
+
saving_channel="saving_accumulation_peak",
|
|
47
51
|
),
|
|
48
|
-
"acc_peaks": FrameSource(getter_name="getAccPeaks", decoder=dense_frame.decode),
|
|
49
52
|
}
|
|
50
53
|
"""Available frame sources."""
|
|
51
54
|
|
|
@@ -118,3 +121,13 @@ class Smx(Pipeline):
|
|
|
118
121
|
logger.debug(f"Returning peak counters for frames: {data['frame_idx']}")
|
|
119
122
|
|
|
120
123
|
return (data["frame_idx"], data["nb_peaks"])
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def channels(self):
|
|
127
|
+
"""Return the channel descriptions"""
|
|
128
|
+
return {
|
|
129
|
+
"frame": self.input_frame_info[0],
|
|
130
|
+
"sparse_frame": self.processed_frame_info[0],
|
|
131
|
+
"acc_corrected": self.processed_frame_info[0],
|
|
132
|
+
"acc_peaks": self.processed_frame_info[0],
|
|
133
|
+
}
|
|
@@ -14,8 +14,8 @@ import numpy as np
|
|
|
14
14
|
import tango
|
|
15
15
|
|
|
16
16
|
from lima2.client import progress_counter, utils
|
|
17
|
-
from lima2.client.devencoded import
|
|
18
|
-
from lima2.client.pipeline import FrameSource, Pipeline
|
|
17
|
+
from lima2.client.devencoded import structured_array
|
|
18
|
+
from lima2.client.pipeline import FrameSource, FrameType, Pipeline
|
|
19
19
|
from lima2.client.topology import TopologyKind
|
|
20
20
|
|
|
21
21
|
# Create a logger
|
|
@@ -25,19 +25,21 @@ logger = logging.getLogger(__name__)
|
|
|
25
25
|
class Xpcs(Pipeline):
|
|
26
26
|
tango_class = "LimaProcessingXpcs"
|
|
27
27
|
|
|
28
|
-
# Members of type "saving_params" in the corresponding lima2 "proc_params" struct.
|
|
29
|
-
SAVING_PARAMS_NAMES = [
|
|
30
|
-
"saving_dense",
|
|
31
|
-
"saving_sparse",
|
|
32
|
-
]
|
|
33
|
-
|
|
34
28
|
FRAME_SOURCES = {
|
|
35
29
|
"input_frame": FrameSource(
|
|
36
|
-
getter_name="getInputFrame",
|
|
30
|
+
getter_name="getInputFrame",
|
|
31
|
+
frame_type=FrameType.DENSE,
|
|
32
|
+
saving_channel=None,
|
|
33
|
+
),
|
|
34
|
+
"frame": FrameSource(
|
|
35
|
+
getter_name="getFrame",
|
|
36
|
+
frame_type=FrameType.DENSE,
|
|
37
|
+
saving_channel="saving_dense",
|
|
37
38
|
),
|
|
38
|
-
"frame": FrameSource(getter_name="getFrame", decoder=dense_frame.decode),
|
|
39
39
|
"sparse_frame": FrameSource(
|
|
40
|
-
getter_name="getSparseFrame",
|
|
40
|
+
getter_name="getSparseFrame",
|
|
41
|
+
frame_type=FrameType.SPARSE,
|
|
42
|
+
saving_channel="saving_sparse",
|
|
41
43
|
),
|
|
42
44
|
}
|
|
43
45
|
"""Available frame sources."""
|
|
@@ -83,7 +83,10 @@ class StateMachine(fsm.StateMachine):
|
|
|
83
83
|
State.FAULT,
|
|
84
84
|
State.UNKNOWN,
|
|
85
85
|
]:
|
|
86
|
-
|
|
86
|
+
_logger.error(
|
|
87
|
+
f"Device {evt.device.dev_name()} in FAULT state. Stopping all devices."
|
|
88
|
+
)
|
|
89
|
+
self.abort()
|
|
87
90
|
|
|
88
91
|
if self.state == State.DISCONNECTED:
|
|
89
92
|
_logger.info(
|
|
@@ -115,18 +118,29 @@ class StateMachine(fsm.StateMachine):
|
|
|
115
118
|
for device in [self.ctrl] + self.recvs:
|
|
116
119
|
device.ping()
|
|
117
120
|
states = [d.acq_state for d in [self.ctrl] + self.recvs]
|
|
118
|
-
|
|
121
|
+
ctl_state = states[0]
|
|
122
|
+
|
|
123
|
+
ServerState = type(ctl_state)
|
|
124
|
+
|
|
125
|
+
if all(s in [ServerState.idle, ServerState.prepared] for s in states):
|
|
126
|
+
# Tolerate a mix of prepared and idle devices. In this case,
|
|
127
|
+
# the global state shall be IDLE (forcing user to prepare again).
|
|
128
|
+
self.state = State.IDLE
|
|
129
|
+
elif any(s == ServerState.fault for s in states):
|
|
130
|
+
self.state = State.FAULT
|
|
131
|
+
elif not all(s == ctl_state for s in states):
|
|
132
|
+
# Otherwise, if states are divergent, sync cannot be done
|
|
119
133
|
self._state = State.UNKNOWN
|
|
120
134
|
raise RuntimeError(f"Cannot sync: inconsistent server states ({states})")
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
135
|
+
else:
|
|
136
|
+
map = {
|
|
137
|
+
ServerState.idle: State.IDLE,
|
|
138
|
+
ServerState.prepared: State.PREPARED,
|
|
139
|
+
ServerState.running: State.RUNNING,
|
|
140
|
+
ServerState.stopped: State.IDLE,
|
|
141
|
+
}
|
|
142
|
+
self.state = map[ctl_state]
|
|
143
|
+
|
|
130
144
|
return self.state
|
|
131
145
|
|
|
132
146
|
@property
|
|
@@ -231,8 +245,18 @@ class StateMachine(fsm.StateMachine):
|
|
|
231
245
|
],
|
|
232
246
|
target=State.FAULT,
|
|
233
247
|
)
|
|
234
|
-
def
|
|
235
|
-
|
|
248
|
+
def abort(self):
|
|
249
|
+
"""Called when a device enters FAULT state."""
|
|
250
|
+
self.ctrl.Stop()
|
|
251
|
+
|
|
252
|
+
# Stop acquisition on receivers that are running
|
|
253
|
+
greenlets = [
|
|
254
|
+
gevent.spawn(recv.Stop)
|
|
255
|
+
for recv in self.recvs
|
|
256
|
+
if recv.acq_state.name == "running"
|
|
257
|
+
]
|
|
258
|
+
gevent.joinall(greenlets, raise_error=False)
|
|
259
|
+
|
|
236
260
|
self.ctrl.Close()
|
|
237
261
|
|
|
238
262
|
def __subscribe_events(self):
|
|
@@ -262,7 +286,8 @@ class StateMachine(fsm.StateMachine):
|
|
|
262
286
|
r.unsubscribe_event(e)
|
|
263
287
|
|
|
264
288
|
_logger.debug(f"on_end_acq while {self.state}")
|
|
265
|
-
|
|
289
|
+
if self.state == State.RUNNING:
|
|
290
|
+
gevent.spawn(self.close)
|
|
266
291
|
|
|
267
292
|
nb_frames_xferreds = []
|
|
268
293
|
|
|
@@ -152,8 +152,9 @@ def distribute_acq(
|
|
|
152
152
|
for i in range(num_receivers):
|
|
153
153
|
# Assign unique filename rank per receiver
|
|
154
154
|
processing_class = pipelines.get_class(proc_params["class_name"])
|
|
155
|
-
for
|
|
156
|
-
|
|
155
|
+
for source in processing_class.FRAME_SOURCES.values():
|
|
156
|
+
if source.saving_channel is not None:
|
|
157
|
+
proc[i][source.saving_channel]["filename_rank"] = i
|
|
157
158
|
|
|
158
159
|
# Idem for detector RAW saving params
|
|
159
160
|
if "saving" in acq[i]:
|
|
@@ -19,7 +19,7 @@ import os
|
|
|
19
19
|
import h5py
|
|
20
20
|
import numpy as np
|
|
21
21
|
|
|
22
|
-
from lima2.client.pipeline import Pipeline
|
|
22
|
+
from lima2.client.pipeline import FrameType, Pipeline
|
|
23
23
|
|
|
24
24
|
logger = logging.getLogger(__name__)
|
|
25
25
|
|
|
@@ -42,42 +42,69 @@ def enable_generation(
|
|
|
42
42
|
rr_offset (int): expected receiver of frame 0 in the round robin frame dispatching
|
|
43
43
|
"""
|
|
44
44
|
|
|
45
|
-
if
|
|
45
|
+
if rr_offset >= len(pipeline._devs):
|
|
46
46
|
raise ValueError(
|
|
47
|
-
f"
|
|
47
|
+
f"Round robin offset cannot exceed the last receiver index ({len(pipeline._devs) - 1})."
|
|
48
48
|
)
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
file_exists_policy = proc_params["saving"]["file_exists_policy"]
|
|
50
|
+
generate_params: list[dict] = []
|
|
51
|
+
"""Params passed to generate_master_file for each enabled saving channel"""
|
|
53
52
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
53
|
+
for name, source in pipeline.FRAME_SOURCES.items():
|
|
54
|
+
# Skip non-dense frame sources
|
|
55
|
+
if source.frame_type != FrameType.DENSE:
|
|
56
|
+
continue
|
|
57
|
+
# Skip non-persistent sources
|
|
58
|
+
if source.saving_channel is None:
|
|
59
|
+
continue
|
|
60
|
+
# Skip source if channel is disabled
|
|
61
|
+
if not proc_params[source.saving_channel]["enabled"]:
|
|
62
|
+
continue
|
|
60
63
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
)
|
|
64
|
+
base_path = proc_params[source.saving_channel]["base_path"]
|
|
65
|
+
filename_prefix = proc_params[source.saving_channel]["filename_prefix"]
|
|
66
|
+
file_exists_policy = proc_params[source.saving_channel]["file_exists_policy"]
|
|
65
67
|
|
|
66
|
-
|
|
68
|
+
master_file_path = f"{base_path}/{filename_prefix}_master.h5"
|
|
69
|
+
if os.path.exists(master_file_path) and file_exists_policy != "overwrite":
|
|
70
|
+
raise ValueError(
|
|
71
|
+
f"Master file (channel={source.saving_channel}) exists at {master_file_path} "
|
|
72
|
+
f"but {file_exists_policy=}. Cannot enable master file generation."
|
|
73
|
+
)
|
|
67
74
|
|
|
68
|
-
|
|
75
|
+
logger.info(
|
|
76
|
+
f"Master file (channel={source.saving_channel}) will be generated "
|
|
77
|
+
f"at {master_file_path}..."
|
|
78
|
+
)
|
|
69
79
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
80
|
+
frame_info = pipeline.channels[name]
|
|
81
|
+
|
|
82
|
+
# Calling register_on_finished here with a lambda doesn't work as expected
|
|
83
|
+
# because some parameters will be evaluated at the time of calling, and their value
|
|
84
|
+
# will have changed in subsequent loop iterations.
|
|
85
|
+
# One solution is to store the parameters in a dict and call register_on_finished
|
|
86
|
+
# outside of the loop.
|
|
87
|
+
generate_params.append(
|
|
88
|
+
dict(
|
|
89
|
+
num_frames=num_frames,
|
|
90
|
+
frame_shape=frame_info["shape"],
|
|
91
|
+
pixel_dtype=frame_info["dtype"],
|
|
92
|
+
num_receivers=len(pipeline._devs),
|
|
93
|
+
saving_params=proc_params[source.saving_channel],
|
|
94
|
+
rr_offset=rr_offset,
|
|
95
|
+
)
|
|
78
96
|
)
|
|
79
97
|
|
|
80
|
-
|
|
98
|
+
if len(generate_params) == 0:
|
|
99
|
+
logger.warning(
|
|
100
|
+
"Master file generation not enabled: no dense frames saving channel enabled."
|
|
101
|
+
)
|
|
102
|
+
else:
|
|
103
|
+
pipeline.register_on_finished(
|
|
104
|
+
on_finished=lambda: [
|
|
105
|
+
generate_master_file(**params) for params in generate_params
|
|
106
|
+
]
|
|
107
|
+
)
|
|
81
108
|
|
|
82
109
|
|
|
83
110
|
def generate_master_file(
|
|
@@ -22,6 +22,7 @@ src/lima2/client/utils.py
|
|
|
22
22
|
src/lima2/client/devencoded/__init__.py
|
|
23
23
|
src/lima2/client/devencoded/dense_frame.py
|
|
24
24
|
src/lima2/client/devencoded/exception.py
|
|
25
|
+
src/lima2/client/devencoded/smx_sparse_frame.py
|
|
25
26
|
src/lima2/client/devencoded/sparse_frame.py
|
|
26
27
|
src/lima2/client/devencoded/structured_array.py
|
|
27
28
|
src/lima2/client/pipelines/__init__.py
|
|
@@ -15,8 +15,8 @@ import numpy as np
|
|
|
15
15
|
import pytest
|
|
16
16
|
import tango
|
|
17
17
|
|
|
18
|
-
from lima2.client import progress_counter
|
|
19
|
-
from lima2.client.pipeline import FrameSource, Pipeline
|
|
18
|
+
from lima2.client import pipeline, progress_counter
|
|
19
|
+
from lima2.client.pipeline import FrameSource, FrameType, Pipeline
|
|
20
20
|
from lima2.client.pipelines.smx import Smx
|
|
21
21
|
from lima2.client.pipelines.xpcs import Xpcs
|
|
22
22
|
from lima2.client.topology import TopologyKind
|
|
@@ -90,6 +90,20 @@ def test_constructor_uneven(processing_devices):
|
|
|
90
90
|
)
|
|
91
91
|
|
|
92
92
|
|
|
93
|
+
def test_bad_subclass():
|
|
94
|
+
with pytest.raises(ValueError):
|
|
95
|
+
|
|
96
|
+
class NoFrameSourcePipeline(Pipeline):
|
|
97
|
+
tango_class = "badcafe"
|
|
98
|
+
# Missing FRAME_SOURCES
|
|
99
|
+
|
|
100
|
+
with pytest.raises(ValueError):
|
|
101
|
+
|
|
102
|
+
class NoTangoClassPipeline(Pipeline):
|
|
103
|
+
FRAME_SOURCES = {}
|
|
104
|
+
# Missing tango_class
|
|
105
|
+
|
|
106
|
+
|
|
93
107
|
def test_progress_counters(processing_devices):
|
|
94
108
|
"""Read progress_counters."""
|
|
95
109
|
processing_devices[0].progress_counters = json.dumps(
|
|
@@ -387,7 +401,7 @@ def test_pop_roi_profiles(processing_devices):
|
|
|
387
401
|
assert proc.byproduct_cache["roi_profiles"].size == 0 # No cache
|
|
388
402
|
|
|
389
403
|
|
|
390
|
-
def test_get_frame(processing_devices):
|
|
404
|
+
def test_get_frame(processing_devices, monkeypatch):
|
|
391
405
|
proc = Pipeline(
|
|
392
406
|
uuid=uuid1(),
|
|
393
407
|
proc_devs=processing_devices,
|
|
@@ -403,10 +417,26 @@ def test_get_frame(processing_devices):
|
|
|
403
417
|
proc.FRAME_SOURCES = {
|
|
404
418
|
"cafedeca": FrameSource(
|
|
405
419
|
getter_name="getFrame",
|
|
406
|
-
|
|
407
|
-
|
|
420
|
+
frame_type=FrameType.DENSE,
|
|
421
|
+
saving_channel=None,
|
|
422
|
+
),
|
|
423
|
+
"sparse": FrameSource(
|
|
424
|
+
getter_name="getSparseFrame",
|
|
425
|
+
frame_type=FrameType.SPARSE,
|
|
426
|
+
saving_channel=None,
|
|
427
|
+
),
|
|
408
428
|
}
|
|
409
429
|
|
|
430
|
+
def mock_decode(data: bytes):
|
|
431
|
+
return MockFrame(data[0], data[1:])
|
|
432
|
+
|
|
433
|
+
# Modify the decoder_by_type map to plug our mock decode function
|
|
434
|
+
# No decoder for SPARSE frame so we can test the case where the decoder is not implemented
|
|
435
|
+
monkeypatch.setattr(pipeline, "decoder_by_type", {FrameType.DENSE: mock_decode})
|
|
436
|
+
|
|
437
|
+
with pytest.raises(NotImplementedError):
|
|
438
|
+
proc.get_frame(0, source="sparse")
|
|
439
|
+
|
|
410
440
|
rcv_0_frames = {
|
|
411
441
|
0: b"\x00milk",
|
|
412
442
|
-1: b"\x02milk",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|