blisswriter 1.0.0b0__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.
- blisswriter-1.0.0b0/PKG-INFO +46 -0
- blisswriter-1.0.0b0/README.md +18 -0
- blisswriter-1.0.0b0/pyproject.toml +3 -0
- blisswriter-1.0.0b0/setup.cfg +62 -0
- blisswriter-1.0.0b0/setup.py +4 -0
- blisswriter-1.0.0b0/src/blisswriter/__init__.py +4 -0
- blisswriter-1.0.0b0/src/blisswriter/app/__init__.py +0 -0
- blisswriter-1.0.0b0/src/blisswriter/app/process.py +44 -0
- blisswriter-1.0.0b0/src/blisswriter/app/register_service.py +210 -0
- blisswriter-1.0.0b0/src/blisswriter/app/service.py +54 -0
- blisswriter-1.0.0b0/src/blisswriter/app/utils.py +7 -0
- blisswriter-1.0.0b0/src/blisswriter/io/__init__.py +2 -0
- blisswriter-1.0.0b0/src/blisswriter/io/h5_config.py +175 -0
- blisswriter-1.0.0b0/src/blisswriter/io/h5_external.py +188 -0
- blisswriter-1.0.0b0/src/blisswriter/io/io_utils.py +108 -0
- blisswriter-1.0.0b0/src/blisswriter/io/nexus.py +1775 -0
- blisswriter-1.0.0b0/src/blisswriter/io/os_utils.py +58 -0
- blisswriter-1.0.0b0/src/blisswriter/nexus/__init__.py +1 -0
- blisswriter-1.0.0b0/src/blisswriter/nexus/devices.py +438 -0
- blisswriter-1.0.0b0/src/blisswriter/nexus/mapping.py +31 -0
- blisswriter-1.0.0b0/src/blisswriter/nexus/publish.py +368 -0
- blisswriter-1.0.0b0/src/blisswriter/nexus/scan_meta.py +18 -0
- blisswriter-1.0.0b0/src/blisswriter/parameters/__init__.py +37 -0
- blisswriter-1.0.0b0/src/blisswriter/parameters/base.py +136 -0
- blisswriter-1.0.0b0/src/blisswriter/parameters/config.py +15 -0
- blisswriter-1.0.0b0/src/blisswriter/patching/__init__.py +2 -0
- blisswriter-1.0.0b0/src/blisswriter/patching/monkey.py +43 -0
- blisswriter-1.0.0b0/src/blisswriter/patching/patch_h5py.py +120 -0
- blisswriter-1.0.0b0/src/blisswriter/patching/patch_isinstance.py +53 -0
- blisswriter-1.0.0b0/src/blisswriter/patching/patch_open.py +62 -0
- blisswriter-1.0.0b0/src/blisswriter/patching/patch_std.py +48 -0
- blisswriter-1.0.0b0/src/blisswriter/session_api.py +8 -0
- blisswriter-1.0.0b0/src/blisswriter/subscribers/__init__.py +0 -0
- blisswriter-1.0.0b0/src/blisswriter/subscribers/base_subscriber.py +209 -0
- blisswriter-1.0.0b0/src/blisswriter/subscribers/scan_subscriber.py +294 -0
- blisswriter-1.0.0b0/src/blisswriter/subscribers/session_subscriber.py +264 -0
- blisswriter-1.0.0b0/src/blisswriter/tango/__init__.py +2 -0
- blisswriter-1.0.0b0/src/blisswriter/tango/servers/NexusWriter.py +778 -0
- blisswriter-1.0.0b0/src/blisswriter/tango/servers/__init__.py +2 -0
- blisswriter-1.0.0b0/src/blisswriter/tests/__init__.py +0 -0
- blisswriter-1.0.0b0/src/blisswriter/tests/helpers/__init__.py +0 -0
- blisswriter-1.0.0b0/src/blisswriter/tests/helpers/nxw_test_math.py +28 -0
- blisswriter-1.0.0b0/src/blisswriter/tests/test_array_order.py +165 -0
- blisswriter-1.0.0b0/src/blisswriter/tests/test_base_subscriber.py +74 -0
- blisswriter-1.0.0b0/src/blisswriter/tests/test_data_merging.py +152 -0
- blisswriter-1.0.0b0/src/blisswriter/tests/test_dataset_proxy.py +391 -0
- blisswriter-1.0.0b0/src/blisswriter/tests/test_devices.py +38 -0
- blisswriter-1.0.0b0/src/blisswriter/tests/test_h5_external.py +111 -0
- blisswriter-1.0.0b0/src/blisswriter/tests/test_logging.py +365 -0
- blisswriter-1.0.0b0/src/blisswriter/tests/test_nexus.py +753 -0
- blisswriter-1.0.0b0/src/blisswriter/tests/test_reference_proxy.py +48 -0
- blisswriter-1.0.0b0/src/blisswriter/tests/test_rotating_file.py +20 -0
- blisswriter-1.0.0b0/src/blisswriter/tests/test_sync_utils.py +67 -0
- blisswriter-1.0.0b0/src/blisswriter/utils/__init__.py +2 -0
- blisswriter-1.0.0b0/src/blisswriter/utils/array_order.py +307 -0
- blisswriter-1.0.0b0/src/blisswriter/utils/array_size.py +32 -0
- blisswriter-1.0.0b0/src/blisswriter/utils/config_utils.py +90 -0
- blisswriter-1.0.0b0/src/blisswriter/utils/data_merging.py +455 -0
- blisswriter-1.0.0b0/src/blisswriter/utils/data_policy.py +23 -0
- blisswriter-1.0.0b0/src/blisswriter/utils/exceptions.py +11 -0
- blisswriter-1.0.0b0/src/blisswriter/utils/log_levels.py +123 -0
- blisswriter-1.0.0b0/src/blisswriter/utils/logging_utils.py +396 -0
- blisswriter-1.0.0b0/src/blisswriter/utils/periodic.py +18 -0
- blisswriter-1.0.0b0/src/blisswriter/utils/process_utils.py +544 -0
- blisswriter-1.0.0b0/src/blisswriter/utils/profiling.py +404 -0
- blisswriter-1.0.0b0/src/blisswriter/utils/scan_utils.py +57 -0
- blisswriter-1.0.0b0/src/blisswriter/utils/session_utils.py +53 -0
- blisswriter-1.0.0b0/src/blisswriter/utils/sync_utils.py +63 -0
- blisswriter-1.0.0b0/src/blisswriter/writer/__init__.py +0 -0
- blisswriter-1.0.0b0/src/blisswriter/writer/capture.py +21 -0
- blisswriter-1.0.0b0/src/blisswriter/writer/datasets.py +1120 -0
- blisswriter-1.0.0b0/src/blisswriter/writer/info.py +519 -0
- blisswriter-1.0.0b0/src/blisswriter/writer/items.py +273 -0
- blisswriter-1.0.0b0/src/blisswriter/writer/main.py +457 -0
- blisswriter-1.0.0b0/src/blisswriter/writer/plots.py +262 -0
- blisswriter-1.0.0b0/src/blisswriter/writer/references.py +80 -0
- blisswriter-1.0.0b0/src/blisswriter/writer/scan.py +1200 -0
- blisswriter-1.0.0b0/src/blisswriter/writer/scan_url_info.py +83 -0
- blisswriter-1.0.0b0/src/blisswriter/writer/types.py +15 -0
- blisswriter-1.0.0b0/src/blisswriter/writer/typing.py +12 -0
- blisswriter-1.0.0b0/src/blisswriter.egg-info/PKG-INFO +46 -0
- blisswriter-1.0.0b0/src/blisswriter.egg-info/SOURCES.txt +85 -0
- blisswriter-1.0.0b0/src/blisswriter.egg-info/dependency_links.txt +1 -0
- blisswriter-1.0.0b0/src/blisswriter.egg-info/entry_points.txt +4 -0
- blisswriter-1.0.0b0/src/blisswriter.egg-info/requires.txt +15 -0
- blisswriter-1.0.0b0/src/blisswriter.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: blisswriter
|
|
3
|
+
Version: 1.0.0b0
|
|
4
|
+
Summary: Bliss NeXus-compliant HDF5 file writer
|
|
5
|
+
Home-page: https://gitlab.esrf.fr/bliss/bliss/-/tree/master/blisswriter
|
|
6
|
+
Author: BCU (ESRF)
|
|
7
|
+
License: LGPLv3
|
|
8
|
+
Project-URL: Source, https://gitlab.esrf.fr/bliss/bliss/-/tree/master/blisswriter
|
|
9
|
+
Project-URL: Documentation, https://bliss.gitlab-pages.esrf.fr/bliss/
|
|
10
|
+
Project-URL: Tracker, https://gitlab.esrf.fr/bliss/bliss/issues/
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Requires-Python: >=3.7
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
Requires-Dist: blissdata>=1
|
|
17
|
+
Requires-Dist: pyicat-plus
|
|
18
|
+
Provides-Extra: tango
|
|
19
|
+
Requires-Dist: blissdata[tango]; extra == "tango"
|
|
20
|
+
Provides-Extra: test
|
|
21
|
+
Requires-Dist: blissdata[tango]; extra == "test"
|
|
22
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: blissdata[tango]; extra == "dev"
|
|
25
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
26
|
+
Requires-Dist: black>=22; extra == "dev"
|
|
27
|
+
Requires-Dist: flake8>=4; extra == "dev"
|
|
28
|
+
|
|
29
|
+
Bliss Writer
|
|
30
|
+
============
|
|
31
|
+
|
|
32
|
+
The `blisswriter` project provides NeXus-compliant HDF5 file writing based on
|
|
33
|
+
data published by `blissdata`. It can be used in three ways
|
|
34
|
+
|
|
35
|
+
* writer service for a single `bliss` session as a *process*
|
|
36
|
+
* command line interface: `NexusSessionWriter`
|
|
37
|
+
* consumer of Redis data published by `blissdata`
|
|
38
|
+
* writer service for a single `bliss` session as a *Tango device*
|
|
39
|
+
* command line interface: `NexusWriterService`
|
|
40
|
+
* consumer of Redis data published by `blissdata`
|
|
41
|
+
* writer python API
|
|
42
|
+
* python API: `from blisswriter.writer.main import ScanWriterWithState`
|
|
43
|
+
* same API used by the writer services
|
|
44
|
+
|
|
45
|
+
`blisswriter` is developed and maintained by the [Software group](https://www.esrf.fr/Instrumentation/software)
|
|
46
|
+
of the [European Synchrotron](https://www.esrf.fr/).
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Bliss Writer
|
|
2
|
+
============
|
|
3
|
+
|
|
4
|
+
The `blisswriter` project provides NeXus-compliant HDF5 file writing based on
|
|
5
|
+
data published by `blissdata`. It can be used in three ways
|
|
6
|
+
|
|
7
|
+
* writer service for a single `bliss` session as a *process*
|
|
8
|
+
* command line interface: `NexusSessionWriter`
|
|
9
|
+
* consumer of Redis data published by `blissdata`
|
|
10
|
+
* writer service for a single `bliss` session as a *Tango device*
|
|
11
|
+
* command line interface: `NexusWriterService`
|
|
12
|
+
* consumer of Redis data published by `blissdata`
|
|
13
|
+
* writer python API
|
|
14
|
+
* python API: `from blisswriter.writer.main import ScanWriterWithState`
|
|
15
|
+
* same API used by the writer services
|
|
16
|
+
|
|
17
|
+
`blisswriter` is developed and maintained by the [Software group](https://www.esrf.fr/Instrumentation/software)
|
|
18
|
+
of the [European Synchrotron](https://www.esrf.fr/).
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
[metadata]
|
|
2
|
+
name = blisswriter
|
|
3
|
+
version = attr: blisswriter.__version__
|
|
4
|
+
author = BCU (ESRF)
|
|
5
|
+
description = Bliss NeXus-compliant HDF5 file writer
|
|
6
|
+
long_description = file: README.md
|
|
7
|
+
long_description_content_type = text/markdown
|
|
8
|
+
license = LGPLv3
|
|
9
|
+
url = https://gitlab.esrf.fr/bliss/bliss/-/tree/master/blisswriter
|
|
10
|
+
project_urls =
|
|
11
|
+
Source = https://gitlab.esrf.fr/bliss/bliss/-/tree/master/blisswriter
|
|
12
|
+
Documentation = https://bliss.gitlab-pages.esrf.fr/bliss/
|
|
13
|
+
Tracker = https://gitlab.esrf.fr/bliss/bliss/issues/
|
|
14
|
+
classifiers =
|
|
15
|
+
Intended Audience :: Science/Research
|
|
16
|
+
License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)
|
|
17
|
+
Programming Language :: Python :: 3
|
|
18
|
+
|
|
19
|
+
[options]
|
|
20
|
+
package_dir =
|
|
21
|
+
=src
|
|
22
|
+
packages = find:
|
|
23
|
+
python_requires = >=3.7
|
|
24
|
+
install_requires =
|
|
25
|
+
blissdata >=1
|
|
26
|
+
pyicat-plus
|
|
27
|
+
|
|
28
|
+
[options.packages.find]
|
|
29
|
+
where = src
|
|
30
|
+
|
|
31
|
+
[options.entry_points]
|
|
32
|
+
console_scripts =
|
|
33
|
+
NexusWriterService = blisswriter.app.service:main
|
|
34
|
+
NexusSessionWriter = blisswriter.app.process:main
|
|
35
|
+
RegisterNexusWriter = blisswriter.app.register_service:main
|
|
36
|
+
|
|
37
|
+
[options.extras_require]
|
|
38
|
+
tango =
|
|
39
|
+
blissdata[tango]
|
|
40
|
+
test =
|
|
41
|
+
%(tango)s
|
|
42
|
+
pytest >=7
|
|
43
|
+
dev =
|
|
44
|
+
%(test)s
|
|
45
|
+
black >=22
|
|
46
|
+
flake8 >=4
|
|
47
|
+
|
|
48
|
+
[flake8]
|
|
49
|
+
ignore = E501, E203, W503
|
|
50
|
+
max-line-length = 88
|
|
51
|
+
exclude =
|
|
52
|
+
.eggs
|
|
53
|
+
|
|
54
|
+
[coverage:run]
|
|
55
|
+
omit =
|
|
56
|
+
setup.py
|
|
57
|
+
*/tests/*
|
|
58
|
+
|
|
59
|
+
[egg_info]
|
|
60
|
+
tag_build =
|
|
61
|
+
tag_date = 0
|
|
62
|
+
|
|
File without changes
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from blisswriter.parameters import all_cli_saveoptions
|
|
3
|
+
from blisswriter.subscribers.session_subscriber import NexusSessionSubscriber
|
|
4
|
+
from blisswriter.utils import logging_utils
|
|
5
|
+
from .utils import config_root_logger
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main(argv=None):
|
|
9
|
+
import argparse
|
|
10
|
+
|
|
11
|
+
if argv is None:
|
|
12
|
+
argv = sys.argv
|
|
13
|
+
|
|
14
|
+
parser = argparse.ArgumentParser(
|
|
15
|
+
description="Start python a process for saving Bliss data in HDF5"
|
|
16
|
+
)
|
|
17
|
+
parser.add_argument("session_name", type=str, help="Session name")
|
|
18
|
+
cli_saveoptions = all_cli_saveoptions()
|
|
19
|
+
for attr, okwargs in cli_saveoptions.items():
|
|
20
|
+
parser.add_argument("--" + attr, **okwargs)
|
|
21
|
+
logging_utils.add_cli_args(parser)
|
|
22
|
+
|
|
23
|
+
# Parse CLI arguments
|
|
24
|
+
args = parser.parse_args(argv[1:])
|
|
25
|
+
config_root_logger(args)
|
|
26
|
+
kwargs = {}
|
|
27
|
+
cli_saveoptions = all_cli_saveoptions(configurable=args.configurable)
|
|
28
|
+
for attr, okwargs in cli_saveoptions.items():
|
|
29
|
+
option = okwargs["dest"]
|
|
30
|
+
try:
|
|
31
|
+
kwargs[option] = getattr(args, option)
|
|
32
|
+
except AttributeError:
|
|
33
|
+
continue
|
|
34
|
+
|
|
35
|
+
# Launch the session writer
|
|
36
|
+
subscriber = NexusSessionSubscriber(
|
|
37
|
+
args.session_name, writer_name="external", **kwargs
|
|
38
|
+
)
|
|
39
|
+
subscriber.start()
|
|
40
|
+
subscriber.join()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
if __name__ == "__main__":
|
|
44
|
+
sys.exit(main())
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from tango import DeviceProxy, DevFailed, Database, DbDevInfo
|
|
6
|
+
|
|
7
|
+
from ..utils import logging_utils
|
|
8
|
+
from .utils import config_root_logger
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
WRITER_CLASS = "NexusWriter"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def beamline():
|
|
17
|
+
"""
|
|
18
|
+
:returns str:
|
|
19
|
+
"""
|
|
20
|
+
name = "id00"
|
|
21
|
+
for k in "BEAMLINENAME", "BEAMLINE":
|
|
22
|
+
name = os.environ.get(k, name)
|
|
23
|
+
return name.lower()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def find_session_writers(session_name, db=None):
|
|
27
|
+
"""
|
|
28
|
+
Find all TANGO devices of class NexusWriter listening to a particular BLISS session.
|
|
29
|
+
|
|
30
|
+
:param std session_name:
|
|
31
|
+
:param Database db:
|
|
32
|
+
:returns list(str): device names
|
|
33
|
+
"""
|
|
34
|
+
if db is None:
|
|
35
|
+
db = Database()
|
|
36
|
+
writers = []
|
|
37
|
+
for obj_name in db.get_device_name("*", WRITER_CLASS):
|
|
38
|
+
prop = db.get_device_property(obj_name, "session")["session"]
|
|
39
|
+
if prop:
|
|
40
|
+
if session_name == prop[0]:
|
|
41
|
+
writers.append(obj_name)
|
|
42
|
+
logger.info(
|
|
43
|
+
"Registered writers for session {}: {}".format(repr(session_name), writers)
|
|
44
|
+
)
|
|
45
|
+
return writers
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def find_session_writer(session_name, db=None):
|
|
49
|
+
"""
|
|
50
|
+
Find TANGO device of class NexusWriter listening to a particular BLISS session.
|
|
51
|
+
|
|
52
|
+
:param std session_name:
|
|
53
|
+
:param Database db:
|
|
54
|
+
:returns str or None: device name
|
|
55
|
+
:raises RuntimeError: more than one writer listening to session
|
|
56
|
+
"""
|
|
57
|
+
writers = find_session_writers(session_name, db=db)
|
|
58
|
+
if not writers:
|
|
59
|
+
return None
|
|
60
|
+
if len(writers) > 1:
|
|
61
|
+
raise ValueError(
|
|
62
|
+
"Found more than one writer for session {}: {}".format(
|
|
63
|
+
repr(session_name), writers
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
return writers[0]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def get_uri(p):
|
|
70
|
+
return "tango://{}:{}/{}".format(p.get_db_host(), p.get_db_port(), p.dev_name())
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def ensure_existence(
|
|
74
|
+
session_name,
|
|
75
|
+
server="nexuswriter",
|
|
76
|
+
instance=None,
|
|
77
|
+
domain=None,
|
|
78
|
+
family="bliss_nxwriter",
|
|
79
|
+
member=None,
|
|
80
|
+
use_existing=True,
|
|
81
|
+
):
|
|
82
|
+
"""
|
|
83
|
+
Find or register TANGO device of class NexusWriter
|
|
84
|
+
|
|
85
|
+
:param str session_name:
|
|
86
|
+
:param str server: device server name
|
|
87
|
+
:param str instance: device server instance (Default: `session_name`)
|
|
88
|
+
:param str domain: location of device
|
|
89
|
+
:param str family: type of device
|
|
90
|
+
:param str member: device name (Default: `session_name`)
|
|
91
|
+
:param bool use_existing: use an existing writer for this session (if any)
|
|
92
|
+
:returns DeviceProxy:
|
|
93
|
+
"""
|
|
94
|
+
db = Database()
|
|
95
|
+
if not member:
|
|
96
|
+
member = session_name
|
|
97
|
+
if not domain:
|
|
98
|
+
domain = beamline()
|
|
99
|
+
if not instance:
|
|
100
|
+
instance = session_name
|
|
101
|
+
dev_name = "/".join([domain, family, member])
|
|
102
|
+
if use_existing:
|
|
103
|
+
pdev_name = find_session_writer(session_name, db=db)
|
|
104
|
+
if pdev_name:
|
|
105
|
+
proxy = DeviceProxy(pdev_name)
|
|
106
|
+
msg = "{} already registered".format(repr(get_uri(proxy)))
|
|
107
|
+
if dev_name == pdev_name:
|
|
108
|
+
logger.info(msg)
|
|
109
|
+
else:
|
|
110
|
+
logger.warning(msg)
|
|
111
|
+
return proxy
|
|
112
|
+
return register(session_name, dev_name, server=server, instance=instance)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def register(session_name, dev_name, server="nexuswriter", instance="nexuswriters"):
|
|
116
|
+
"""
|
|
117
|
+
Register TANGO device of class NexusWriter
|
|
118
|
+
|
|
119
|
+
:param str session_name:
|
|
120
|
+
:param str dev_name: for example id00/bliss_nxwriter/test_session
|
|
121
|
+
:param str instance: device server instance
|
|
122
|
+
:returns DeviceProxy:
|
|
123
|
+
"""
|
|
124
|
+
# Register proxy is not already registered
|
|
125
|
+
try:
|
|
126
|
+
proxy = DeviceProxy(dev_name)
|
|
127
|
+
logger.info("{} already registered".format(repr(get_uri(proxy))))
|
|
128
|
+
except DevFailed:
|
|
129
|
+
db = Database()
|
|
130
|
+
dev_info = DbDevInfo()
|
|
131
|
+
dev_info.name = dev_name
|
|
132
|
+
dev_info._class = WRITER_CLASS
|
|
133
|
+
server = "/".join([server, instance])
|
|
134
|
+
dev_info.server = server
|
|
135
|
+
db.add_device(dev_info)
|
|
136
|
+
proxy = DeviceProxy(dev_name)
|
|
137
|
+
logger.info("{} registered".format(repr(get_uri(proxy))))
|
|
138
|
+
proxy.put_property({"session": session_name})
|
|
139
|
+
try:
|
|
140
|
+
session = proxy.get_property("session")["session"][0]
|
|
141
|
+
except (IndexError, KeyError):
|
|
142
|
+
session = ""
|
|
143
|
+
if session != session_name:
|
|
144
|
+
raise RuntimeError(
|
|
145
|
+
"{} is listening to Bliss session {} instead of {}".format(
|
|
146
|
+
repr(get_uri(proxy)), repr(session), repr(session_name)
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
return proxy
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def main(argv=None):
|
|
153
|
+
import argparse
|
|
154
|
+
|
|
155
|
+
if argv is None:
|
|
156
|
+
argv = sys.argv
|
|
157
|
+
|
|
158
|
+
parser = argparse.ArgumentParser(
|
|
159
|
+
description="Register a Tango device saving Bliss data in HDF5"
|
|
160
|
+
)
|
|
161
|
+
parser.add_argument("session", type=str, help="Bliss session name")
|
|
162
|
+
parser.add_argument(
|
|
163
|
+
"--server",
|
|
164
|
+
type=str,
|
|
165
|
+
default="nexuswriter",
|
|
166
|
+
help="Server name ('nexuswriter' by default)",
|
|
167
|
+
)
|
|
168
|
+
parser.add_argument(
|
|
169
|
+
"--instance",
|
|
170
|
+
type=str,
|
|
171
|
+
default="",
|
|
172
|
+
help="Server instance name (session name by default)",
|
|
173
|
+
)
|
|
174
|
+
parser.add_argument(
|
|
175
|
+
"--domain",
|
|
176
|
+
type=str,
|
|
177
|
+
default="",
|
|
178
|
+
help="Device domain name (checks environment or 'id00' by default)",
|
|
179
|
+
)
|
|
180
|
+
parser.add_argument(
|
|
181
|
+
"--family",
|
|
182
|
+
type=str,
|
|
183
|
+
default="bliss_nxwriter",
|
|
184
|
+
help="Device family name ('bliss_nxwriter' by default)",
|
|
185
|
+
)
|
|
186
|
+
parser.add_argument(
|
|
187
|
+
"--member", type=str, default="", help="Device name (session name by default)"
|
|
188
|
+
)
|
|
189
|
+
parser.add_argument(
|
|
190
|
+
"--ignore_existing",
|
|
191
|
+
action="store_false",
|
|
192
|
+
dest="use_existing",
|
|
193
|
+
help="Ignore existing writer for this session",
|
|
194
|
+
)
|
|
195
|
+
logging_utils.add_cli_args(parser)
|
|
196
|
+
args = parser.parse_args(argv[1:])
|
|
197
|
+
config_root_logger(args)
|
|
198
|
+
ensure_existence(
|
|
199
|
+
args.session,
|
|
200
|
+
server=args.server,
|
|
201
|
+
instance=args.instance,
|
|
202
|
+
domain=args.domain,
|
|
203
|
+
family=args.family,
|
|
204
|
+
member=args.member,
|
|
205
|
+
use_existing=args.use_existing,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
if __name__ == "__main__":
|
|
210
|
+
sys.exit(main())
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from blisswriter.tango.servers import NexusWriter
|
|
3
|
+
from blisswriter.utils.logging_utils import add_cli_args
|
|
4
|
+
from blisswriter.utils.log_levels import tango_cli_slog_level
|
|
5
|
+
from blisswriter.utils.log_levels import add_tango_cli_args
|
|
6
|
+
from .utils import config_root_logger
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def run(server, instance, log_level):
|
|
10
|
+
"""
|
|
11
|
+
:param str server: device server name
|
|
12
|
+
:param str instance: device server instance name
|
|
13
|
+
:param str log_level:
|
|
14
|
+
:returns Util:
|
|
15
|
+
"""
|
|
16
|
+
verbose = tango_cli_slog_level.get(log_level, 0)
|
|
17
|
+
if verbose:
|
|
18
|
+
verbose = "-v{:d}".format(verbose)
|
|
19
|
+
serverargs = [server, instance, verbose]
|
|
20
|
+
else:
|
|
21
|
+
serverargs = [server, instance]
|
|
22
|
+
return NexusWriter.main(args=serverargs)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def main(argv=None):
|
|
26
|
+
import argparse
|
|
27
|
+
|
|
28
|
+
if argv is None:
|
|
29
|
+
argv = sys.argv
|
|
30
|
+
|
|
31
|
+
parser = argparse.ArgumentParser(
|
|
32
|
+
description="Start a Tango server for saving Bliss data in HDF5"
|
|
33
|
+
)
|
|
34
|
+
parser.add_argument(
|
|
35
|
+
"instance",
|
|
36
|
+
type=str,
|
|
37
|
+
default="nexuswriters",
|
|
38
|
+
help="Server instance name ('nexuswriters' by default)",
|
|
39
|
+
)
|
|
40
|
+
parser.add_argument(
|
|
41
|
+
"--server",
|
|
42
|
+
type=str,
|
|
43
|
+
default="nexuswriter",
|
|
44
|
+
help="Server name ('nexuswriter' by default)",
|
|
45
|
+
)
|
|
46
|
+
add_cli_args(parser)
|
|
47
|
+
add_tango_cli_args(parser)
|
|
48
|
+
args = parser.parse_args(argv[1:])
|
|
49
|
+
config_root_logger(args)
|
|
50
|
+
run(args.server, args.instance, args.log_tango)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
if __name__ == "__main__":
|
|
54
|
+
sys.exit(main())
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""HDF5 configuration of optimal data storage (IO speed, compression, ...)
|
|
2
|
+
"""
|
|
3
|
+
|
|
4
|
+
from typing import Optional, Tuple
|
|
5
|
+
from numbers import Integral
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
|
|
8
|
+
import numpy
|
|
9
|
+
from numpy.typing import DTypeLike
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
import hdf5plugin
|
|
13
|
+
except ImportError:
|
|
14
|
+
hdf5plugin = None
|
|
15
|
+
|
|
16
|
+
from ..utils.array_size import ShapeType
|
|
17
|
+
from ..utils import array_size
|
|
18
|
+
|
|
19
|
+
VarShapeType = Tuple[array_size.Integral] # <=0 marks a variable dimension
|
|
20
|
+
VarH5pyShapeType = Tuple[
|
|
21
|
+
Optional[array_size.StrictPositiveIntegral]
|
|
22
|
+
] # None marks a variable dimension
|
|
23
|
+
|
|
24
|
+
DEFAULT_CHUNK_NBYTES = 1 << 20
|
|
25
|
+
DEFAULT_COMPRESSION_LIMIT_NBYTES = 1 << 20
|
|
26
|
+
DEFAULT_CHUNK_SPLIT = 4
|
|
27
|
+
DEFAULT_COMPRESSION_SCHEME = "gzip-byteshuffle"
|
|
28
|
+
|
|
29
|
+
# Default data size is 2 MB for a 1D detector
|
|
30
|
+
# and 2 KB for a 0D detector:
|
|
31
|
+
DEFAULT_SCAN_DIM_SIZE = 512
|
|
32
|
+
DEFAULT_DETECTOR_DIM_SIZE = 1024
|
|
33
|
+
DEFAULT_DTYPE = numpy.int32
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def guess_data_shape(
|
|
37
|
+
scan_shape: VarShapeType,
|
|
38
|
+
detector_shape: VarShapeType,
|
|
39
|
+
max_shape: Optional[VarH5pyShapeType],
|
|
40
|
+
) -> ShapeType:
|
|
41
|
+
scan_shape = tuple(n if n > 0 else DEFAULT_SCAN_DIM_SIZE for n in scan_shape)
|
|
42
|
+
detector_shape = tuple(
|
|
43
|
+
n if n > 0 else DEFAULT_SCAN_DIM_SIZE for n in detector_shape
|
|
44
|
+
)
|
|
45
|
+
data_shape = scan_shape + detector_shape
|
|
46
|
+
if max_shape:
|
|
47
|
+
assert len(max_shape) == len(
|
|
48
|
+
data_shape
|
|
49
|
+
), "HDF5 dataset shape must have the same dimensions as maxshape"
|
|
50
|
+
data_shape = tuple(
|
|
51
|
+
n1 if not n2 else max(n1, n2) for n1, n2 in zip(data_shape, max_shape)
|
|
52
|
+
)
|
|
53
|
+
return data_shape
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def guess_chunk_shape(
|
|
57
|
+
data_shape: ShapeType,
|
|
58
|
+
dtype: DTypeLike,
|
|
59
|
+
chunk_split: Optional[Integral] = None,
|
|
60
|
+
chunk_nbytes: Optional[Integral] = None,
|
|
61
|
+
) -> Optional[ShapeType]:
|
|
62
|
+
"""Try to guess the optimal chunk shape with these constraints:
|
|
63
|
+
* Split any dimension for partial access
|
|
64
|
+
* Below the maximal chunk size (1 MB by default, uncompressed)
|
|
65
|
+
|
|
66
|
+
The inner-most dimensions are split in `chunk_split` parts
|
|
67
|
+
until chunk_nbytes is reached. The chunk size in the outer
|
|
68
|
+
dimensions will be 1, unless the data size is to small.
|
|
69
|
+
"""
|
|
70
|
+
if chunk_nbytes is None:
|
|
71
|
+
chunk_nbytes = DEFAULT_CHUNK_NBYTES
|
|
72
|
+
if chunk_split is None:
|
|
73
|
+
chunk_split = DEFAULT_CHUNK_SPLIT
|
|
74
|
+
itemsize = array_size.dtype_nbytes(dtype)
|
|
75
|
+
size = array_size.shape_to_size(data_shape)
|
|
76
|
+
nbytes = size * itemsize
|
|
77
|
+
if nbytes <= chunk_nbytes:
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
max_size = chunk_nbytes // itemsize
|
|
81
|
+
current_size = 1
|
|
82
|
+
chunk_shape = []
|
|
83
|
+
for n_i in data_shape[-1::-1]:
|
|
84
|
+
if current_size >= max_size:
|
|
85
|
+
c_i = 1
|
|
86
|
+
else:
|
|
87
|
+
a = int(numpy.ceil(n_i / chunk_split))
|
|
88
|
+
b = int(numpy.ceil(max_size / current_size))
|
|
89
|
+
c_i = min(a, b)
|
|
90
|
+
chunk_shape.append(c_i)
|
|
91
|
+
current_size *= c_i
|
|
92
|
+
chunk_shape = tuple(chunk_shape[::-1])
|
|
93
|
+
if chunk_shape == data_shape:
|
|
94
|
+
return None
|
|
95
|
+
return chunk_shape
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def guess_compression(
|
|
99
|
+
data_shape: ShapeType,
|
|
100
|
+
dtype: DTypeLike,
|
|
101
|
+
compression_limit_nbytes: Optional[Integral] = None,
|
|
102
|
+
) -> bool:
|
|
103
|
+
"""Compression is needed when the total data size exceeds the limits (1 MB by default)."""
|
|
104
|
+
if compression_limit_nbytes is None:
|
|
105
|
+
compression_limit_nbytes = DEFAULT_COMPRESSION_LIMIT_NBYTES
|
|
106
|
+
nbytes = array_size.shape_to_nbytes(data_shape, dtype)
|
|
107
|
+
return nbytes > compression_limit_nbytes
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def get_compression_arguments(compression_scheme: Optional[str] = None) -> Mapping:
|
|
111
|
+
if compression_scheme:
|
|
112
|
+
compression_scheme = compression_scheme.lower()
|
|
113
|
+
if compression_scheme is None:
|
|
114
|
+
compression_scheme = DEFAULT_COMPRESSION_SCHEME
|
|
115
|
+
if compression_scheme == "none":
|
|
116
|
+
return dict()
|
|
117
|
+
elif compression_scheme == "gzip":
|
|
118
|
+
return {"compression": "gzip"}
|
|
119
|
+
elif compression_scheme == "byteshuffle":
|
|
120
|
+
return {"shuffle": True}
|
|
121
|
+
elif compression_scheme == "gzip-byteshuffle":
|
|
122
|
+
return {"compression": "gzip", "shuffle": True}
|
|
123
|
+
elif compression_scheme == "bitshuffle":
|
|
124
|
+
if hdf5plugin is None:
|
|
125
|
+
raise RuntimeError(
|
|
126
|
+
"Writer does not support HDF5 'bitshuffle' compression. Install the hdf5plugin library"
|
|
127
|
+
)
|
|
128
|
+
return hdf5plugin.Bitshuffle(nelems=0, lz4=False)
|
|
129
|
+
elif compression_scheme == "lz4-bitshuffle":
|
|
130
|
+
if hdf5plugin is None:
|
|
131
|
+
raise RuntimeError(
|
|
132
|
+
"Writer does not support HDF5 'bitshuffle' compression. Install the hdf5plugin library"
|
|
133
|
+
)
|
|
134
|
+
return hdf5plugin.Bitshuffle(nelems=0, lz4=True)
|
|
135
|
+
else:
|
|
136
|
+
raise ValueError(f"Unknown HDF5 compression '{compression_scheme}'")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def guess_dataset_config(
|
|
140
|
+
scan_shape: VarShapeType,
|
|
141
|
+
detector_shape: VarShapeType,
|
|
142
|
+
dtype: Optional[DTypeLike] = None,
|
|
143
|
+
chunk_split: Optional[Integral] = None,
|
|
144
|
+
chunk_nbytes: Optional[Integral] = None,
|
|
145
|
+
compression_limit_nbytes: Optional[Integral] = None,
|
|
146
|
+
compression_scheme: Optional[str] = None,
|
|
147
|
+
max_shape: Optional[VarH5pyShapeType] = None,
|
|
148
|
+
) -> dict:
|
|
149
|
+
"""Dataset configuration passed the `h5py.Group.create_dataset` for optimal
|
|
150
|
+
storage (IO speed, compression, ...)
|
|
151
|
+
"""
|
|
152
|
+
data_shape = guess_data_shape(
|
|
153
|
+
scan_shape=scan_shape, detector_shape=detector_shape, max_shape=max_shape
|
|
154
|
+
)
|
|
155
|
+
if dtype is None:
|
|
156
|
+
dtype = DEFAULT_DTYPE
|
|
157
|
+
chunk_shape = guess_chunk_shape(
|
|
158
|
+
data_shape=data_shape,
|
|
159
|
+
dtype=dtype,
|
|
160
|
+
chunk_split=chunk_split,
|
|
161
|
+
chunk_nbytes=chunk_nbytes,
|
|
162
|
+
)
|
|
163
|
+
config = {"chunks": chunk_shape}
|
|
164
|
+
compression = guess_compression(
|
|
165
|
+
data_shape=data_shape,
|
|
166
|
+
dtype=dtype,
|
|
167
|
+
compression_limit_nbytes=compression_limit_nbytes,
|
|
168
|
+
)
|
|
169
|
+
if compression:
|
|
170
|
+
config.update(get_compression_arguments(compression_scheme=compression_scheme))
|
|
171
|
+
chunking_required = compression or max_shape is not None
|
|
172
|
+
if chunking_required and chunk_shape is None:
|
|
173
|
+
# Do not let h5py guess the chunk size
|
|
174
|
+
config["chunks"] = data_shape
|
|
175
|
+
return config
|