hdfmap 0.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
hdfmap/__init__.py ADDED
@@ -0,0 +1,76 @@
1
+ """
2
+ hdfmap
3
+ Map objects within an HDF file and create a dataset namespace.
4
+
5
+ --- Usage ---
6
+ # HdfMap from NeXus file:
7
+ from hdfmap import create_nexus_map, load_hdf
8
+ hmap = create_nexus_map('file.nxs')
9
+ with load_hdf('file.nxs') as nxs:
10
+ address = hmap.get_address('energy')
11
+ energy = nxs[address][()]
12
+ string = hmap.format_hdf(nxs, "the energy is {energy:.2f} keV")
13
+ d = hmap.get_data_block(nxs) # classic data table, d.scannable, d.metadata
14
+
15
+ # Shortcuts - single file reloading class
16
+ from hdfmap import NexusLoader
17
+ scan = NexusLoader('file.nxs')
18
+ [data1, data2] = scan.get_data(['dataset_name_1', 'dataset_name_2'])
19
+ data = scan.eval('dataset_name_1 * 100 + 2')
20
+ string = scan.format('my data is {dataset_name_1:.2f}')
21
+
22
+ # Shortcuts - multifile load data
23
+ from hdfmap import hdf_data, hdf_eval, hdf_format, hdf_image
24
+ all_data = hdf_data([f"file{n}.nxs" for n in range(100)], 'dataset_name')
25
+ normalised_data = hdf_eval(filenames, 'total / Transmission / (rc / 300.)')
26
+ descriptions = hdf_eval(filenames, 'Energy: {en:5.3f} keV')
27
+ image = hdf_image(filenames, index=31)
28
+
29
+
30
+ By Dr Dan Porter
31
+ Diamond Light Source Ltd
32
+ 2024
33
+ """
34
+
35
+ from .logging import set_all_logging_level
36
+ from .hdfmap_class import HdfMap
37
+ from .nexus import NexusMap
38
+ from .file_functions import list_files, load_hdf, create_hdf_map, create_nexus_map
39
+ from .file_functions import hdf_data, hdf_image, hdf_eval, hdf_format, nexus_data_block
40
+ from .reloader_class import HdfLoader, NexusLoader
41
+
42
+
43
+ __all__ = [
44
+ HdfMap, NexusMap, load_hdf, create_hdf_map, create_nexus_map,
45
+ hdf_data, hdf_image, hdf_eval, hdf_format, nexus_data_block, HdfLoader, NexusLoader,
46
+ set_all_logging_level
47
+ ]
48
+
49
+ __version__ = "0.4.0"
50
+ __date__ = "2024/08/16"
51
+
52
+
53
+ def version_info() -> str:
54
+ return 'hdfmap version %s (%s)' % (__version__, __date__)
55
+
56
+
57
+ def module_info() -> str:
58
+ import sys
59
+ out = 'Python version %s' % sys.version
60
+ out += '\n at: %s' % sys.executable
61
+ out += '\n %s: %s' % (version_info(), __file__)
62
+ # Modules
63
+ import numpy
64
+ out += '\n numpy version: %s' % numpy.__version__
65
+ import h5py
66
+ out += '\n h5py version: %s' % h5py.__version__
67
+ # import imageio
68
+ # out += '\n imageio version: %s' % imageio.__version__
69
+ try:
70
+ import hdf5plugin
71
+ out += '\n hdf5plugin: %s' % hdf5plugin.version
72
+ except ImportError:
73
+ out += '\n hdf5plugin: None'
74
+ import os
75
+ out += '\nRunning in directory: %s\n' % os.path.abspath('.')
76
+ return out
@@ -0,0 +1,172 @@
1
+ """
2
+ hdf eval functions
3
+ """
4
+
5
+ import os
6
+ import ast
7
+ import builtins
8
+ import datetime
9
+ import re
10
+ import typing
11
+ import numpy as np
12
+ import h5py
13
+
14
+ from .logging import create_logger
15
+
16
+ # parameters
17
+ GLOBALS = {'np': np}
18
+ GLOBALS_NAMELIST = dir(builtins) + list(GLOBALS.keys())
19
+ logger = create_logger(__name__)
20
+ # regex patterns
21
+ special_characters = re.compile(r'\W') # finds all special non-alphanumberic characters
22
+ long_floats = re.compile(r'\d+\.\d{5,}') # finds floats with long trailing decimals
23
+
24
+
25
+ def expression_safe_name(string: str, replace: str = '_') -> str:
26
+ """
27
+ Returns an expression safe name
28
+ :param string: any string
29
+ :param replace: str replace special characters with this
30
+ :return: string with special characters replaced
31
+ """
32
+ return special_characters.sub('_', string)
33
+
34
+
35
+ def round_string_floats(string):
36
+ """
37
+ Shorten string by removing long floats
38
+ :param string: string, e.g. '#810002 scan eta 74.89533603616637 76.49533603616636 0.02 pil3_100k 1 roi2'
39
+ :return: shorter string, e.g. '#810002 scan eta 74.895 76.495 0.02 pil3_100k 1 roi2'
40
+ """
41
+ def subfun(m):
42
+ return str(round(float(m.group()), 3))
43
+ return long_floats.sub(subfun, string)
44
+
45
+
46
+ def dataset2data(dataset: h5py.Dataset, index: int | slice = (), direct_load=False) -> datetime.datetime | str | np.ndarray:
47
+ """
48
+ Read the data from a h5py Dataset and convert to either datetime, str or squeezed numpy array
49
+ :param dataset: h5py.Dataset containing data
50
+ :param index: index of array (not used if dataset is string/ bytes type)
51
+ :param direct_load: loads the data directly without conversion if True
52
+ :return datetime.datetime: if data is an isoformat string, returns datetime object
53
+ :return str: if data is another string, returns str with long floats rounded
54
+ :return np.ndarray: if data is another numeric object
55
+ """
56
+ if direct_load:
57
+ return dataset[index]
58
+ if np.issubdtype(dataset, np.number):
59
+ return np.squeeze(dataset[index]) # numeric np.ndarray
60
+ try:
61
+ # timestamp -> datetime64 -> datetime
62
+ timestamp = np.squeeze(dataset[index]).astype(np.datetime64).astype(datetime.datetime)
63
+ # single datetime obj vs array of datetime obj
64
+ return timestamp[()] if timestamp.ndim == 0 else timestamp
65
+ except ValueError:
66
+ try:
67
+ string_dataset = dataset.asstr()[()]
68
+ if dataset.ndim == 0:
69
+ return round_string_floats(string_dataset) # bytes or str -> str
70
+ return string_dataset # str array
71
+ except ValueError:
72
+ return np.squeeze(dataset[index]) # other np.ndarray
73
+
74
+
75
+ def check_unsafe_eval(eval_str: str) -> None:
76
+ """
77
+ Check str for naughty eval arguments such as sys, os or import
78
+ This is not foolproof.
79
+ :param eval_str: str
80
+ :return: pass or raise error
81
+ """
82
+ bad_names = ['import', 'os.', 'sys.', 'open(', 'eval', 'exec']
83
+ for bad in bad_names:
84
+ if bad in eval_str:
85
+ raise Exception('This operation is not allowed as it contains: "%s"' % bad)
86
+
87
+
88
+ def find_identifiers(expression: str) -> list[str]:
89
+ """Returns list of variable names in expression, ommiting builtins and globals"""
90
+ # varnames = re.findall(r'[a-zA-Z]\w*', expression)
91
+ return [node.id for node in ast.walk(ast.parse(expression, mode='eval'))
92
+ if type(node) is ast.Name and node.id not in GLOBALS_NAMELIST]
93
+
94
+
95
+ def extra_hdf_data(hdf_file: h5py.File) -> dict:
96
+ """Extract filename, filepath and other additional data fom hdf file"""
97
+ filepath = getattr(hdf_file, 'filename', 'unknown')
98
+ return {
99
+ 'filepath': filepath,
100
+ 'filename': os.path.basename(filepath),
101
+ }
102
+
103
+
104
+ def generate_namespace(hdf_file: h5py.File, hdf_namespace: dict[str, str], identifiers: list[str] | None = None,
105
+ default: typing.Any = np.array('--')) -> dict[str, typing.Any]:
106
+ """
107
+ Generate namespace dict - create a dictionary linking the name of a dataset to the dataset value
108
+
109
+ Adds additional values if not in name_path dict:
110
+ filename: str, name of hdf_file
111
+ filepath: str, full path of hdf_file
112
+ _*name*: str hdf path of *name*
113
+
114
+ :param hdf_file: h5py.File object
115
+ :param hdf_namespace: locations of data in hdf file, dict[identifier]='/hdf/dataset/path'
116
+ :param identifiers: list of names to load from hdf_file, if None, use generate all items in name_path
117
+ :param default: any, if varname not in name_path - return default instead
118
+ :return: dict {'name': value, '_name': '/hdf/path'}
119
+ """
120
+ if identifiers is None:
121
+ identifiers = list(hdf_namespace.keys())
122
+ # TODO: add ROI commands e.g. nroi[1,2,3,4] -> default_image([1,2,3,4])
123
+ # TODO: add name@attribute e.g. incident_energy@units -> 'eV'
124
+ # TODO: add name.label e.g. axes.label -> 'eta [Deg]'
125
+ namespace = {
126
+ name: dataset2data(hdf_file[hdf_namespace[name]])
127
+ for name in identifiers if name in hdf_namespace and hdf_namespace[name] in hdf_file
128
+ }
129
+ defaults = {
130
+ name: default
131
+ for name in identifiers if (name not in hdf_namespace) or (hdf_namespace[name] not in hdf_file)
132
+ }
133
+ hdf_paths = {name: hdf_namespace[name[1:]] for name in identifiers
134
+ if name.startswith('_') and name[1:] in hdf_namespace}
135
+ # add extra params
136
+ extras = extra_hdf_data(hdf_file)
137
+ return {**defaults, **extras, **hdf_paths, **namespace}
138
+
139
+
140
+ def eval_hdf(hdf_file: h5py.File, expression: str, hdf_namespace: dict[str, str]) -> typing.Any:
141
+ """
142
+ Evaluate an expression using the namespace of the hdf file
143
+ :param hdf_file: h5py.File object
144
+ :param expression: str expression to be evaluated
145
+ :param hdf_namespace: dict of {'variable name': '/hdf/dataset/path'}
146
+ :return: eval(expression)
147
+ """
148
+ if expression in hdf_file:
149
+ return dataset2data(hdf_file[expression])
150
+ check_unsafe_eval(expression)
151
+ # find identifiers matching names in the namespace
152
+ identifiers = [name for name in hdf_namespace if name in special_characters.split(expression)]
153
+ # find other non-builtin identifiers
154
+ identifiers += [name for name in find_identifiers(expression) if name not in identifiers]
155
+ namespace = generate_namespace(hdf_file, hdf_namespace, identifiers)
156
+ logger.info(f"Expression: {expression}\nidentifiers: {identifiers}\n")
157
+ logger.debug(f"namespace: {namespace}\n")
158
+ return eval(expression, GLOBALS, namespace)
159
+
160
+
161
+ def format_hdf(hdf_file: h5py.File, expression: str, hdf_namespace: dict[str, str]) -> str:
162
+ """
163
+ Evaluate a formatted string expression using the namespace of the hdf file
164
+ :param hdf_file: h5py.File object
165
+ :param expression: str expression using {name} format specifiers
166
+ :param hdf_namespace: dict of {'variable name': '/hdf/dataset/path'}
167
+ :return: eval_hdf(f"expression")
168
+ """
169
+ expression = 'f"""' + expression + '"""' # convert to fstr
170
+ return eval_hdf(hdf_file, expression, hdf_namespace)
171
+
172
+
@@ -0,0 +1,210 @@
1
+ import os
2
+ import h5py
3
+ import numpy as np
4
+
5
+ from .logging import create_logger
6
+ from .hdfmap_class import HdfMap
7
+ from .nexus import NexusMap
8
+
9
+
10
+ EXTENSIONS = ['.nxs', '.hdf', '.hdf5', '.h5']
11
+ DEFAULT_EXTENSION = EXTENSIONS[0]
12
+ DEFAULT_HDF_PATH = "entry1/scan_command"
13
+ logger = create_logger(__name__)
14
+
15
+
16
+ def load_hdf(hdf_filename: str) -> h5py.File:
17
+ """Load hdf file, return h5py.File object"""
18
+ return h5py.File(hdf_filename, 'r')
19
+
20
+
21
+ def list_files(folder_directory: str, extension=DEFAULT_EXTENSION) -> list[str]:
22
+ """Return list of files in directory with extension, returning list of full file paths"""
23
+ try:
24
+ return sorted(
25
+ (file.path for file in os.scandir(folder_directory) if file.is_file() and file.name.endswith(extension)),
26
+ key=lambda x: os.path.getmtime(x)
27
+ )
28
+ except FileNotFoundError:
29
+ return []
30
+
31
+
32
+ def as_str_list(string: str | list[str]) -> list[str]:
33
+ """
34
+ Helper function to convert str or list of str to list of str
35
+ :param string: str, byteString, list, array
36
+ :return: list of str
37
+ """
38
+ return list(np.asarray(string, dtype=str).reshape(-1))
39
+
40
+
41
+ def create_hdf_map(hdf_filename: str) -> HdfMap:
42
+ """
43
+ Create a HdfMap from a hdf file
44
+ :param hdf_filename: str filename of hdf file
45
+ :return: HdfMap
46
+ """
47
+ with load_hdf(hdf_filename) as hdf:
48
+ hdf_map = HdfMap(hdf)
49
+ return hdf_map
50
+
51
+
52
+ def create_nexus_map(hdf_filename: str, groups: None | list[str] = None,
53
+ default_entry_only: bool = False) -> NexusMap:
54
+ """
55
+ Create a HdfMap from a NeXus file, loading default parameters and allowing a reduced, single entry map
56
+ :param hdf_filename: str filename of hdf file
57
+ :param groups: list of groups to collect datasets from
58
+ :param default_entry_only: if True, only the first or default entry will be loaded
59
+ :return: NexusMap
60
+ """
61
+ hdf_map = NexusMap()
62
+ with load_hdf(hdf_filename) as hdf:
63
+ hdf_map.populate(hdf, groups=groups, default_entry_only=default_entry_only)
64
+ if not hdf_map.scannables:
65
+ print('NXdata not found, getting scannables from most common array size')
66
+ size = hdf_map.most_common_size()
67
+ hdf_map.generate_scannables(size)
68
+ return hdf_map
69
+
70
+
71
+ def hdf_data(filenames: str | list[str], name_or_path: str | list[str], hdf_map: HdfMap = None,
72
+ index=(), default=None, fixed_output=False):
73
+ """
74
+ General purpose function to retrieve data from HDF files
75
+ :param filenames: str or list of str - file paths
76
+ :param name_or_path: str or list of str - names or paths of HDF datasets
77
+ :param hdf_map: HdfMap object, or None to generate from first file
78
+ :param index: dataset index or slice
79
+ :param default: value to give if dataset doesn't exist in file
80
+ :param fixed_output: if True, always returns list of list
81
+ :return if single file, single dataset: single value
82
+ :return if multi file or multi dataset: list, len(filenames) or len(name_or_path)
83
+ :return if multi file and multi dataset: list[files: list[names]]
84
+ """
85
+ # cast as 1D arrays
86
+ filenames = as_str_list(filenames)
87
+ name_or_path = as_str_list(name_or_path)
88
+ # generate hdf_map
89
+ if hdf_map is None:
90
+ hdf_map = create_hdf_map(filenames[0])
91
+ out = []
92
+ for filename in filenames:
93
+ logger.info(f"\nHDF file: {filename}")
94
+ with load_hdf(filename) as hdf:
95
+ out.append([hdf_map.get_data(hdf, name, index=index, default=default) for name in name_or_path])
96
+ if fixed_output:
97
+ return out
98
+ if len(filenames) == 1 and len(name_or_path) == 1:
99
+ return out[0][0]
100
+ if len(filenames) == 1 and len(name_or_path) > 1:
101
+ return out[0]
102
+ if len(name_or_path) == 1:
103
+ return [val[0] for val in out]
104
+ return out
105
+
106
+
107
+ def hdf_eval(filenames: str | list[str], expression: str, hdf_map: HdfMap = None, fixed_output=False):
108
+ """
109
+ Evaluate expression using dataset names
110
+ :param filenames: str or list of str - file paths
111
+ :param expression: str expression to evaluate in each file, e.g. "roi2_sum / Transmission"
112
+ :param hdf_map: HdfMap object, or None to generate from first file
113
+ :param fixed_output: if True, always returns list len(filenames)
114
+ :return if single file: single output
115
+ :return if multi file: list, len(filenames)
116
+ """
117
+ # cast as 1D arrays
118
+ filenames = as_str_list(filenames)
119
+ # generate hdf_map
120
+ if hdf_map is None:
121
+ hdf_map = create_hdf_map(filenames[0])
122
+ out = []
123
+ for filename in filenames:
124
+ logger.info(f"\nHDF file: {filename}")
125
+ with load_hdf(filename) as hdf:
126
+ out.append(hdf_map.eval(hdf, expression))
127
+ if not fixed_output and len(filenames) == 1:
128
+ return out[0]
129
+ return out
130
+
131
+
132
+ def hdf_format(filenames: str | list[str], expression: str, hdf_map: HdfMap = None, fixed_output=False):
133
+ """
134
+ Evaluate string format expression using dataset names
135
+ :param filenames: str or list of str - file paths
136
+ :param expression: str expression to evaluate in each file, e.g. "the energy is {en:.2f} keV"
137
+ :param hdf_map: HdfMap object, or None to generate from first file
138
+ :param fixed_output: if True, always returns list len(filenames)
139
+ :return if single file: single output
140
+ :return if multi file: list, len(filenames)
141
+ """
142
+ # cast as 1D arrays
143
+ filenames = as_str_list(filenames)
144
+ # generate hdf_map
145
+ if hdf_map is None:
146
+ hdf_map = create_hdf_map(filenames[0])
147
+ out = []
148
+ for filename in filenames:
149
+ logger.info(f"\nHDF file: {filename}")
150
+ with load_hdf(filename) as hdf:
151
+ out.append(hdf_map.format_hdf(hdf, expression))
152
+ if not fixed_output and len(filenames) == 1:
153
+ return out[0]
154
+ return out
155
+
156
+
157
+ def hdf_image(filenames: str | list[str], index: slice = None, hdf_map: HdfMap = None, fixed_output=False):
158
+ """
159
+ Evaluate string format expression using dataset names
160
+ :param filenames: str or list of str - file paths
161
+ :param index: index or slice of dataset volume, or None to use middle index
162
+ :param hdf_map: HdfMap object, or None to generate from first file
163
+ :param fixed_output: if True, always returns list len(filenames)
164
+ :return if single file: single output - numpy array
165
+ :return if multi file: list, len(filenames)
166
+ """
167
+ # cast as 1D arrays
168
+ filenames = as_str_list(filenames)
169
+ # generate hdf_map
170
+ if hdf_map is None:
171
+ hdf_map = create_hdf_map(filenames[0])
172
+ out = []
173
+ for filename in filenames:
174
+ logger.info(f"\nHDF file: {filename}")
175
+ with load_hdf(filename) as hdf:
176
+ out.append(hdf_map.get_image(hdf, index=index))
177
+ if not fixed_output and len(filenames) == 1:
178
+ return out[0]
179
+ return out
180
+
181
+
182
+ def nexus_data_block(filenames: str | list[str], hdf_map: HdfMap = None, fixed_output=False):
183
+ """
184
+ Create classic dict like dataloader objects from nexus files
185
+ E.G.
186
+ d = nexus_data_block('filename')
187
+ d.scannable -> array
188
+ d.metadata.filename -> value
189
+ d.keys() -> list of items
190
+
191
+ :param filenames: str or list of str - file paths
192
+ :param hdf_map: HdfMap object, or None to generate from first file
193
+ :param fixed_output: if True, always returns list len(filenames)
194
+ :return if single file: single output - dict like DataObject
195
+ :return if multi file: list, len(filenames)
196
+ """
197
+ # cast as 1D arrays
198
+ filenames = as_str_list(filenames)
199
+ # generate hdf_map
200
+ if hdf_map is None:
201
+ hdf_map = create_nexus_map(filenames[0])
202
+ out = []
203
+ for filename in filenames:
204
+ logger.info(f"\nHDF file: {filename}")
205
+ with load_hdf(filename) as hdf:
206
+ out.append(hdf_map.get_dataholder(hdf))
207
+ if not fixed_output and len(filenames) == 1:
208
+ return out[0]
209
+ return out
210
+