ewoksdata 2.2.1__tar.gz → 2.3.0rc2__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.
- {ewoksdata-2.2.1/src/ewoksdata.egg-info → ewoksdata-2.3.0rc2}/PKG-INFO +3 -4
- {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/pyproject.toml +25 -7
- {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/src/ewoksdata/data/blissdata/__init__.py +3 -0
- ewoksdata-2.3.0rc2/src/ewoksdata/data/blissdata/_lima.py +447 -0
- {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/src/ewoksdata/data/blissdata/blissdatav1.py +0 -2
- {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/src/ewoksdata/data/blissdata/blissdatav2.py +0 -2
- {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/src/ewoksdata/data/hdf5/config.py +4 -3
- ewoksdata-2.3.0rc2/src/ewoksdata/data/hdf5/dataset_writer/_base.py +112 -0
- {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/src/ewoksdata/data/hdf5/dataset_writer/_non_sequential.py +74 -25
- ewoksdata-2.3.0rc2/src/ewoksdata/data/hdf5/dataset_writer/_non_sequential_stack.py +238 -0
- {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/src/ewoksdata/data/hdf5/dataset_writer/_sequential.py +7 -12
- {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/src/ewoksdata/data/hdf5/dataset_writer/_sequential_stack.py +18 -22
- ewoksdata-2.3.0rc2/src/ewoksdata/data/hdf5/mirror.py +673 -0
- {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/src/ewoksdata/data/hdf5/types.py +5 -3
- ewoksdata-2.3.0rc2/src/ewoksdata/tests/integration/__init__.py +0 -0
- ewoksdata-2.3.0rc2/src/ewoksdata/tests/integration/conftest.py +90 -0
- ewoksdata-2.3.0rc2/src/ewoksdata/tests/integration/test_blissdata.py +48 -0
- ewoksdata-2.3.0rc2/src/ewoksdata/tests/unit/__init__.py +0 -0
- ewoksdata-2.3.0rc2/src/ewoksdata/tests/unit/conftest.py +48 -0
- ewoksdata-2.3.0rc2/src/ewoksdata/tests/unit/data/bliss_scans.py +239 -0
- {ewoksdata-2.2.1/src/ewoksdata/tests → ewoksdata-2.3.0rc2/src/ewoksdata/tests/unit}/test_contextiterator.py +1 -1
- {ewoksdata-2.2.1/src/ewoksdata/tests → ewoksdata-2.3.0rc2/src/ewoksdata/tests/unit}/test_data_bliss.py +9 -9
- {ewoksdata-2.2.1/src/ewoksdata/tests → ewoksdata-2.3.0rc2/src/ewoksdata/tests/unit}/test_data_hdf5.py +1 -1
- {ewoksdata-2.2.1/src/ewoksdata/tests → ewoksdata-2.3.0rc2/src/ewoksdata/tests/unit}/test_data_nexus.py +2 -2
- {ewoksdata-2.2.1/src/ewoksdata/tests → ewoksdata-2.3.0rc2/src/ewoksdata/tests/unit}/test_dataset_writer.py +206 -12
- ewoksdata-2.3.0rc2/src/ewoksdata/tests/unit/test_find_lima_scans.py +217 -0
- ewoksdata-2.3.0rc2/src/ewoksdata/tests/unit/test_mirror_hdf5.py +1035 -0
- {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2/src/ewoksdata.egg-info}/PKG-INFO +3 -4
- {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/src/ewoksdata.egg-info/SOURCES.txt +16 -8
- {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/src/ewoksdata.egg-info/requires.txt +2 -3
- ewoksdata-2.2.1/src/ewoksdata/data/hdf5/dataset_writer/_base.py +0 -55
- ewoksdata-2.2.1/src/ewoksdata/data/hdf5/dataset_writer/_non_sequential_stack.py +0 -158
- ewoksdata-2.2.1/src/ewoksdata/tests/conftest.py +0 -30
- ewoksdata-2.2.1/src/ewoksdata/tests/data/bliss_scans.py +0 -137
- {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/LICENSE.md +0 -0
- {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/README.md +0 -0
- {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/setup.cfg +0 -0
- {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/src/ewoksdata/__init__.py +0 -0
- {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/src/ewoksdata/data/__init__.py +0 -0
- {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/src/ewoksdata/data/bliss.py +0 -0
- {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/src/ewoksdata/data/blissdata/blissdatav0.py +0 -0
- {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/src/ewoksdata/data/contextiterator.py +0 -0
- {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/src/ewoksdata/data/hdf5/__init__.py +0 -0
- {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/src/ewoksdata/data/hdf5/dataset_writer/__init__.py +0 -0
- {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/src/ewoksdata/data/nexus.py +0 -0
- {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/src/ewoksdata/data/url.py +0 -0
- {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/src/ewoksdata/data/utils.py +0 -0
- {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/src/ewoksdata/tests/__init__.py +0 -0
- {ewoksdata-2.2.1/src/ewoksdata/tests → ewoksdata-2.3.0rc2/src/ewoksdata/tests/unit}/data/__init__.py +0 -0
- {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/src/ewoksdata.egg-info/dependency_links.txt +0 -0
- {ewoksdata-2.2.1 → ewoksdata-2.3.0rc2}/src/ewoksdata.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ewoksdata
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0rc2
|
|
4
4
|
Summary: Ewoks tasks for data access
|
|
5
5
|
Author-email: ESRF <dau-pydev@esrf.fr>
|
|
6
6
|
License: # MIT License
|
|
@@ -40,6 +40,7 @@ Requires-Dist: h5py>=2.8
|
|
|
40
40
|
Requires-Dist: silx>=1.1.0
|
|
41
41
|
Requires-Dist: hdf5plugin
|
|
42
42
|
Requires-Dist: blissdata>=0.3.4
|
|
43
|
+
Requires-Dist: esrf-pathlib>=1.1.0
|
|
43
44
|
Requires-Dist: redis
|
|
44
45
|
Requires-Dist: redis!=5.3.0b1,!=5.3.0b3
|
|
45
46
|
Requires-Dist: redis-om!=1.0.0b0; python_version < "3.9"
|
|
@@ -51,9 +52,7 @@ Requires-Dist: ewoksdata[online]; extra == "test"
|
|
|
51
52
|
Requires-Dist: pytest>=7; extra == "test"
|
|
52
53
|
Provides-Extra: dev
|
|
53
54
|
Requires-Dist: ewoksdata[test]; extra == "dev"
|
|
54
|
-
Requires-Dist:
|
|
55
|
-
Requires-Dist: flake8>=4; extra == "dev"
|
|
56
|
-
Requires-Dist: isort; extra == "dev"
|
|
55
|
+
Requires-Dist: ruff; extra == "dev"
|
|
57
56
|
Provides-Extra: doc
|
|
58
57
|
Requires-Dist: ewoksdata[test]; extra == "doc"
|
|
59
58
|
Requires-Dist: sphinx>=4.5; extra == "doc"
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ewoksdata"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.3.0rc2"
|
|
8
8
|
authors = [{ name = "ESRF", email = "dau-pydev@esrf.fr" }]
|
|
9
9
|
description = "Ewoks tasks for data access"
|
|
10
10
|
readme = { file = "README.md", content-type = "text/markdown" }
|
|
@@ -21,6 +21,7 @@ dependencies = [
|
|
|
21
21
|
"silx >=1.1.0",
|
|
22
22
|
"hdf5plugin",
|
|
23
23
|
"blissdata >=0.3.4",
|
|
24
|
+
"esrf-pathlib >=1.1.0",
|
|
24
25
|
"redis", # missing dependency in blissdata<1
|
|
25
26
|
"redis !=5.3.0b1,!=5.3.0b3",
|
|
26
27
|
"redis-om !=1.0.0b0;python_version < '3.9'", # https://github.com/redis/redis-om-python/issues/693
|
|
@@ -44,9 +45,7 @@ test = [
|
|
|
44
45
|
]
|
|
45
46
|
dev = [
|
|
46
47
|
"ewoksdata[test]",
|
|
47
|
-
"
|
|
48
|
-
"flake8 >=4",
|
|
49
|
-
"isort",
|
|
48
|
+
"ruff",
|
|
50
49
|
]
|
|
51
50
|
doc = [
|
|
52
51
|
"ewoksdata[test]",
|
|
@@ -64,6 +63,25 @@ where = ["src"]
|
|
|
64
63
|
[tool.coverage.run]
|
|
65
64
|
omit = ['*/tests/*']
|
|
66
65
|
|
|
67
|
-
[tool.
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
[tool.ruff]
|
|
67
|
+
line-length = 88
|
|
68
|
+
|
|
69
|
+
[tool.ruff.lint]
|
|
70
|
+
select = [
|
|
71
|
+
"E", # pycodestyle errors
|
|
72
|
+
"F", # pyflakes
|
|
73
|
+
"I", # isort
|
|
74
|
+
"S", # flake8-bandit
|
|
75
|
+
]
|
|
76
|
+
ignore = [
|
|
77
|
+
"E501", # line too long (docstrings/comments are not wrapped by the formatter)
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
[tool.ruff.lint.per-file-ignores]
|
|
81
|
+
"**/test_*.py" = ["S101"]
|
|
82
|
+
"**/tests/**/conftest.py" = ["S101"]
|
|
83
|
+
"ci/*.py" = ["S101"]
|
|
84
|
+
|
|
85
|
+
[tool.ruff.lint.isort]
|
|
86
|
+
force-single-line = true
|
|
87
|
+
known-first-party = ["ewoksdata"]
|
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import re
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from importlib.metadata import version
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Dict
|
|
7
|
+
from typing import List
|
|
8
|
+
from typing import Optional
|
|
9
|
+
from typing import Tuple
|
|
10
|
+
from typing import Union
|
|
11
|
+
|
|
12
|
+
import h5py
|
|
13
|
+
import hdf5plugin # noqa F401
|
|
14
|
+
import numpy
|
|
15
|
+
from blissdata.h5api.utils.lima import LimaDataset
|
|
16
|
+
from esrf_pathlib import ESRFPath
|
|
17
|
+
from packaging.version import Version
|
|
18
|
+
from silx.io import h5py_utils
|
|
19
|
+
|
|
20
|
+
_logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
_FORBIDDEN_CREATORS = ["lima"]
|
|
23
|
+
_ALLOWED_CREATORS = ["bliss", "blissdata"]
|
|
24
|
+
|
|
25
|
+
_ESRF_TEMPLATE_PROPOSAL = "raw_proposal_file"
|
|
26
|
+
_ESRF_TEMPLATE_COLLECTION = "raw_collection_file"
|
|
27
|
+
_ESRF_TEMPLATE_DATASET = "raw_dataset_file"
|
|
28
|
+
_ALLOWED_ESRFPATH_TEMPLATES = [
|
|
29
|
+
_ESRF_TEMPLATE_PROPOSAL,
|
|
30
|
+
_ESRF_TEMPLATE_COLLECTION,
|
|
31
|
+
_ESRF_TEMPLATE_DATASET,
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class LimaScan:
|
|
37
|
+
"""A single scan/detector entry with the information to access the data, to be used for ewoks tasks.
|
|
38
|
+
|
|
39
|
+
One instance is created by :func:`find_lima_scans` per scan/detector pair, and it holds
|
|
40
|
+
everything needed to read the frames, either through the Bliss master file (``url``) or
|
|
41
|
+
directly from the Lima data files (``lima_urls``, ``lima_url_template``).
|
|
42
|
+
|
|
43
|
+
:ivar filename: The dataset master file containing the scan, i.e. the file holding the
|
|
44
|
+
``1.1``, ``2.1``, ``2.2``, ... groups. Normalized as an ESRF path (mount point stripped).
|
|
45
|
+
:ivar url: The url to access the data through the master file, in the format ``"filename::h5path"``,
|
|
46
|
+
the h5path being ``<scan_nb>.<subscan_nb>/measurement/<detector_name>``.
|
|
47
|
+
:ivar collection_name: The name of the collection/sample, taken from the file path.
|
|
48
|
+
:ivar dataset_name: The name of the dataset, taken from the file path.
|
|
49
|
+
:ivar scan_nb: The scan number, i.e. the part before the dot in the entry name.
|
|
50
|
+
:ivar subscan_nb: The subscan number, i.e. the part after the dot in the entry name (most of the time 1).
|
|
51
|
+
:ivar sample_id: The sample id parsed from the dataset name, ``None`` unless the dataset follows
|
|
52
|
+
the Daiquiri ``roi<sample_id>_<subsample_id>`` convention.
|
|
53
|
+
:ivar subsample_id: The subsample id parsed from the dataset name, ``None`` in the same conditions
|
|
54
|
+
as ``sample_id``.
|
|
55
|
+
:ivar detector_name: The name of the detector, as found in the measurement group.
|
|
56
|
+
:ivar data_shape: The shape of a single frame, i.e. the dataset shape without the frame dimension.
|
|
57
|
+
:ivar nb_frames: The number of frames in the scan, i.e. the first dimension of the dataset.
|
|
58
|
+
:ivar lima_urls: The urls of the Lima files holding the data, in the format ``"filename::h5path"``.
|
|
59
|
+
Missing files are skipped, so this list can be shorter than expected (or empty) for broken links.
|
|
60
|
+
:ivar lima_url_template: The template used to find the Lima files, ``None`` when they were found
|
|
61
|
+
through the virtual sources of the master file instead. See :func:`find_lima_scans`.
|
|
62
|
+
:ivar lima_url_template_args: The arguments to format ``lima_url_template``, ``None`` when there is
|
|
63
|
+
no template. See :func:`_get_lima_url_template_args`.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
filename: str
|
|
67
|
+
url: str
|
|
68
|
+
collection_name: str
|
|
69
|
+
dataset_name: str
|
|
70
|
+
scan_nb: int
|
|
71
|
+
subscan_nb: int
|
|
72
|
+
sample_id: Optional[int]
|
|
73
|
+
subsample_id: Optional[int]
|
|
74
|
+
detector_name: str
|
|
75
|
+
data_shape: Tuple[int, ...]
|
|
76
|
+
nb_frames: int
|
|
77
|
+
lima_urls: List[str]
|
|
78
|
+
lima_url_template: Optional[str]
|
|
79
|
+
lima_url_template_args: Optional[dict]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _normalize_esrf_path(
|
|
83
|
+
filename: Optional[Union[str, Path, ESRFPath]], assert_exists: bool = True
|
|
84
|
+
) -> ESRFPath:
|
|
85
|
+
"""
|
|
86
|
+
This method allows to use bliss files using VISA paths and also avoids
|
|
87
|
+
the full path problem between mounting points.
|
|
88
|
+
"""
|
|
89
|
+
if filename is not None:
|
|
90
|
+
filename = ESRFPath(filename).resolve().strip_mount_point()
|
|
91
|
+
if assert_exists and not filename.is_file():
|
|
92
|
+
raise FileNotFoundError(f"{filename} does not exist")
|
|
93
|
+
return filename
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _get_lima_url_template_args(
|
|
97
|
+
scan_nb: int,
|
|
98
|
+
img_acq_device: str,
|
|
99
|
+
collection_name: str,
|
|
100
|
+
dataset_name: str,
|
|
101
|
+
scan_number_format: str = "%05d",
|
|
102
|
+
) -> Dict[str, str]:
|
|
103
|
+
"""
|
|
104
|
+
Lima url template args is a dictionary meant to format the lima_url_template string, once it is passed to blissdata.h5api.dynamic_hdf5.File.
|
|
105
|
+
For the blissdata API, it is useful during online data analysis from the .h5 file (only if the Lima files are already available, depending on number_frames_per_file)
|
|
106
|
+
For offline data analysis, it is useful in some cases:
|
|
107
|
+
- ID13 sometimes has used the Eiger hardware saving format (rare cases), hence the links from the master file are broken,
|
|
108
|
+
and the reading should be done through the dynamicfile API using lima_url_template and lima_url_template_args, to find the correct files and links to read the data.
|
|
109
|
+
For the rest of the cases, lima_url_template is not needed, as the master file has the data linked to the Lima files.
|
|
110
|
+
"""
|
|
111
|
+
return {
|
|
112
|
+
"scan_nb": scan_number_format % int(scan_nb),
|
|
113
|
+
"img_acq_device": img_acq_device,
|
|
114
|
+
"collection_name": collection_name,
|
|
115
|
+
"dataset_name": dataset_name,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _parse_daiquiri_roi(
|
|
120
|
+
dataset_name: str,
|
|
121
|
+
) -> Tuple[Optional[int], Optional[int]]:
|
|
122
|
+
"""Return the ``(sample_id, subsample_id)`` encoded in a Daiquiri dataset name
|
|
123
|
+
(e.g. ``roi122006_140844``), or ``(None, None)`` when the name does not follow that convention.
|
|
124
|
+
"""
|
|
125
|
+
match_roi = re.match(r"^roi(\d+)_(\d+)$", dataset_name)
|
|
126
|
+
if match_roi:
|
|
127
|
+
sample_id, subsample_id = int(match_roi.group(1)), int(match_roi.group(2))
|
|
128
|
+
else:
|
|
129
|
+
sample_id, subsample_id = None, None
|
|
130
|
+
return sample_id, subsample_id
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _validate_bliss_file(h5_file: h5py.File) -> bool:
|
|
134
|
+
"""Return True if the HDF5 root ``creator`` attribute is a 'bliss', 'blissdata', False otherwise."""
|
|
135
|
+
creator = h5_file.attrs.get("creator")
|
|
136
|
+
if isinstance(creator, bytes):
|
|
137
|
+
creator = creator.decode()
|
|
138
|
+
creator = creator.lower()
|
|
139
|
+
for forbidden in _FORBIDDEN_CREATORS:
|
|
140
|
+
if creator in forbidden:
|
|
141
|
+
_logger.error(f"File creator is '{creator}' which is not allowed.")
|
|
142
|
+
return False
|
|
143
|
+
for allowed in _ALLOWED_CREATORS:
|
|
144
|
+
if creator in allowed:
|
|
145
|
+
return True
|
|
146
|
+
_logger.error(
|
|
147
|
+
f"File creator is '{creator}' which is not recognized as a valid Bliss file creator."
|
|
148
|
+
)
|
|
149
|
+
return False
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _discover_2d_detectors(measurement_group: h5py.Group) -> List[str]:
|
|
153
|
+
"""Return the names of the 2D (Lima) detectors (3D datasets) in a measurement group."""
|
|
154
|
+
# Is there a better way to search for Lima devices?
|
|
155
|
+
return [dset for dset in measurement_group if measurement_group[dset].ndim == 3]
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _get_lima_urls_from_virtual_sources(
|
|
159
|
+
dset: h5py.Dataset,
|
|
160
|
+
) -> List[str]:
|
|
161
|
+
"""Return the ``filename::h5path`` urls of the Lima files backing a virtual dataset.
|
|
162
|
+
|
|
163
|
+
The virtual sources of the Bliss master file are resolved relative to its directory.
|
|
164
|
+
Sources pointing to a missing file are skipped with a warning, so the returned list
|
|
165
|
+
may be shorter than the number of virtual sources (or empty for broken links).
|
|
166
|
+
"""
|
|
167
|
+
urls_lima = []
|
|
168
|
+
for vs in dset.virtual_sources():
|
|
169
|
+
filename_lima = Path(dset.file.filename).parent / vs.file_name
|
|
170
|
+
if not filename_lima.is_file():
|
|
171
|
+
_logger.warning(
|
|
172
|
+
f"Lima file {filename_lima} does not exist. Virtual dataset may be broken."
|
|
173
|
+
)
|
|
174
|
+
continue
|
|
175
|
+
urls_lima.append(f"{str(_normalize_esrf_path(filename_lima))}::{vs.dset_name}")
|
|
176
|
+
return urls_lima
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _get_lima_urls_from_template(
|
|
180
|
+
filename_dataset: str,
|
|
181
|
+
lima_url_template: str,
|
|
182
|
+
scan_nb: int,
|
|
183
|
+
detector: str,
|
|
184
|
+
collection_name: str,
|
|
185
|
+
dataset_name: str,
|
|
186
|
+
scan_number_format: Optional[str] = "%05d",
|
|
187
|
+
) -> List[str]:
|
|
188
|
+
"""Return the ``filename::h5path`` urls of the Lima files found with ``lima_url_template``.
|
|
189
|
+
|
|
190
|
+
Alternative to :func:`_get_lima_urls_from_virtual_sources` for the cases where the links of
|
|
191
|
+
the master file cannot be used (e.g. Eiger hardware saving): the files are searched on disk
|
|
192
|
+
with the blissdata API instead. Files that do not exist are skipped with a warning.
|
|
193
|
+
|
|
194
|
+
:param filename_dataset: The dataset master file; its directory is where the Lima files are searched.
|
|
195
|
+
:param lima_url_template: The ``"<file template>::<h5path>"`` template, see :func:`find_lima_scans`.
|
|
196
|
+
:param scan_nb: The scan number, formatted with ``scan_number_format``.
|
|
197
|
+
:param detector: The detector name, i.e. the ``{img_acq_device}`` placeholder of the template.
|
|
198
|
+
:param collection_name: The collection name, i.e. the ``{collection_name}`` placeholder of the template.
|
|
199
|
+
:param dataset_name: The dataset name, i.e. the ``{dataset_name}`` placeholder of the template.
|
|
200
|
+
:param scan_number_format: The format used for the scan number (default ``"%05d"``).
|
|
201
|
+
"""
|
|
202
|
+
_, h5path_data = lima_url_template.split("::")
|
|
203
|
+
lima_url_template_args = _get_lima_url_template_args(
|
|
204
|
+
scan_nb=scan_nb,
|
|
205
|
+
img_acq_device=detector,
|
|
206
|
+
collection_name=collection_name,
|
|
207
|
+
dataset_name=dataset_name,
|
|
208
|
+
scan_number_format=scan_number_format,
|
|
209
|
+
)
|
|
210
|
+
limadataset_params = {
|
|
211
|
+
"name": f"/{scan_nb}.1/instrument/{detector}/data", # this is dummy parameter
|
|
212
|
+
"dirname": str(Path(filename_dataset).parent),
|
|
213
|
+
"url_template": lima_url_template,
|
|
214
|
+
"url_template_args": lima_url_template_args,
|
|
215
|
+
}
|
|
216
|
+
if Version(version("blissdata")) < Version("1.1.0"):
|
|
217
|
+
for p in ("url_template", "url_template_args"):
|
|
218
|
+
limadataset_params.pop(p)
|
|
219
|
+
|
|
220
|
+
limadataset = LimaDataset(**limadataset_params)
|
|
221
|
+
limadataset.search()
|
|
222
|
+
urls_lima = []
|
|
223
|
+
for lima_file in limadataset._files:
|
|
224
|
+
if not Path(lima_file).is_file():
|
|
225
|
+
_logger.warning(
|
|
226
|
+
f"Lima file {lima_file} does not exist. Check the lima_url_template and lima_url_template_args, the Lima files may not be found."
|
|
227
|
+
)
|
|
228
|
+
continue
|
|
229
|
+
urls_lima.append(f"{_normalize_esrf_path(lima_file)}::{h5path_data}")
|
|
230
|
+
return urls_lima
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _validate_lima_urls(lima_urls: List[str]) -> None:
|
|
234
|
+
"""Check that the data path is an accessible dataset in each Lima file."""
|
|
235
|
+
for lima_url in lima_urls:
|
|
236
|
+
filename_lima, h5path_lima = lima_url.split("::")
|
|
237
|
+
with h5py_utils.File(filename=filename_lima, mode="r") as f:
|
|
238
|
+
if h5path_lima not in f:
|
|
239
|
+
raise KeyError(
|
|
240
|
+
f"Validation of Lima files failed: {h5path_lima} not found in {filename_lima}"
|
|
241
|
+
)
|
|
242
|
+
if not isinstance(f[h5path_lima], h5py.Dataset):
|
|
243
|
+
raise TypeError(
|
|
244
|
+
f"Validation of Lima files failed: {h5path_lima} is not a dataset in {filename_lima}"
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _validate_virtual_datasets(
|
|
249
|
+
dataset: h5py.Dataset,
|
|
250
|
+
nb_frames_to_validate: int = 10,
|
|
251
|
+
) -> None:
|
|
252
|
+
"""Check that the virtual dataset is readable and not all zeros (broken links)."""
|
|
253
|
+
try:
|
|
254
|
+
dataset_from_bliss_file = dataset[0:nb_frames_to_validate]
|
|
255
|
+
except OSError:
|
|
256
|
+
raise OSError(
|
|
257
|
+
f"Validation of Virtual datasets failed: could not read data from the dataset {dataset.name}. This may be due to broken links to Lima files."
|
|
258
|
+
)
|
|
259
|
+
if numpy.array_equal(
|
|
260
|
+
dataset_from_bliss_file, numpy.zeros_like(dataset_from_bliss_file)
|
|
261
|
+
):
|
|
262
|
+
raise ValueError(
|
|
263
|
+
f"Validation of Virtual datasets failed: data in {dataset.name} is all zeros, which points to a broken virtual dataset (broken links to Lima files)."
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _parse_scansubscan_entry(h5path_entry: str) -> Tuple[int, int]:
|
|
268
|
+
"""Return the ``(scan_nb, subscan_nb)`` integers of a Bliss root entry name (e.g. ``"2.1"``)."""
|
|
269
|
+
pattern_scan_subscan_entry = r"^(\d+)\.(\d+)$"
|
|
270
|
+
match_scan_entry = re.match(pattern_scan_subscan_entry, h5path_entry)
|
|
271
|
+
scan_nb = int(match_scan_entry.group(1))
|
|
272
|
+
subscan_nb = int(match_scan_entry.group(2))
|
|
273
|
+
return scan_nb, subscan_nb
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _find_bliss_dataset_files(
|
|
277
|
+
*args: List[Union[str, Path, ESRFPath]],
|
|
278
|
+
) -> List[ESRFPath]:
|
|
279
|
+
"""Return the sorted, deduplicated dataset master files described by ``args``.
|
|
280
|
+
|
|
281
|
+
Both files and directories are accepted: a file is taken as-is, while a directory is expanded
|
|
282
|
+
into the ``*.h5`` files it directly contains (not recursive). Anything that is neither an
|
|
283
|
+
existing file nor an existing directory is ignored.
|
|
284
|
+
|
|
285
|
+
Each resulting filename is normalized to an :class:`ESRFPath` and expanded according to its
|
|
286
|
+
template: a proposal file yields all dataset files of all its collections, a collection file
|
|
287
|
+
yields all dataset files of that collection, and a dataset file yields itself. Filenames
|
|
288
|
+
matching none of :data:`ALLOWED_ESRFPATH_TEMPLATES` are skipped with an error log.
|
|
289
|
+
"""
|
|
290
|
+
bliss_filenames = []
|
|
291
|
+
for item in args:
|
|
292
|
+
esrf_item = ESRFPath(item)
|
|
293
|
+
if esrf_item.is_file():
|
|
294
|
+
bliss_filenames.append(esrf_item)
|
|
295
|
+
elif esrf_item.is_dir():
|
|
296
|
+
bliss_filenames += sorted(esrf_item.glob("*.h5"))
|
|
297
|
+
|
|
298
|
+
filenames_datasets = []
|
|
299
|
+
for bliss_filename in bliss_filenames:
|
|
300
|
+
esrf_filename = _normalize_esrf_path(filename=bliss_filename)
|
|
301
|
+
if not esrf_filename:
|
|
302
|
+
continue
|
|
303
|
+
|
|
304
|
+
if esrf_filename.template_name not in _ALLOWED_ESRFPATH_TEMPLATES:
|
|
305
|
+
_logger.error(
|
|
306
|
+
f"{str(esrf_filename)} is a {esrf_filename.template_name} files are not allowed. Only {_ALLOWED_ESRFPATH_TEMPLATES}"
|
|
307
|
+
)
|
|
308
|
+
continue
|
|
309
|
+
if esrf_filename.template_name == _ESRF_TEMPLATE_PROPOSAL:
|
|
310
|
+
filenames_datasets += list(
|
|
311
|
+
esrf_filename.parent.replace_fields(collection="*", dataset="*").glob()
|
|
312
|
+
)
|
|
313
|
+
elif esrf_filename.template_name == _ESRF_TEMPLATE_COLLECTION:
|
|
314
|
+
filenames_datasets += list(
|
|
315
|
+
esrf_filename.parent.replace_fields(dataset="*").glob()
|
|
316
|
+
)
|
|
317
|
+
elif esrf_filename.template_name == _ESRF_TEMPLATE_DATASET:
|
|
318
|
+
filenames_datasets += [esrf_filename]
|
|
319
|
+
else:
|
|
320
|
+
_logger.debug("Unsupported HDF5 file name %r", esrf_filename)
|
|
321
|
+
return sorted(set(filenames_datasets))
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def find_lima_scans(
|
|
325
|
+
bliss_filenames: List[str],
|
|
326
|
+
detectors: Optional[List[str]] = None,
|
|
327
|
+
scans: Optional[List[int]] = None,
|
|
328
|
+
subscans: Optional[List[int]] = None,
|
|
329
|
+
lima_url_template: Optional[str] = None,
|
|
330
|
+
scan_number_format: Optional[str] = "%05d",
|
|
331
|
+
validate_data_lima_files: bool = False,
|
|
332
|
+
validate_data_virtual_datasets: bool = False,
|
|
333
|
+
nb_frames_to_validate: int = 10,
|
|
334
|
+
) -> List[LimaScan]:
|
|
335
|
+
"""
|
|
336
|
+
Access the given Bliss .h5 files and find the scans matching the requested scan and subscan numbers and detectors.
|
|
337
|
+
|
|
338
|
+
:param bliss_filenames: A list of HDF5 filenames (e.g. ``path/to/file.h5``), written by NexusWriter,
|
|
339
|
+
and/or directories containing such files (e.g. ``path/to/dir``, expanded to the ``*.h5``
|
|
340
|
+
files it directly contains, not recursive).
|
|
341
|
+
A proposal, collection or dataset file is accepted; anything else is skipped with an error log.
|
|
342
|
+
:param detectors: A list of detector names to filter the scans (e.g. ``["detector1", "detector2"]``). If None, all 2D detectors are considered.
|
|
343
|
+
:param scans: A list of scan numbers to filter the scans (e.g. ``[1, 2, 3]``). If None, all scans are considered.
|
|
344
|
+
:param subscans: A list of subscan numbers to filter the scans (e.g. ``[1, 2]``). If None, all subscans are considered.
|
|
345
|
+
:param lima_url_template: A template to find the data files. To be used if the data files don't follow the Lima format (e.g. Eiger hardware saving)
|
|
346
|
+
the template must be a format string with the following placeholders:
|
|
347
|
+
- {dirname}: the directory of the dataset master file
|
|
348
|
+
- {scan_nb}: the scan number integer, which will be formatted according to the parameter scan_number_format
|
|
349
|
+
- {img_acq_device}: the name of the detector
|
|
350
|
+
- {collection_name}: the collection name
|
|
351
|
+
- {dataset_name}: the dataset name
|
|
352
|
+
example: "{dirname}/scan{scan_nb}/{img_acq_device}/{collection_name}_{dataset_name}_{scan_nb}_data_{{file_index}}.h5::/entry_0000/measurement/data"
|
|
353
|
+
:param scan_number_format: The format to use to find the scan number in the lima_url_template (default is "%05d" for 5 digits with leading zeros)
|
|
354
|
+
:param validate_data_lima_files: Whether to validate if the data of the Lima files are accesible
|
|
355
|
+
:param validate_data_virtual_datasets: Whether to validate if the data of the Virtual datasets are accesible and not broken (e.g. all zeros).
|
|
356
|
+
The number of data frames in memory is controlled by `nb_frames_to_validate`
|
|
357
|
+
:param nb_frames_to_validate: The number of frames to load in memory for the validation of virtual datasets.
|
|
358
|
+
|
|
359
|
+
:raises KeyError: When ``validate_data_lima_files`` is enabled and the data path is missing in a Lima file.
|
|
360
|
+
:raises TypeError: When ``validate_data_lima_files`` is enabled and the data path is not a dataset in a Lima file.
|
|
361
|
+
:raises OSError: When ``validate_data_virtual_datasets`` is enabled and a virtual dataset cannot be read (broken links).
|
|
362
|
+
:raises ValueError: When ``validate_data_virtual_datasets`` is enabled and a virtual dataset only contains zeros (broken links).
|
|
363
|
+
|
|
364
|
+
:return: One :class:`LimaScan` object per matching scan/detector pair (empty list when nothing
|
|
365
|
+
matches), see :class:`LimaScan` for the description of each field.
|
|
366
|
+
"""
|
|
367
|
+
lima_scans = []
|
|
368
|
+
filenames_datasets = _find_bliss_dataset_files(*bliss_filenames)
|
|
369
|
+
for filename_dataset in filenames_datasets:
|
|
370
|
+
with h5py_utils.File(filename=filename_dataset, mode="r") as h5_bliss_root:
|
|
371
|
+
if not _validate_bliss_file(h5_bliss_root):
|
|
372
|
+
continue
|
|
373
|
+
|
|
374
|
+
for scan_entry_path in h5_bliss_root:
|
|
375
|
+
scan_nb, subscan_nb = _parse_scansubscan_entry(scan_entry_path)
|
|
376
|
+
if scans and scan_nb not in scans:
|
|
377
|
+
continue
|
|
378
|
+
if subscans and subscan_nb not in subscans:
|
|
379
|
+
continue
|
|
380
|
+
if "measurement" not in h5_bliss_root[scan_entry_path]:
|
|
381
|
+
continue
|
|
382
|
+
|
|
383
|
+
# Only for Daiquiri collected datasets
|
|
384
|
+
roi_id, subsample_id = _parse_daiquiri_roi(filename_dataset.dataset)
|
|
385
|
+
|
|
386
|
+
measurement_group = h5_bliss_root[f"{scan_entry_path}/measurement"]
|
|
387
|
+
if detectors:
|
|
388
|
+
detectors = [d for d in detectors if d in measurement_group]
|
|
389
|
+
else:
|
|
390
|
+
detectors = _discover_2d_detectors(measurement_group)
|
|
391
|
+
|
|
392
|
+
for detector in detectors:
|
|
393
|
+
# Data from virtual datasets
|
|
394
|
+
if validate_data_virtual_datasets:
|
|
395
|
+
_validate_virtual_datasets(
|
|
396
|
+
dataset=measurement_group[detector],
|
|
397
|
+
nb_frames_to_validate=nb_frames_to_validate,
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
# Search the lima files
|
|
401
|
+
if lima_url_template:
|
|
402
|
+
lima_urls = _get_lima_urls_from_template(
|
|
403
|
+
filename_dataset=str(filename_dataset),
|
|
404
|
+
lima_url_template=lima_url_template,
|
|
405
|
+
scan_nb=scan_nb,
|
|
406
|
+
detector=detector,
|
|
407
|
+
collection_name=filename_dataset.collection,
|
|
408
|
+
dataset_name=filename_dataset.dataset,
|
|
409
|
+
scan_number_format=scan_number_format,
|
|
410
|
+
)
|
|
411
|
+
else:
|
|
412
|
+
lima_urls = _get_lima_urls_from_virtual_sources(
|
|
413
|
+
measurement_group[detector]
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
if validate_data_lima_files:
|
|
417
|
+
_validate_lima_urls(lima_urls)
|
|
418
|
+
|
|
419
|
+
lima_scans.append(
|
|
420
|
+
LimaScan(
|
|
421
|
+
filename=str(filename_dataset),
|
|
422
|
+
url=f"{filename_dataset}::{scan_nb}.{subscan_nb}/measurement/{detector}",
|
|
423
|
+
collection_name=filename_dataset.collection,
|
|
424
|
+
dataset_name=filename_dataset.dataset,
|
|
425
|
+
scan_nb=scan_nb,
|
|
426
|
+
subscan_nb=subscan_nb,
|
|
427
|
+
sample_id=roi_id,
|
|
428
|
+
subsample_id=subsample_id,
|
|
429
|
+
detector_name=detector,
|
|
430
|
+
data_shape=measurement_group[detector].shape[1:],
|
|
431
|
+
nb_frames=measurement_group[detector].shape[0],
|
|
432
|
+
lima_urls=lima_urls,
|
|
433
|
+
lima_url_template=lima_url_template,
|
|
434
|
+
lima_url_template_args=(
|
|
435
|
+
_get_lima_url_template_args(
|
|
436
|
+
scan_nb=scan_nb,
|
|
437
|
+
img_acq_device=detector,
|
|
438
|
+
collection_name=filename_dataset.collection,
|
|
439
|
+
dataset_name=filename_dataset.dataset,
|
|
440
|
+
scan_number_format=scan_number_format,
|
|
441
|
+
)
|
|
442
|
+
if lima_url_template
|
|
443
|
+
else None
|
|
444
|
+
),
|
|
445
|
+
)
|
|
446
|
+
)
|
|
447
|
+
return lima_scans
|
|
@@ -190,7 +190,6 @@ def iter_bliss_scan_data_from_memory_slice(
|
|
|
190
190
|
|
|
191
191
|
restart_buffer = time.perf_counter()
|
|
192
192
|
while stream_on:
|
|
193
|
-
|
|
194
193
|
# While loop will stop unless one single stream is successfully sliced
|
|
195
194
|
stream_on = False
|
|
196
195
|
|
|
@@ -245,7 +244,6 @@ def iter_bliss_scan_data_from_memory_slice(
|
|
|
245
244
|
incoming_buffers[stream_name] = []
|
|
246
245
|
|
|
247
246
|
if not stream_on or ((time.perf_counter() - restart_buffer) > yield_timeout):
|
|
248
|
-
|
|
249
247
|
frames_to_yield = min(
|
|
250
248
|
[len(value) for value in non_yielded_buffers.values()]
|
|
251
249
|
)
|
|
@@ -271,7 +271,6 @@ def iter_bliss_scan_data_from_memory_slice(
|
|
|
271
271
|
|
|
272
272
|
restart_buffer = time.perf_counter()
|
|
273
273
|
while stream_on:
|
|
274
|
-
|
|
275
274
|
# While loop will stop unless one single stream is successfully sliced
|
|
276
275
|
stream_on = False
|
|
277
276
|
|
|
@@ -326,7 +325,6 @@ def iter_bliss_scan_data_from_memory_slice(
|
|
|
326
325
|
incoming_buffers[stream_name] = []
|
|
327
326
|
|
|
328
327
|
if not stream_on or ((time.perf_counter() - restart_buffer) > yield_timeout):
|
|
329
|
-
|
|
330
328
|
frames_to_yield = min(
|
|
331
329
|
[len(value) for value in non_yielded_buffers.values()]
|
|
332
330
|
)
|
|
@@ -55,9 +55,10 @@ def guess_data_shape(
|
|
|
55
55
|
detector_shape = tuple(n if n else DEFAULT_SCAN_DIM_SIZE for n in detector_shape)
|
|
56
56
|
data_shape = scan_shape + detector_shape
|
|
57
57
|
if max_shape:
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
if len(max_shape) != len(data_shape):
|
|
59
|
+
raise ValueError(
|
|
60
|
+
"HDF5 dataset shape must have the same dimensions as maxshape"
|
|
61
|
+
)
|
|
61
62
|
data_shape = tuple(
|
|
62
63
|
n1 if not n2 else max(n1, n2) for n1, n2 in zip(data_shape, max_shape)
|
|
63
64
|
)
|