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.
Files changed (49) hide show
  1. {lima2_client-2.1.0/src/lima2.client.egg-info → lima2_client-2.1.2}/PKG-INFO +1 -1
  2. lima2_client-2.1.2/src/lima2/client/devencoded/smx_sparse_frame.py +128 -0
  3. {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2/client/pipeline.py +43 -17
  4. {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2/client/pipelines/legacy.py +9 -7
  5. {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2/client/pipelines/smx.py +27 -14
  6. {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2/client/pipelines/xpcs.py +13 -11
  7. {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2/client/state_machine.py +39 -14
  8. {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2/client/topology.py +3 -2
  9. {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2/master_file/generate.py +54 -27
  10. {lima2_client-2.1.0 → lima2_client-2.1.2/src/lima2.client.egg-info}/PKG-INFO +1 -1
  11. {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2.client.egg-info/SOURCES.txt +1 -0
  12. {lima2_client-2.1.0 → lima2_client-2.1.2}/tests/test_pipeline.py +35 -5
  13. {lima2_client-2.1.0 → lima2_client-2.1.2}/LICENSE +0 -0
  14. {lima2_client-2.1.0 → lima2_client-2.1.2}/README.md +0 -0
  15. {lima2_client-2.1.0 → lima2_client-2.1.2}/pyproject.toml +0 -0
  16. {lima2_client-2.1.0 → lima2_client-2.1.2}/setup.cfg +0 -0
  17. {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2/client/__init__.py +0 -0
  18. {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2/client/client.py +0 -0
  19. {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2/client/convert.py +0 -0
  20. {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2/client/detector.py +0 -0
  21. {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2/client/devencoded/__init__.py +0 -0
  22. {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2/client/devencoded/dense_frame.py +0 -0
  23. {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2/client/devencoded/exception.py +0 -0
  24. {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2/client/devencoded/sparse_frame.py +0 -0
  25. {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2/client/devencoded/structured_array.py +0 -0
  26. {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2/client/pipelines/__init__.py +0 -0
  27. {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2/client/progress_counter.py +0 -0
  28. {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2/client/py.typed +0 -0
  29. {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2/client/shell.py +0 -0
  30. {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2/client/utils.py +0 -0
  31. {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2/server/__init__.py +0 -0
  32. {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2/server/control.py +0 -0
  33. {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2/server/receiver.py +0 -0
  34. {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2/server/timer.py +0 -0
  35. {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2.client.egg-info/dependency_links.txt +0 -0
  36. {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2.client.egg-info/entry_points.txt +0 -0
  37. {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2.client.egg-info/requires.txt +0 -0
  38. {lima2_client-2.1.0 → lima2_client-2.1.2}/src/lima2.client.egg-info/top_level.txt +0 -0
  39. {lima2_client-2.1.0 → lima2_client-2.1.2}/tests/test_client.py +0 -0
  40. {lima2_client-2.1.0 → lima2_client-2.1.2}/tests/test_convert.py +0 -0
  41. {lima2_client-2.1.0 → lima2_client-2.1.2}/tests/test_dense_frame.py +0 -0
  42. {lima2_client-2.1.0 → lima2_client-2.1.2}/tests/test_detector.py +0 -0
  43. {lima2_client-2.1.0 → lima2_client-2.1.2}/tests/test_pipelines.py +0 -0
  44. {lima2_client-2.1.0 → lima2_client-2.1.2}/tests/test_progress_counter.py +0 -0
  45. {lima2_client-2.1.0 → lima2_client-2.1.2}/tests/test_sparse_frame.py +0 -0
  46. {lima2_client-2.1.0 → lima2_client-2.1.2}/tests/test_state_machine.py +0 -0
  47. {lima2_client-2.1.0 → lima2_client-2.1.2}/tests/test_timer.py +0 -0
  48. {lima2_client-2.1.0 → lima2_client-2.1.2}/tests/test_topology.py +0 -0
  49. {lima2_client-2.1.0 → lima2_client-2.1.2}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: lima2.client
3
- Version: 2.1.0
3
+ Version: 2.1.2
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)
@@ -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 structured_array
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 decode function for a given frame source.
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
- decoder: Callable
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
- class Pipeline:
56
- """A base class for all processing pipelines."""
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
- Definition in child classes is enforced by __init_subclass__().
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 getter name and decode function.
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 frame_source.decoder(getters[rcv_idx](-1))
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 frame_source.decoder(frame)
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.devencoded import dense_frame
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", decoder=dense_frame.decode
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(getter_name="getFrame", decoder=dense_frame.decode),
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", decoder=sparse_frame.decode
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", decoder=dense_frame.decode
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 dense_frame, sparse_frame, structured_array
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", decoder=dense_frame.decode
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", decoder=sparse_frame.decode
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
- self.error()
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
- if not all(s == states[0] for s in states):
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
- server_state = states[0]
122
- ServerState = type(server_state)
123
- map = {
124
- ServerState.idle: State.IDLE,
125
- ServerState.prepared: State.PREPARED,
126
- ServerState.running: State.RUNNING,
127
- ServerState.stopped: State.IDLE,
128
- }
129
- self.state = map[server_state]
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 error(self):
235
- # Send close to control device only (recv close by themselves)
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
- gevent.spawn(self.close)
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 name in processing_class.SAVING_PARAMS_NAMES:
156
- proc[i][name]["filename_rank"] = i
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 proc_params["class_name"] != "LimaProcessingLegacy":
45
+ if rr_offset >= len(pipeline._devs):
46
46
  raise ValueError(
47
- f"Unsupported master file generation for pipeline {proc_params['class_name']}"
47
+ f"Round robin offset cannot exceed the last receiver index ({len(pipeline._devs) - 1})."
48
48
  )
49
49
 
50
- base_path = proc_params["saving"]["base_path"]
51
- filename_prefix = proc_params["saving"]["filename_prefix"]
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
- master_file_path = f"{base_path}/{filename_prefix}_master.h5"
55
- if os.path.exists(master_file_path) and file_exists_policy != "overwrite":
56
- raise ValueError(
57
- f"Master file exists at {master_file_path} but {file_exists_policy=}. "
58
- "Aborting master file generation."
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
- if rr_offset >= len(pipeline._devs):
62
- raise ValueError(
63
- f"Round robin offset cannot exceed the last receiver index ({len(pipeline._devs) - 1})."
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
- logger.info(f"Master file will be generated at {master_file_path}...")
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
- frame_info = pipeline.channels["frame"]
75
+ logger.info(
76
+ f"Master file (channel={source.saving_channel}) will be generated "
77
+ f"at {master_file_path}..."
78
+ )
69
79
 
70
- def on_finished() -> None:
71
- generate_master_file(
72
- num_frames=num_frames,
73
- frame_shape=frame_info["shape"],
74
- pixel_dtype=frame_info["dtype"],
75
- num_receivers=len(pipeline._devs),
76
- saving_params=proc_params["saving"],
77
- rr_offset=rr_offset,
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
- pipeline.register_on_finished(on_finished=on_finished)
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(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: lima2.client
3
- Version: 2.1.0
3
+ Version: 2.1.2
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)
@@ -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
- decoder=lambda raw: MockFrame(idx=int(raw[0]), data=raw[1:]),
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