blissdata-lima2 2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4 @@
1
+ from .stream import Lima2Stream, Lima2View # noqa: F401
2
+
3
+ stream_cls = Lima2Stream
4
+ view_cls = Lima2View
@@ -0,0 +1,254 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # This file is part of the bliss project
4
+ #
5
+ # Copyright (c) Beamline Control Unit, ESRF
6
+ # Distributed under the GNU LGPLv3. See LICENSE for more info.
7
+
8
+ import logging
9
+ from collections.abc import Sequence
10
+
11
+ import lima2.client.services as l2s
12
+ import numpy as np
13
+ from blissdata.exceptions import (
14
+ EmptyViewException,
15
+ EndOfStream,
16
+ IndexNoMoreThereError,
17
+ IndexNotYetThereError,
18
+ )
19
+ from blissdata.lima.image_utils import ImageData
20
+ from blissdata.streams import (
21
+ BaseStream,
22
+ BaseView,
23
+ EventRange,
24
+ EventStream,
25
+ StreamDefinition,
26
+ )
27
+ from blissdata.streams.default import Stream
28
+ from blissdata.streams.encoding.numeric import NumericStreamEncoder
29
+ from blissdata.streams.lima.stream import LimaDirectAccess
30
+ from lima2.common.devencoded.sparse_frame import SparseFrame
31
+ from numpy.typing import DTypeLike
32
+
33
+ _logger = logging.getLogger(__name__)
34
+
35
+
36
+ def get_frame(
37
+ services: l2s.ConductorServices,
38
+ acq_uuid: str,
39
+ source: str,
40
+ frame_idx: int,
41
+ ) -> ImageData:
42
+ frm = services.pipeline.get_frame(frame_idx=frame_idx, source=source, uuid=acq_uuid)
43
+
44
+ if isinstance(frm, SparseFrame):
45
+ frm = frm.densify()
46
+
47
+ return ImageData(array=frm.data, frame_id=frm.idx, acq_tag=None)
48
+
49
+
50
+ class Lima2View(BaseView):
51
+ def __init__(
52
+ self,
53
+ services: l2s.ConductorServices,
54
+ acq_uuid: str,
55
+ source: str,
56
+ start: int,
57
+ stop: int,
58
+ ) -> None:
59
+ self._services = services
60
+ """Lima2 client services."""
61
+ self._acq_uuid = acq_uuid
62
+ """Lima2 acquisition id."""
63
+ self._source = source
64
+ """Frame source name."""
65
+ self._idx_range = range(start, stop)
66
+ """Range of absolute frame indices accessible via this view."""
67
+
68
+ @property
69
+ def index(self) -> int:
70
+ return self._idx_range.start
71
+
72
+ def __len__(self) -> int:
73
+ return len(self._idx_range)
74
+
75
+ def get_data(
76
+ self, start: int | None = None, stop: int | None = None
77
+ ) -> list[ImageData]:
78
+ try:
79
+ return [
80
+ get_frame(
81
+ services=self._services,
82
+ acq_uuid=self._acq_uuid,
83
+ source=self._source,
84
+ frame_idx=idx,
85
+ )
86
+ for idx in self._idx_range[start:stop]
87
+ ]
88
+ except RuntimeError as e:
89
+ raise IndexNoMoreThereError(
90
+ f"Can't fetch {self._source} {self._idx_range[start:stop].start} "
91
+ f"to {self._idx_range[start:stop].stop}: {e}"
92
+ ) from e
93
+
94
+
95
+ class Lima2Stream(BaseStream, LimaDirectAccess):
96
+ """Stream of Lima2 frames.
97
+
98
+ Frames don't actually transit inside the stream. The stream length can be
99
+ queried to determine the number of accessible frames.
100
+
101
+ Indexing or slicing the stream attempts to fetch frames directly from the
102
+ Lima2 backend.
103
+ """
104
+
105
+ PROTOCOL_VERSION = 2
106
+
107
+ def __init__(self, event_stream: EventStream) -> None:
108
+ BaseStream.__init__(self, event_stream)
109
+
110
+ _logger.debug(f"Instantiate Lima2Stream with {event_stream.info=}")
111
+
112
+ if event_stream.info["protocol_version"] != Lima2Stream.PROTOCOL_VERSION:
113
+ raise RuntimeError(
114
+ f"Lima2 protocol version mismatch "
115
+ f"(expected {Lima2Stream.PROTOCOL_VERSION}, "
116
+ f"got {event_stream.info['protocol_version']})"
117
+ )
118
+
119
+ self._dtype = np.dtype(event_stream.info["dtype"])
120
+ self._shape = tuple(event_stream.info["shape"])
121
+ self._acq_uuid = str(event_stream.info["acq_uuid"])
122
+ self._source = str(event_stream.info["source_name"])
123
+
124
+ self._services = l2s.init(
125
+ hostname=str(event_stream.info["conductor_hostname"]),
126
+ port=int(event_stream.info["conductor_port"]),
127
+ )
128
+ """Lima2 client session."""
129
+
130
+ self._length = 0
131
+ """Current number of accessible frames."""
132
+
133
+ self._cursor = Stream(event_stream).cursor()
134
+
135
+ @property
136
+ def kind(self):
137
+ return "array"
138
+
139
+ @staticmethod
140
+ def make_definition(
141
+ name: str,
142
+ source_name: str,
143
+ conductor_hostname: str,
144
+ conductor_port: int,
145
+ acq_uuid: str,
146
+ master_file: tuple[str, str] | None,
147
+ dtype: DTypeLike,
148
+ shape: Sequence,
149
+ ) -> StreamDefinition:
150
+ info = {
151
+ "plugin": "lima2",
152
+ "dtype": np.dtype(dtype).name,
153
+ "shape": shape,
154
+ "protocol_version": Lima2Stream.PROTOCOL_VERSION,
155
+ "acq_uuid": acq_uuid,
156
+ "source_name": source_name,
157
+ "conductor_hostname": conductor_hostname,
158
+ "conductor_port": conductor_port,
159
+ "master_file": master_file,
160
+ }
161
+
162
+ return StreamDefinition(name, info, NumericStreamEncoder(np.uint32))
163
+
164
+ @property
165
+ def plugin(self) -> str:
166
+ return "lima2"
167
+
168
+ @property
169
+ def dtype(self) -> np.dtype:
170
+ return self._dtype
171
+
172
+ @property
173
+ def shape(self) -> tuple[int, ...]:
174
+ return self._shape
175
+
176
+ def __len__(self) -> int:
177
+ try:
178
+ view = self._cursor.read(block=False, last_only=True)
179
+ except EndOfStream:
180
+ view = None
181
+
182
+ if view is not None:
183
+ last_status = view.get_data()[0]
184
+ self._length = int(last_status)
185
+ return self._length
186
+
187
+ def __getitem__(self, key: int | slice) -> ImageData | list[ImageData]:
188
+ idx_range = range(len(self))
189
+ if type(key) is int:
190
+ if key < 0 and not self.is_sealed():
191
+ raise IndexNotYetThereError(
192
+ "Can't index from end of stream until it is sealed"
193
+ )
194
+ return get_frame(
195
+ services=self._services,
196
+ acq_uuid=self._acq_uuid,
197
+ source=self._source,
198
+ frame_idx=idx_range[key],
199
+ )
200
+ elif type(key) is slice:
201
+ if not self.is_sealed() and ((key.start or 0) < 0 or (key.stop or 0) < 0):
202
+ raise IndexNotYetThereError(
203
+ "Can't slice from end of stream until it is sealed"
204
+ )
205
+
206
+ return [
207
+ get_frame(
208
+ services=self._services,
209
+ acq_uuid=self._acq_uuid,
210
+ source=self._source,
211
+ frame_idx=idx,
212
+ )
213
+ for idx in idx_range[key]
214
+ ]
215
+ else:
216
+ raise TypeError(f"{type(key)}")
217
+
218
+ def _need_last_only(self, last_only: bool) -> bool:
219
+ # Lima2 event stream represents current progress
220
+ # -> only the latest one is relevant.
221
+ return True
222
+
223
+ def _build_view_from_events(
224
+ self, index: int, events: EventRange, last_only: bool
225
+ ) -> Lima2View:
226
+ """
227
+ Build a Lima2View to access a slice of frames which starts at `index`,
228
+ and ends at the most recent frame according to `events`.
229
+ """
230
+ _logger.debug(f"{self.name}: {index=} -> {events=}")
231
+
232
+ # events.data[-1] corresponds to the current number of contiguous frames
233
+ # accessible from the lima2 backend.
234
+ stop_idx = events.data[-1]
235
+
236
+ if stop_idx <= index:
237
+ # no new image despite new events
238
+ raise EmptyViewException
239
+
240
+ return Lima2View(
241
+ services=self._services,
242
+ acq_uuid=self._acq_uuid,
243
+ source=self._source,
244
+ start=stop_idx - 1 if last_only else index,
245
+ stop=stop_idx,
246
+ )
247
+
248
+ def get_last_live_image(self) -> ImageData:
249
+ return get_frame(
250
+ services=self._services,
251
+ acq_uuid=self._acq_uuid,
252
+ source=self._source,
253
+ frame_idx=-1,
254
+ )
File without changes
@@ -0,0 +1 @@
1
+ from blissdata.tests.conftest import redis_db, redis_url, data_store # noqa F401
@@ -0,0 +1,178 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # This file is part of the bliss project
4
+ #
5
+ # Copyright (c) Beamline Control Unit, ESRF
6
+ # Distributed under the GNU LGPLv3. See LICENSE for more info.
7
+
8
+ """Unit test suite for Lima2 stream and view (streams/lima2.py)."""
9
+
10
+ from unittest.mock import Mock, call
11
+ from uuid import uuid1
12
+
13
+ import numpy as np
14
+ import pytest
15
+ from blissdata.exceptions import IndexNotYetThereError
16
+ from blissdata.lima.image_utils import ImageData
17
+ from blissdata.streams import EventStream
18
+
19
+ from blissdata_lima2 import Lima2Stream, Lima2View
20
+ from blissdata_lima2.stream import get_frame
21
+
22
+
23
+ def test_lima2_get_frame():
24
+ services = Mock()
25
+ uuid = str(uuid1())
26
+ frm = get_frame(services=services, acq_uuid=uuid, source="cafe", frame_idx=123)
27
+ services.pipeline.get_frame.assert_called_with(
28
+ frame_idx=123, source="cafe", uuid=uuid
29
+ )
30
+ assert type(frm) is ImageData
31
+
32
+
33
+ def test_lima2_view():
34
+ services = Mock()
35
+ uuid = str(uuid1())
36
+ view = Lima2View(
37
+ services=services,
38
+ acq_uuid=uuid,
39
+ source="cafe",
40
+ start=0,
41
+ stop=42,
42
+ )
43
+ assert len(view) == 42
44
+ assert view.index == 0
45
+
46
+ frames = view.get_data()
47
+ assert len(frames) == len(view)
48
+
49
+ services.pipeline.get_frame.assert_has_calls(
50
+ [call(uuid=uuid, source="cafe", frame_idx=i) for i in range(42)]
51
+ )
52
+
53
+
54
+ def test_lima2_protocol(data_store):
55
+ uuid = str(uuid1())
56
+
57
+ stream_def = Lima2Stream.make_definition(
58
+ name="device:cafe",
59
+ source_name="cafe",
60
+ conductor_hostname="www.lima2.org",
61
+ conductor_port=12345,
62
+ acq_uuid=uuid,
63
+ master_file=None,
64
+ dtype=np.float128, # fat pixels >:)
65
+ shape=(4, 1024, 512),
66
+ )
67
+ model = data_store._stream_model(
68
+ encoding=stream_def.encoder.info(), info=stream_def.info
69
+ )
70
+ model.info["protocol_version"] = 1 # hack the protocol number
71
+ event_stream = EventStream.create(data_store, stream_def.name, model)
72
+
73
+ with pytest.raises(RuntimeError):
74
+ _ = Lima2Stream(event_stream=event_stream)
75
+
76
+
77
+ def test_lima2_stream(data_store, monkeypatch):
78
+ uuid = str(uuid1())
79
+
80
+ stream_def = Lima2Stream.make_definition(
81
+ name="device:cafe",
82
+ source_name="cafe",
83
+ conductor_hostname="www.lima2.org",
84
+ conductor_port=12345,
85
+ acq_uuid=uuid,
86
+ master_file=None,
87
+ dtype=np.float128, # fat pixels >:)
88
+ shape=(4, 1024, 512),
89
+ )
90
+ model = data_store._stream_model(
91
+ encoding=stream_def.encoder.info(), info=stream_def.info
92
+ )
93
+
94
+ event_stream = EventStream.create(data_store, stream_def.name, model)
95
+ stream = Lima2Stream(event_stream=event_stream)
96
+
97
+ assert stream.plugin == "lima2"
98
+ assert stream.shape == stream_def.info["shape"]
99
+
100
+ # Feed the event stream
101
+ event_stream.send(np.uint32(42))
102
+ event_stream.join()
103
+ assert len(stream) == 42
104
+
105
+ event_stream.send(np.uint32(123))
106
+ event_stream.join()
107
+ assert len(stream) == 123
108
+
109
+ mock_get_frame = Mock()
110
+ monkeypatch.setattr("blissdata_lima2.stream.get_frame", mock_get_frame)
111
+
112
+ # Indexing
113
+ _ = stream[0]
114
+ mock_get_frame.assert_called_with(
115
+ services=stream._services,
116
+ acq_uuid=uuid,
117
+ source="cafe",
118
+ frame_idx=0,
119
+ )
120
+
121
+ with pytest.raises(IndexNotYetThereError):
122
+ _ = stream[-1]
123
+
124
+ # Slicing
125
+ _ = stream[:3]
126
+ assert mock_get_frame.mock_calls[-3:] == [
127
+ call(services=stream._services, acq_uuid=uuid, source="cafe", frame_idx=0),
128
+ call(services=stream._services, acq_uuid=uuid, source="cafe", frame_idx=1),
129
+ call(services=stream._services, acq_uuid=uuid, source="cafe", frame_idx=2),
130
+ ]
131
+
132
+ with pytest.raises(IndexNotYetThereError):
133
+ _ = stream[-2:]
134
+
135
+ with pytest.raises(IndexNotYetThereError):
136
+ _ = stream[:-2]
137
+
138
+ event_stream.seal()
139
+ event_stream.join()
140
+
141
+ # Now slicing/indexing from end is ok
142
+ _ = stream[-2:]
143
+ assert mock_get_frame.mock_calls[-2:] == [
144
+ call(
145
+ services=stream._services,
146
+ acq_uuid=uuid,
147
+ source="cafe",
148
+ frame_idx=123 - 2,
149
+ ),
150
+ call(
151
+ services=stream._services,
152
+ acq_uuid=uuid,
153
+ source="cafe",
154
+ frame_idx=123 - 1,
155
+ ),
156
+ ]
157
+
158
+ _ = stream[-3]
159
+ assert mock_get_frame.mock_calls[-1] == call(
160
+ services=stream._services,
161
+ acq_uuid=uuid,
162
+ source="cafe",
163
+ frame_idx=123 - 3,
164
+ )
165
+
166
+ _ = stream[:]
167
+ assert mock_get_frame.mock_calls[-123:] == [
168
+ call(services=stream._services, acq_uuid=uuid, source="cafe", frame_idx=i)
169
+ for i in range(123)
170
+ ]
171
+
172
+ _ = stream.get_last_live_image()
173
+ mock_get_frame.assert_called_with(
174
+ services=stream._services,
175
+ acq_uuid=uuid,
176
+ source="cafe",
177
+ frame_idx=-1,
178
+ )
@@ -0,0 +1,24 @@
1
+ Metadata-Version: 2.4
2
+ Name: blissdata-lima2
3
+ Version: 2.0
4
+ Summary: Lima2 plugin for blissdata
5
+ Maintainer: BCU (ESRF)
6
+ License-Expression: MIT
7
+ Keywords: blissdata,lima2
8
+ Classifier: Intended Audience :: Science/Research
9
+ Classifier: Programming Language :: Python :: 3
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE.md
13
+ Requires-Dist: blissdata~=2.3
14
+ Requires-Dist: lima2-client~=4.0
15
+ Provides-Extra: dev
16
+ Requires-Dist: black; extra == "dev"
17
+ Requires-Dist: flake8; extra == "dev"
18
+ Requires-Dist: pytest; extra == "dev"
19
+ Requires-Dist: pytest-redis; extra == "dev"
20
+ Dynamic: license-file
21
+
22
+ ## blissdata-lima2
23
+
24
+ Lima2 plugin for blissdata.
@@ -0,0 +1,11 @@
1
+ blissdata_lima2/__init__.py,sha256=HjlW54bFxQEN3BpM50XLU5OkbSsihhoJSOEBT6ojOlQ,104
2
+ blissdata_lima2/stream.py,sha256=9esgHEHvPDCiRbq94y8PmlTjyi4VfmSO1sLoGiARdHo,7745
3
+ blissdata_lima2/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ blissdata_lima2/tests/conftest.py,sha256=OFm368MkFYoAfQj-LEbUEa0_pbHvAB-rKgUtn6YZ0E8,82
5
+ blissdata_lima2/tests/test_lima2_stream.py,sha256=b6fPkJ_J0Ch9U_DjLdptuH7JrI0T60U0U-eEYgUf_Xs,4825
6
+ blissdata_lima2-2.0.dist-info/licenses/LICENSE.md,sha256=Ep4SbBn-yQzazV_DparPnregYNRiRfWuHFdm4fs0HGY,1076
7
+ blissdata_lima2-2.0.dist-info/METADATA,sha256=dlxcaEbSdzDpJ-Y-axUbJkpBucSl3HNBq17wVw06eK4,664
8
+ blissdata_lima2-2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
+ blissdata_lima2-2.0.dist-info/entry_points.txt,sha256=OmwUIvXzPNXIYa0SwkuBzrjsOMYPyeq9QHffgpWgL7g,36
10
+ blissdata_lima2-2.0.dist-info/top_level.txt,sha256=qD9nEgajoWcQsG0Dn0Rk-VIX91rKra1_CBBmN7O_1M8,16
11
+ blissdata_lima2-2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [blissdata]
2
+ lima2 = blissdata_lima2
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2020-2024 Beamline Control Unit, ESRF
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1 @@
1
+ blissdata_lima2