blissdata 0.3.2__tar.gz → 1.0.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.
- blissdata-1.0.0/PKG-INFO +42 -0
- blissdata-1.0.0/README.md +3 -0
- {blissdata-0.3.2/blissdata/data/nodes → blissdata-1.0.0/blissdata}/__init__.py +1 -5
- {blissdata-0.3.2 → blissdata-1.0.0}/blissdata/beacon/_base.py +3 -1
- {blissdata-0.3.2 → blissdata-1.0.0}/blissdata/beacon/data.py +14 -4
- {blissdata-0.3.2 → blissdata-1.0.0}/blissdata/h5api/dynamic_hdf5.py +4 -1
- {blissdata-0.3.2 → blissdata-1.0.0}/blissdata/h5api/utils/hdf5.py +30 -5
- {blissdata-0.3.2 → blissdata-1.0.0}/blissdata/h5api/utils/hdf5_retry.py +1 -1
- blissdata-1.0.0/blissdata/lima/client.py +315 -0
- blissdata-0.3.2/blissdata/data/lima_image.py → blissdata-1.0.0/blissdata/lima/image_utils.py +45 -32
- blissdata-1.0.0/blissdata/redis_engine/encoding/__init__.py +48 -0
- blissdata-1.0.0/blissdata/redis_engine/encoding/json.py +26 -0
- blissdata-1.0.0/blissdata/redis_engine/encoding/numeric.py +133 -0
- blissdata-1.0.0/blissdata/redis_engine/exceptions.py +51 -0
- blissdata-1.0.0/blissdata/redis_engine/identities.py +37 -0
- blissdata-1.0.0/blissdata/redis_engine/memory_tracker.py +340 -0
- blissdata-1.0.0/blissdata/redis_engine/scan.py +322 -0
- blissdata-1.0.0/blissdata/redis_engine/sink.py +511 -0
- blissdata-1.0.0/blissdata/redis_engine/store.py +404 -0
- blissdata-1.0.0/blissdata/redis_engine/stream.py +533 -0
- blissdata-1.0.0/blissdata/scan.py +41 -0
- blissdata-1.0.0/blissdata/schemas/scan_info.py +344 -0
- blissdata-1.0.0/blissdata/schemas/scan_info_display.py +126 -0
- blissdata-1.0.0/blissdata/stream.py +204 -0
- {blissdata-0.3.2 → blissdata-1.0.0}/blissdata/tests/beacon/test_data.py +2 -4
- blissdata-1.0.0/blissdata/tests/conftest.py +58 -0
- blissdata-1.0.0/blissdata/tests/h5api/__init__.py +0 -0
- {blissdata-0.3.2 → blissdata-1.0.0}/blissdata/tests/h5api/scanner.py +1 -1
- {blissdata-0.3.2 → blissdata-1.0.0}/blissdata/tests/h5api/test_dynamic_files.py +25 -29
- {blissdata-0.3.2 → blissdata-1.0.0}/blissdata/tests/h5api/test_static_files.py +21 -0
- blissdata-1.0.0/blissdata/tests/redis_engine/__init__.py +0 -0
- blissdata-1.0.0/blissdata/tests/redis_engine/conftest.py +14 -0
- blissdata-1.0.0/blissdata/tests/redis_engine/encoding/__init__.py +0 -0
- blissdata-1.0.0/blissdata/tests/redis_engine/encoding/test_numeric.py +192 -0
- blissdata-1.0.0/blissdata/tests/redis_engine/test_memory_tracker.py +499 -0
- blissdata-1.0.0/blissdata/tests/redis_engine/test_scan.py +78 -0
- blissdata-1.0.0/blissdata/tests/redis_engine/test_search.py +216 -0
- blissdata-1.0.0/blissdata/tests/redis_engine/test_streaming.py +682 -0
- blissdata-1.0.0/blissdata/tests/redis_engine/utils.py +23 -0
- blissdata-1.0.0/blissdata.egg-info/PKG-INFO +42 -0
- {blissdata-0.3.2 → blissdata-1.0.0}/blissdata.egg-info/SOURCES.txt +29 -35
- blissdata-1.0.0/blissdata.egg-info/entry_points.txt +2 -0
- {blissdata-0.3.2 → blissdata-1.0.0}/blissdata.egg-info/requires.txt +12 -3
- {blissdata-0.3.2 → blissdata-1.0.0}/pyproject.toml +1 -4
- {blissdata-0.3.2 → blissdata-1.0.0}/setup.cfg +13 -3
- blissdata-0.3.2/PKG-INFO +0 -67
- blissdata-0.3.2/README.md +0 -49
- blissdata-0.3.2/blissdata/__init__.py +0 -17
- blissdata-0.3.2/blissdata/client.py +0 -62
- blissdata-0.3.2/blissdata/common/__init__.py +0 -15
- blissdata-0.3.2/blissdata/common/utils.py +0 -143
- blissdata-0.3.2/blissdata/data/__init__.py +0 -15
- blissdata-0.3.2/blissdata/data/events/__init__.py +0 -22
- blissdata-0.3.2/blissdata/data/events/channel.py +0 -149
- blissdata-0.3.2/blissdata/data/events/lima.py +0 -458
- blissdata-0.3.2/blissdata/data/events/node.py +0 -28
- blissdata-0.3.2/blissdata/data/events/scan.py +0 -49
- blissdata-0.3.2/blissdata/data/events/walk.py +0 -42
- blissdata-0.3.2/blissdata/data/expiration.py +0 -42
- blissdata-0.3.2/blissdata/data/node.py +0 -1619
- blissdata-0.3.2/blissdata/data/nodes/channel.py +0 -390
- blissdata-0.3.2/blissdata/data/nodes/dataset.py +0 -82
- blissdata-0.3.2/blissdata/data/nodes/dataset_collection.py +0 -12
- blissdata-0.3.2/blissdata/data/nodes/lima.py +0 -422
- blissdata-0.3.2/blissdata/data/nodes/node_ref_channel.py +0 -34
- blissdata-0.3.2/blissdata/data/nodes/proposal.py +0 -12
- blissdata-0.3.2/blissdata/data/nodes/scan.py +0 -184
- blissdata-0.3.2/blissdata/data/nodes/scan_group.py +0 -12
- blissdata-0.3.2/blissdata/data/remote_node.py +0 -214
- blissdata-0.3.2/blissdata/data/scan.py +0 -659
- blissdata-0.3.2/blissdata/redis/__init__.py +0 -12
- blissdata-0.3.2/blissdata/redis/caching.py +0 -395
- blissdata-0.3.2/blissdata/redis/connection.py +0 -168
- blissdata-0.3.2/blissdata/redis/manager.py +0 -164
- blissdata-0.3.2/blissdata/redis/proxy.py +0 -969
- blissdata-0.3.2/blissdata/redis/scripting.py +0 -24
- blissdata-0.3.2/blissdata/settings.py +0 -1160
- blissdata-0.3.2/blissdata/streaming.py +0 -817
- blissdata-0.3.2/blissdata/streaming_events.py +0 -355
- blissdata-0.3.2/blissdata/tests/redis/manager.py +0 -23
- blissdata-0.3.2/blissdata.egg-info/PKG-INFO +0 -67
- {blissdata-0.3.2 → blissdata-1.0.0}/LICENSE +0 -0
- {blissdata-0.3.2 → blissdata-1.0.0}/blissdata/beacon/__init__.py +0 -0
- {blissdata-0.3.2 → blissdata-1.0.0}/blissdata/beacon/config.py +0 -0
- {blissdata-0.3.2 → blissdata-1.0.0}/blissdata/beacon/files.py +0 -0
- {blissdata-0.3.2 → blissdata-1.0.0}/blissdata/h5api/__init__.py +0 -0
- {blissdata-0.3.2 → blissdata-1.0.0}/blissdata/h5api/abstract.py +0 -0
- {blissdata-0.3.2 → blissdata-1.0.0}/blissdata/h5api/file_arguments.py +0 -0
- {blissdata-0.3.2 → blissdata-1.0.0}/blissdata/h5api/static_hdf5.py +0 -0
- {blissdata-0.3.2 → blissdata-1.0.0}/blissdata/h5api/utils/__init__.py +0 -0
- {blissdata-0.3.2 → blissdata-1.0.0}/blissdata/h5api/utils/bliss.py +0 -0
- {blissdata-0.3.2 → blissdata-1.0.0}/blissdata/h5api/utils/lima.py +0 -0
- {blissdata-0.3.2 → blissdata-1.0.0}/blissdata/h5api/utils/types.py +0 -0
- {blissdata-0.3.2/blissdata/tests → blissdata-1.0.0/blissdata/lima}/__init__.py +0 -0
- {blissdata-0.3.2/blissdata/tests/beacon → blissdata-1.0.0/blissdata/redis_engine}/__init__.py +0 -0
- {blissdata-0.3.2/blissdata/tests/h5api → blissdata-1.0.0/blissdata/schemas}/__init__.py +0 -0
- {blissdata-0.3.2/blissdata/tests/redis → blissdata-1.0.0/blissdata/tests}/__init__.py +0 -0
- /blissdata-0.3.2/blissdata/tests/conftest.py → /blissdata-1.0.0/blissdata/tests/beacon/__init__.py +0 -0
- {blissdata-0.3.2 → blissdata-1.0.0}/blissdata/tests/beacon/test_config.py +0 -0
- {blissdata-0.3.2 → blissdata-1.0.0}/blissdata/tests/beacon/test_files.py +0 -0
- {blissdata-0.3.2 → blissdata-1.0.0}/blissdata.egg-info/dependency_links.txt +0 -0
- {blissdata-0.3.2 → blissdata-1.0.0}/blissdata.egg-info/top_level.txt +0 -0
- {blissdata-0.3.2 → blissdata-1.0.0}/setup.py +0 -0
blissdata-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: blissdata
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Bliss data streaming client
|
|
5
|
+
Home-page: https://gitlab.esrf.fr/bliss/bliss/-/tree/master/blissdata
|
|
6
|
+
Author: BCU (ESRF)
|
|
7
|
+
License: LGPLv3
|
|
8
|
+
Classifier: Intended Audience :: Science/Research
|
|
9
|
+
Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Requires-Python: <3.10,>=3.7
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: beartype
|
|
15
|
+
Requires-Dist: h5py<3.10,>=2.9
|
|
16
|
+
Requires-Dist: msgpack
|
|
17
|
+
Requires-Dist: numpy>=1.13
|
|
18
|
+
Requires-Dist: redis>=4.6.0
|
|
19
|
+
Requires-Dist: redis-om
|
|
20
|
+
Requires-Dist: silx>=1.1.0
|
|
21
|
+
Provides-Extra: tango
|
|
22
|
+
Requires-Dist: pytango>=9.3.1; extra == "tango"
|
|
23
|
+
Provides-Extra: test
|
|
24
|
+
Requires-Dist: gevent>=20.9.0; extra == "test"
|
|
25
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
26
|
+
Requires-Dist: pytest-mock; extra == "test"
|
|
27
|
+
Requires-Dist: pytest-redis; extra == "test"
|
|
28
|
+
Requires-Dist: pytest-timeout; extra == "test"
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: gevent>=20.9.0; extra == "dev"
|
|
31
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
32
|
+
Requires-Dist: pytest-mock; extra == "dev"
|
|
33
|
+
Requires-Dist: pytest-redis; extra == "dev"
|
|
34
|
+
Requires-Dist: pytest-timeout; extra == "dev"
|
|
35
|
+
Requires-Dist: black==22.3; extra == "dev"
|
|
36
|
+
Requires-Dist: flake8>=4; extra == "dev"
|
|
37
|
+
Provides-Extra: beacon
|
|
38
|
+
Requires-Dist: ruamel.yaml; extra == "beacon"
|
|
39
|
+
|
|
40
|
+
# Blissdata
|
|
41
|
+
|
|
42
|
+
Documentation [here](https://bliss.gitlab-pages.esrf.fr/bliss/master/blissdata/getting_started.html).
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Base client to communicate with Beacon."""
|
|
2
2
|
|
|
3
|
+
import platform
|
|
3
4
|
import socket
|
|
4
5
|
import struct
|
|
5
6
|
import threading
|
|
@@ -28,7 +29,8 @@ class BeaconClient:
|
|
|
28
29
|
else:
|
|
29
30
|
self._address = host, port
|
|
30
31
|
connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
31
|
-
|
|
32
|
+
if platform.system() != "Windows":
|
|
33
|
+
connection.setsockopt(socket.SOL_IP, socket.IP_TOS, 0x10)
|
|
32
34
|
connection.connect(self._address)
|
|
33
35
|
connection.settimeout(timeout)
|
|
34
36
|
self._connection = connection
|
|
@@ -16,7 +16,8 @@ class BeaconData(BeaconClient):
|
|
|
16
16
|
REDIS_DATA_SERVER_OK = 34
|
|
17
17
|
|
|
18
18
|
def get_redis_db(self) -> str:
|
|
19
|
-
"""Returns the URL of the Redis database that contains the Bliss settings.
|
|
19
|
+
"""Returns the URL of the Redis database that contains the Bliss settings.
|
|
20
|
+
For example 'redis://foobar:25001' or 'unix:///tmp/redis.sock'."""
|
|
20
21
|
msg = b"%s%s" % (struct.pack("<ii", self.REDIS_QUERY, 0), b"")
|
|
21
22
|
self._connection.sendall(msg)
|
|
22
23
|
data = b""
|
|
@@ -32,14 +33,23 @@ class BeaconData(BeaconClient):
|
|
|
32
33
|
break
|
|
33
34
|
if message_type != self.REDIS_QUERY_ANSWER:
|
|
34
35
|
raise RuntimeError(f"Unexpected message type '{message_type}'")
|
|
35
|
-
|
|
36
|
+
host, port = message.decode().split(":")
|
|
37
|
+
if host == "localhost":
|
|
38
|
+
return f"unix://{port}"
|
|
39
|
+
else:
|
|
40
|
+
return f"redis://{host}:{port}"
|
|
36
41
|
|
|
37
42
|
def get_redis_data_db(self) -> str:
|
|
38
|
-
"""Returns the URL of the Redis database that contains the Bliss scan data.
|
|
43
|
+
"""Returns the URL of the Redis database that contains the Bliss scan data.
|
|
44
|
+
For example 'redis://foobar:25002' or 'unix:///tmp/redis_data.sock'."""
|
|
39
45
|
response = self._request(self.REDIS_DATA_SERVER_QUERY, "")
|
|
40
46
|
response_type, data = response.read()
|
|
41
47
|
if response_type == self.REDIS_DATA_SERVER_OK:
|
|
42
|
-
|
|
48
|
+
host, port = data.decode().split("|")[:2]
|
|
49
|
+
if host == "localhost":
|
|
50
|
+
return f"unix://{port}"
|
|
51
|
+
else:
|
|
52
|
+
return f"redis://{host}:{port}"
|
|
43
53
|
elif response_type == self.REDIS_DATA_SERVER_FAILED:
|
|
44
54
|
raise RuntimeError(data.decode())
|
|
45
55
|
raise RuntimeError(f"Unexpected Beacon response type {response_type}")
|
|
@@ -74,7 +74,10 @@ class Group(Node, abstract.Group):
|
|
|
74
74
|
|
|
75
75
|
def __getitem__(self, key: str) -> Node:
|
|
76
76
|
if not key.startswith(hdf5.SEP):
|
|
77
|
-
|
|
77
|
+
if self.name == hdf5.SEP:
|
|
78
|
+
key = f"{hdf5.SEP}{key}"
|
|
79
|
+
else:
|
|
80
|
+
key = f"{self.name}{hdf5.SEP}{key}"
|
|
78
81
|
h5py_obj = self._retry_handler.get_item(key)
|
|
79
82
|
if isinstance(h5py_obj, h5py.Group):
|
|
80
83
|
return Group(self._retry_handler, h5py_obj.name)
|
|
@@ -10,7 +10,7 @@ from . import types
|
|
|
10
10
|
from . import lima
|
|
11
11
|
from .hdf5_retry import retry_file_access
|
|
12
12
|
from .hdf5_retry import ignore_retry_timeout
|
|
13
|
-
from .hdf5_retry import RetryError, SoftRetryError
|
|
13
|
+
from .hdf5_retry import RetryTimeoutError, RetryError, SoftRetryError
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
HDF5Item = Union[h5py.Dataset, h5py.Group, h5py.File, lima.LimaDataset, lima.LimaGroup]
|
|
@@ -71,13 +71,19 @@ class DynamicHDF5Handler:
|
|
|
71
71
|
return self._file_obj
|
|
72
72
|
|
|
73
73
|
def get_item(self, name: str) -> HDF5Item:
|
|
74
|
-
|
|
74
|
+
try:
|
|
75
|
+
return self._retry_get_item(name)
|
|
76
|
+
except RetryTimeoutError as e:
|
|
77
|
+
raise KeyError(name) from e
|
|
75
78
|
|
|
76
79
|
def slice_dataset(self, name: str, idx: types.DataIndexType) -> types.DataType:
|
|
77
80
|
return self._retry_slice_dataset(name, idx)
|
|
78
81
|
|
|
79
82
|
def get_attr(self, name: str, key: str) -> Any:
|
|
80
|
-
|
|
83
|
+
try:
|
|
84
|
+
return self._retry_get_attr(name, key)
|
|
85
|
+
except RetryTimeoutError as e:
|
|
86
|
+
raise KeyError(name) from e
|
|
81
87
|
|
|
82
88
|
@ignore_retry_timeout
|
|
83
89
|
def iter_attrs(self, name: str) -> Iterator[str]:
|
|
@@ -138,7 +144,21 @@ class DynamicHDF5Handler:
|
|
|
138
144
|
item = self._native_items.get(name)
|
|
139
145
|
if item is not None:
|
|
140
146
|
return item
|
|
141
|
-
|
|
147
|
+
try:
|
|
148
|
+
item = self.file_obj[name]
|
|
149
|
+
except KeyError as e:
|
|
150
|
+
parent_name = name.rpartition(SEP)[0]
|
|
151
|
+
if not parent_name:
|
|
152
|
+
parent_name = SEP
|
|
153
|
+
if parent_name != name:
|
|
154
|
+
try:
|
|
155
|
+
parent_item = self._get_item(parent_name)
|
|
156
|
+
except KeyError:
|
|
157
|
+
pass
|
|
158
|
+
else:
|
|
159
|
+
if self._is_finished(parent_item):
|
|
160
|
+
raise RetryTimeoutError(str(e)) from e
|
|
161
|
+
raise
|
|
142
162
|
self._native_items[name] = item
|
|
143
163
|
return item
|
|
144
164
|
|
|
@@ -152,7 +172,12 @@ class DynamicHDF5Handler:
|
|
|
152
172
|
|
|
153
173
|
def _get_attr(self, name: str, key: str) -> Any:
|
|
154
174
|
item = self._get_item(name)
|
|
155
|
-
|
|
175
|
+
try:
|
|
176
|
+
return item.attrs[key]
|
|
177
|
+
except KeyError as e:
|
|
178
|
+
if self._is_finished(item):
|
|
179
|
+
raise RetryTimeoutError(str(e)) from e
|
|
180
|
+
raise
|
|
156
181
|
|
|
157
182
|
def _iter_attrs(self, name: str, start_index: int = 0) -> Iterator[str]:
|
|
158
183
|
item = self._get_item(name)
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# This file is part of the bliss project
|
|
4
|
+
#
|
|
5
|
+
# Copyright (c) 2015-2023 Beamline Control Unit, ESRF
|
|
6
|
+
# Distributed under the GNU LGPLv3. See LICENSE for more info.
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
import math
|
|
10
|
+
import numpy as np
|
|
11
|
+
from abc import ABC, abstractmethod
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
from blissdata.redis_engine.exceptions import (
|
|
16
|
+
IndexNoMoreThereError,
|
|
17
|
+
IndexNotYetThereError,
|
|
18
|
+
)
|
|
19
|
+
from . import image_utils
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
from gevent.monkey import is_anything_patched
|
|
23
|
+
except ImportError:
|
|
24
|
+
use_gevent = False
|
|
25
|
+
else:
|
|
26
|
+
use_gevent = is_anything_patched()
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
if use_gevent:
|
|
30
|
+
from tango.gevent import DeviceProxy
|
|
31
|
+
else:
|
|
32
|
+
from tango import DeviceProxy
|
|
33
|
+
from tango import DevSource
|
|
34
|
+
except ImportError:
|
|
35
|
+
DeviceProxy = None
|
|
36
|
+
DevSource = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def acquisition_on_server(data_store, server_url):
|
|
40
|
+
return int(data_store._redis.get(server_url).decode())
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def prepare_next_lima_acquisition(data_store, server_url):
|
|
44
|
+
pipe = data_store._redis.pipeline(transaction=True)
|
|
45
|
+
# when the key doesn't exist, set it to -1 before .incr()
|
|
46
|
+
pipe.setnx(server_url, -1)
|
|
47
|
+
pipe.incr(server_url)
|
|
48
|
+
return pipe.execute()[1]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class ImageReference:
|
|
53
|
+
format: str
|
|
54
|
+
file_path: str
|
|
55
|
+
data_path: Optional[str]
|
|
56
|
+
index: int
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class LimaClientInterface(ABC):
|
|
60
|
+
"""
|
|
61
|
+
Interface which have to be used to provide a service to LimaStream
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
@abstractmethod
|
|
65
|
+
def update(self, *args, **kwargs):
|
|
66
|
+
raise NotImplementedError
|
|
67
|
+
|
|
68
|
+
@abstractmethod
|
|
69
|
+
def __len__(self):
|
|
70
|
+
raise NotImplementedError
|
|
71
|
+
|
|
72
|
+
@abstractmethod
|
|
73
|
+
def __getitem__(self, frame_id):
|
|
74
|
+
raise NotImplementedError
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def lima_client_factory(
|
|
78
|
+
data_store, channel_info: dict, ref_mode: bool = False
|
|
79
|
+
) -> LimaClientInterface:
|
|
80
|
+
"""Factory to instanciate the appropriate lima client (Lima1, Lima2) in
|
|
81
|
+
either normal mode (client returns images) or ref mode (client returns
|
|
82
|
+
references to files)."""
|
|
83
|
+
format = channel_info.get("format")
|
|
84
|
+
if format == "lima_v1":
|
|
85
|
+
if ref_mode:
|
|
86
|
+
return LimaRefClient(data_store, **channel_info["lima_info"])
|
|
87
|
+
else:
|
|
88
|
+
return LimaClient(data_store, **channel_info["lima_info"])
|
|
89
|
+
elif format == "lima_v2":
|
|
90
|
+
raise NotImplementedError("No client for Lima2 yet")
|
|
91
|
+
else:
|
|
92
|
+
raise ValueError(f"Not a lima stream, format: {format}")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class LimaClient(LimaClientInterface):
|
|
96
|
+
PROTOCOL_VERSION = 1
|
|
97
|
+
|
|
98
|
+
def __init__(self, data_store, **lima_info):
|
|
99
|
+
protocol = lima_info["protocol_version"]
|
|
100
|
+
if self.PROTOCOL_VERSION != protocol:
|
|
101
|
+
raise Exception(
|
|
102
|
+
f"{type(self).__name__} supports lima json protocol {self.PROTOCOL_VERSION}, found version {protocol}"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
self._data_store = data_store
|
|
106
|
+
self._proxy = None
|
|
107
|
+
self._server_url = lima_info["server_url"]
|
|
108
|
+
self._buffer_max_number = lima_info["buffer_max_number"]
|
|
109
|
+
self._frame_per_acquisition = lima_info["frame_per_acquisition"]
|
|
110
|
+
self._acquisition_offset = lima_info["acquisition_offset"]
|
|
111
|
+
self._acq_trigger_mode: str | None = None
|
|
112
|
+
|
|
113
|
+
self._saved = "file_path" in lima_info
|
|
114
|
+
if self._saved:
|
|
115
|
+
self._file_offset = lima_info["file_offset"]
|
|
116
|
+
self._frame_per_file = lima_info["frame_per_file"]
|
|
117
|
+
self._file_path = lima_info["file_path"]
|
|
118
|
+
self._data_path = lima_info["data_path"]
|
|
119
|
+
self._file_format = lima_info["file_format"]
|
|
120
|
+
self._file_per_acquisition = math.ceil(
|
|
121
|
+
self._frame_per_acquisition / self._frame_per_file
|
|
122
|
+
)
|
|
123
|
+
else:
|
|
124
|
+
self._file_format = None
|
|
125
|
+
self._frame_per_file = 0
|
|
126
|
+
|
|
127
|
+
self._last_index = -1
|
|
128
|
+
self._last_index_saved = -1
|
|
129
|
+
self._last_index_saved_and_closed = -1
|
|
130
|
+
self._last_acq_seen_on_server = -1
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def acq_trigger_mode(self) -> str:
|
|
134
|
+
"""Returns the cached acq trigger mode of this detector"""
|
|
135
|
+
if self._acq_trigger_mode is not None:
|
|
136
|
+
return self._acq_trigger_mode
|
|
137
|
+
acq_trigger_mode = self.proxy.acq_trigger_mode
|
|
138
|
+
self._acq_trigger_mode = acq_trigger_mode
|
|
139
|
+
return acq_trigger_mode
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def file_format(self):
|
|
143
|
+
return self._file_format
|
|
144
|
+
|
|
145
|
+
def __len__(self):
|
|
146
|
+
return self._last_index + 1
|
|
147
|
+
|
|
148
|
+
def update(self, last_index, last_index_saved):
|
|
149
|
+
self._last_index = last_index
|
|
150
|
+
self._last_index_saved = last_index_saved
|
|
151
|
+
if self._saved:
|
|
152
|
+
self._last_index_saved_and_closed = self._last_readable_index(
|
|
153
|
+
last_index_saved
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def _decompose_frame_id(self, frame_id):
|
|
157
|
+
try:
|
|
158
|
+
acq_in_scan, frame_in_acq = divmod(frame_id, self._frame_per_acquisition)
|
|
159
|
+
except ZeroDivisionError:
|
|
160
|
+
acq_in_scan, frame_in_acq = 0, frame_id
|
|
161
|
+
try:
|
|
162
|
+
file_in_acq, frame_in_file = divmod(frame_in_acq, self._frame_per_file)
|
|
163
|
+
except ZeroDivisionError:
|
|
164
|
+
file_in_acq, frame_in_file = 0, frame_in_acq
|
|
165
|
+
return acq_in_scan, file_in_acq, frame_in_file
|
|
166
|
+
|
|
167
|
+
def _last_readable_index(self, frame_id):
|
|
168
|
+
"""Return the last index that can be read from a file (i.e. file is complete)"""
|
|
169
|
+
acq_in_scan, file_in_acq, _ = self._decompose_frame_id(frame_id)
|
|
170
|
+
return (
|
|
171
|
+
acq_in_scan * self._file_per_acquisition
|
|
172
|
+
+ file_in_acq * self._frame_per_file
|
|
173
|
+
- 1
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
def _is_acquisition_still_on_server(self, acq_id):
|
|
177
|
+
if acq_id < self._last_acq_seen_on_server:
|
|
178
|
+
# We already know this acquisition is outdated
|
|
179
|
+
return False
|
|
180
|
+
self._last_acq_seen_on_server = acquisition_on_server(
|
|
181
|
+
self._data_store, self._server_url
|
|
182
|
+
)
|
|
183
|
+
return acq_id == self._last_acq_seen_on_server
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def proxy(self):
|
|
187
|
+
"""Lazy connection to the tango device, because a lima client may never use it."""
|
|
188
|
+
if self._proxy is None:
|
|
189
|
+
if DeviceProxy is None:
|
|
190
|
+
raise RuntimeError("requires 'pytango' to be installed")
|
|
191
|
+
self._proxy = DeviceProxy(self._server_url)
|
|
192
|
+
self._proxy.set_source(DevSource.DEV)
|
|
193
|
+
return self._proxy
|
|
194
|
+
|
|
195
|
+
def get_last_live_image(self) -> image_utils.ImageData:
|
|
196
|
+
"""Returns the last frame from the memory buffer of Lima.
|
|
197
|
+
|
|
198
|
+
This code does not check if the frame is part of the actual scan.
|
|
199
|
+
|
|
200
|
+
Raises:
|
|
201
|
+
NoImageAvailable: when the lima server buffer does not yet contain any frame
|
|
202
|
+
ImageFormatNotSupported: when the retrieved data is not supported
|
|
203
|
+
"""
|
|
204
|
+
data = image_utils.image_from_server(self.proxy, -1)
|
|
205
|
+
|
|
206
|
+
def normalize_frame_id(frame_id: int | None) -> int | None:
|
|
207
|
+
if frame_id == 0:
|
|
208
|
+
trigger_mode = self.acq_trigger_mode
|
|
209
|
+
if trigger_mode == "INTERNAL_TRIGGER":
|
|
210
|
+
# Because of the bliss/lima architecture we can't really know the index
|
|
211
|
+
if self._last_index == 0:
|
|
212
|
+
# For a ct the index is valid
|
|
213
|
+
return 0
|
|
214
|
+
return None
|
|
215
|
+
return frame_id
|
|
216
|
+
|
|
217
|
+
frame_id = normalize_frame_id(data.frame_id)
|
|
218
|
+
return image_utils.ImageData(data.array, frame_id)
|
|
219
|
+
|
|
220
|
+
def __getitem__(self, key):
|
|
221
|
+
if isinstance(key, int):
|
|
222
|
+
return self._get_index(key)
|
|
223
|
+
elif isinstance(key, slice):
|
|
224
|
+
return self._get_slice(key)
|
|
225
|
+
else:
|
|
226
|
+
raise TypeError(
|
|
227
|
+
f"{type(self).__name__} indices must be integers or slices, not {type(key).__name__}"
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
def _get_index(self, frame_id):
|
|
231
|
+
if frame_id == -1:
|
|
232
|
+
return self.get_last_live_image().array
|
|
233
|
+
if frame_id < -1:
|
|
234
|
+
raise NotImplementedError("Negative index not supported")
|
|
235
|
+
|
|
236
|
+
if self._saved and frame_id <= self._last_index_saved_and_closed:
|
|
237
|
+
# immediately accessible from file
|
|
238
|
+
return self._get_from_file(frame_id)
|
|
239
|
+
elif frame_id <= self._last_index:
|
|
240
|
+
# try to access on lima server
|
|
241
|
+
try:
|
|
242
|
+
return self._get_from_server(frame_id)
|
|
243
|
+
except IndexNoMoreThereError:
|
|
244
|
+
# wait for image to become available from file
|
|
245
|
+
# TODO wait_for_file(timeout)
|
|
246
|
+
if self._saved:
|
|
247
|
+
return self._get_from_file(frame_id)
|
|
248
|
+
else:
|
|
249
|
+
raise
|
|
250
|
+
else:
|
|
251
|
+
raise IndexNotYetThereError
|
|
252
|
+
|
|
253
|
+
def _get_slice(self, key):
|
|
254
|
+
key_range = range(*key.indices(len(self)))
|
|
255
|
+
return np.array([self._get_index(i) for i in key_range])
|
|
256
|
+
|
|
257
|
+
def _file_ref(self, frame_id) -> ImageReference:
|
|
258
|
+
"""Convert a frame_id to an image reference to access from file.
|
|
259
|
+
IMPORTANT: It doesn't mean such frame_id is actually available."""
|
|
260
|
+
acq_in_scan, file_in_acq, frame_in_file = self._decompose_frame_id(frame_id)
|
|
261
|
+
file_in_scan = acq_in_scan * self._file_per_acquisition + file_in_acq
|
|
262
|
+
file_number = file_in_scan + self._file_offset
|
|
263
|
+
file_path = self._file_path % (file_number)
|
|
264
|
+
return ImageReference(
|
|
265
|
+
self._file_format, file_path, self._data_path, frame_in_file
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
def _get_from_server(self, frame_id):
|
|
269
|
+
if self._buffer_max_number <= (self._last_index - frame_id):
|
|
270
|
+
raise IndexNoMoreThereError() # TODO IndexError here and more details in LimaStream ???
|
|
271
|
+
|
|
272
|
+
acq_in_scan, file_in_acq, frame_in_file = self._decompose_frame_id(frame_id)
|
|
273
|
+
frame_in_acq = file_in_acq * self._frame_per_file + frame_in_file
|
|
274
|
+
|
|
275
|
+
# We need to check the server acquisition is the same before AND after reading the image.
|
|
276
|
+
# This is the only way to ensure the image belong to an acquisition. This is due to the
|
|
277
|
+
# lack of atomic operation to get an image and the current acquisition on server.
|
|
278
|
+
if not self._is_acquisition_still_on_server(
|
|
279
|
+
acq_in_scan + self._acquisition_offset
|
|
280
|
+
):
|
|
281
|
+
raise IndexNoMoreThereError() # TODO IndexError here and more details in LimaStream ???
|
|
282
|
+
|
|
283
|
+
image, index = image_utils.image_from_server(self.proxy, frame_in_acq)
|
|
284
|
+
assert frame_in_acq == index
|
|
285
|
+
|
|
286
|
+
if not self._is_acquisition_still_on_server(
|
|
287
|
+
acq_in_scan + self._acquisition_offset
|
|
288
|
+
):
|
|
289
|
+
raise IndexNoMoreThereError() # TODO IndexError here and more details in LimaStream ???
|
|
290
|
+
|
|
291
|
+
return image
|
|
292
|
+
|
|
293
|
+
def _get_from_file(self, frame_id):
|
|
294
|
+
ref = self._file_ref(frame_id)
|
|
295
|
+
return image_utils.image_from_file(
|
|
296
|
+
ref.file_path, ref.data_path, ref.index, ref.format
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
class LimaRefClient(LimaClient):
|
|
301
|
+
def __getitem__(self, key):
|
|
302
|
+
saved_len = self._last_index_saved + 1
|
|
303
|
+
if isinstance(key, int):
|
|
304
|
+
if key < 0:
|
|
305
|
+
key += saved_len
|
|
306
|
+
elif key >= saved_len:
|
|
307
|
+
raise IndexNotYetThereError
|
|
308
|
+
return self._file_ref(key)
|
|
309
|
+
elif isinstance(key, slice):
|
|
310
|
+
key_range = range(*key.indices(saved_len))
|
|
311
|
+
return [self._file_ref(i) for i in key_range]
|
|
312
|
+
else:
|
|
313
|
+
raise TypeError(
|
|
314
|
+
f"{type(self).__name__} indices must be integers or slices, not {type(key).__name__}"
|
|
315
|
+
)
|