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.
Files changed (86) hide show
  1. blisswriter-1.0.0b0/PKG-INFO +46 -0
  2. blisswriter-1.0.0b0/README.md +18 -0
  3. blisswriter-1.0.0b0/pyproject.toml +3 -0
  4. blisswriter-1.0.0b0/setup.cfg +62 -0
  5. blisswriter-1.0.0b0/setup.py +4 -0
  6. blisswriter-1.0.0b0/src/blisswriter/__init__.py +4 -0
  7. blisswriter-1.0.0b0/src/blisswriter/app/__init__.py +0 -0
  8. blisswriter-1.0.0b0/src/blisswriter/app/process.py +44 -0
  9. blisswriter-1.0.0b0/src/blisswriter/app/register_service.py +210 -0
  10. blisswriter-1.0.0b0/src/blisswriter/app/service.py +54 -0
  11. blisswriter-1.0.0b0/src/blisswriter/app/utils.py +7 -0
  12. blisswriter-1.0.0b0/src/blisswriter/io/__init__.py +2 -0
  13. blisswriter-1.0.0b0/src/blisswriter/io/h5_config.py +175 -0
  14. blisswriter-1.0.0b0/src/blisswriter/io/h5_external.py +188 -0
  15. blisswriter-1.0.0b0/src/blisswriter/io/io_utils.py +108 -0
  16. blisswriter-1.0.0b0/src/blisswriter/io/nexus.py +1775 -0
  17. blisswriter-1.0.0b0/src/blisswriter/io/os_utils.py +58 -0
  18. blisswriter-1.0.0b0/src/blisswriter/nexus/__init__.py +1 -0
  19. blisswriter-1.0.0b0/src/blisswriter/nexus/devices.py +438 -0
  20. blisswriter-1.0.0b0/src/blisswriter/nexus/mapping.py +31 -0
  21. blisswriter-1.0.0b0/src/blisswriter/nexus/publish.py +368 -0
  22. blisswriter-1.0.0b0/src/blisswriter/nexus/scan_meta.py +18 -0
  23. blisswriter-1.0.0b0/src/blisswriter/parameters/__init__.py +37 -0
  24. blisswriter-1.0.0b0/src/blisswriter/parameters/base.py +136 -0
  25. blisswriter-1.0.0b0/src/blisswriter/parameters/config.py +15 -0
  26. blisswriter-1.0.0b0/src/blisswriter/patching/__init__.py +2 -0
  27. blisswriter-1.0.0b0/src/blisswriter/patching/monkey.py +43 -0
  28. blisswriter-1.0.0b0/src/blisswriter/patching/patch_h5py.py +120 -0
  29. blisswriter-1.0.0b0/src/blisswriter/patching/patch_isinstance.py +53 -0
  30. blisswriter-1.0.0b0/src/blisswriter/patching/patch_open.py +62 -0
  31. blisswriter-1.0.0b0/src/blisswriter/patching/patch_std.py +48 -0
  32. blisswriter-1.0.0b0/src/blisswriter/session_api.py +8 -0
  33. blisswriter-1.0.0b0/src/blisswriter/subscribers/__init__.py +0 -0
  34. blisswriter-1.0.0b0/src/blisswriter/subscribers/base_subscriber.py +209 -0
  35. blisswriter-1.0.0b0/src/blisswriter/subscribers/scan_subscriber.py +294 -0
  36. blisswriter-1.0.0b0/src/blisswriter/subscribers/session_subscriber.py +264 -0
  37. blisswriter-1.0.0b0/src/blisswriter/tango/__init__.py +2 -0
  38. blisswriter-1.0.0b0/src/blisswriter/tango/servers/NexusWriter.py +778 -0
  39. blisswriter-1.0.0b0/src/blisswriter/tango/servers/__init__.py +2 -0
  40. blisswriter-1.0.0b0/src/blisswriter/tests/__init__.py +0 -0
  41. blisswriter-1.0.0b0/src/blisswriter/tests/helpers/__init__.py +0 -0
  42. blisswriter-1.0.0b0/src/blisswriter/tests/helpers/nxw_test_math.py +28 -0
  43. blisswriter-1.0.0b0/src/blisswriter/tests/test_array_order.py +165 -0
  44. blisswriter-1.0.0b0/src/blisswriter/tests/test_base_subscriber.py +74 -0
  45. blisswriter-1.0.0b0/src/blisswriter/tests/test_data_merging.py +152 -0
  46. blisswriter-1.0.0b0/src/blisswriter/tests/test_dataset_proxy.py +391 -0
  47. blisswriter-1.0.0b0/src/blisswriter/tests/test_devices.py +38 -0
  48. blisswriter-1.0.0b0/src/blisswriter/tests/test_h5_external.py +111 -0
  49. blisswriter-1.0.0b0/src/blisswriter/tests/test_logging.py +365 -0
  50. blisswriter-1.0.0b0/src/blisswriter/tests/test_nexus.py +753 -0
  51. blisswriter-1.0.0b0/src/blisswriter/tests/test_reference_proxy.py +48 -0
  52. blisswriter-1.0.0b0/src/blisswriter/tests/test_rotating_file.py +20 -0
  53. blisswriter-1.0.0b0/src/blisswriter/tests/test_sync_utils.py +67 -0
  54. blisswriter-1.0.0b0/src/blisswriter/utils/__init__.py +2 -0
  55. blisswriter-1.0.0b0/src/blisswriter/utils/array_order.py +307 -0
  56. blisswriter-1.0.0b0/src/blisswriter/utils/array_size.py +32 -0
  57. blisswriter-1.0.0b0/src/blisswriter/utils/config_utils.py +90 -0
  58. blisswriter-1.0.0b0/src/blisswriter/utils/data_merging.py +455 -0
  59. blisswriter-1.0.0b0/src/blisswriter/utils/data_policy.py +23 -0
  60. blisswriter-1.0.0b0/src/blisswriter/utils/exceptions.py +11 -0
  61. blisswriter-1.0.0b0/src/blisswriter/utils/log_levels.py +123 -0
  62. blisswriter-1.0.0b0/src/blisswriter/utils/logging_utils.py +396 -0
  63. blisswriter-1.0.0b0/src/blisswriter/utils/periodic.py +18 -0
  64. blisswriter-1.0.0b0/src/blisswriter/utils/process_utils.py +544 -0
  65. blisswriter-1.0.0b0/src/blisswriter/utils/profiling.py +404 -0
  66. blisswriter-1.0.0b0/src/blisswriter/utils/scan_utils.py +57 -0
  67. blisswriter-1.0.0b0/src/blisswriter/utils/session_utils.py +53 -0
  68. blisswriter-1.0.0b0/src/blisswriter/utils/sync_utils.py +63 -0
  69. blisswriter-1.0.0b0/src/blisswriter/writer/__init__.py +0 -0
  70. blisswriter-1.0.0b0/src/blisswriter/writer/capture.py +21 -0
  71. blisswriter-1.0.0b0/src/blisswriter/writer/datasets.py +1120 -0
  72. blisswriter-1.0.0b0/src/blisswriter/writer/info.py +519 -0
  73. blisswriter-1.0.0b0/src/blisswriter/writer/items.py +273 -0
  74. blisswriter-1.0.0b0/src/blisswriter/writer/main.py +457 -0
  75. blisswriter-1.0.0b0/src/blisswriter/writer/plots.py +262 -0
  76. blisswriter-1.0.0b0/src/blisswriter/writer/references.py +80 -0
  77. blisswriter-1.0.0b0/src/blisswriter/writer/scan.py +1200 -0
  78. blisswriter-1.0.0b0/src/blisswriter/writer/scan_url_info.py +83 -0
  79. blisswriter-1.0.0b0/src/blisswriter/writer/types.py +15 -0
  80. blisswriter-1.0.0b0/src/blisswriter/writer/typing.py +12 -0
  81. blisswriter-1.0.0b0/src/blisswriter.egg-info/PKG-INFO +46 -0
  82. blisswriter-1.0.0b0/src/blisswriter.egg-info/SOURCES.txt +85 -0
  83. blisswriter-1.0.0b0/src/blisswriter.egg-info/dependency_links.txt +1 -0
  84. blisswriter-1.0.0b0/src/blisswriter.egg-info/entry_points.txt +4 -0
  85. blisswriter-1.0.0b0/src/blisswriter.egg-info/requires.txt +15 -0
  86. 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,3 @@
1
+ [build-system]
2
+ build-backend = "setuptools.build_meta"
3
+ requires = ["setuptools>=46.4", "wheel"]
@@ -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
+
@@ -0,0 +1,4 @@
1
+ import setuptools
2
+
3
+ if __name__ == "__main__":
4
+ setuptools.setup()
@@ -0,0 +1,4 @@
1
+ """NeXus-compliant HDF5 writer for Bliss
2
+ """
3
+
4
+ __version__ = "1.0.0b0"
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,7 @@
1
+ import logging
2
+ from blisswriter.utils import logging_utils
3
+
4
+
5
+ def config_root_logger(args):
6
+ logger = logging.getLogger("blisswriter")
7
+ logging_utils.cliconfig(logger, args)
@@ -0,0 +1,2 @@
1
+ """Nexus writer I/O utilities
2
+ """
@@ -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