evefile 0.1.0rc1__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.
evefile/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ """evefile package
2
+
3
+ Transitional package to read eveH5 files containing synchrotron radiometry
4
+ data recorded at BESSY/MLS in Berlin.
5
+ """
6
+
7
+ # Import facade class
8
+ from evefile.boundaries.evefile import EveFile # noqa
@@ -0,0 +1 @@
1
+ """Interfaces of the evefile package: facades and resources."""
@@ -0,0 +1,554 @@
1
+ """
2
+
3
+ *High-level Python object representation of eveH5 file contents.*
4
+
5
+ .. sidebar:: Contents
6
+
7
+ .. contents::
8
+ :local:
9
+ :depth: 1
10
+
11
+ This module provides a high-level representation of the contents of an eveH5
12
+ file. Being a high-level, user-facing object representation, technically
13
+ speaking this module is a facade. The corresponding resource
14
+ (persistence-layer-facing interface) would be the :mod:`eveh5
15
+ <evefile.boundaries.eveh5>` module.
16
+
17
+
18
+ Overview
19
+ ========
20
+
21
+ A first overview of the classes implemented in this module and their
22
+ hierarchy is given in the UML diagram below.
23
+
24
+
25
+ .. figure:: /uml/evefile.boundaries.evefile.*
26
+ :align: center
27
+
28
+ Class hierarchy of the :mod:`evefile.boundaries.evefile` module,
29
+ providing the facade (user-facing interface) for an eveH5 file.
30
+ Basically, it inherits from :class:`evefile.entities.file.File`
31
+ and adds behaviour. Most of this behaviour is contributed by the various
32
+ modules of the :mod:`controllers <evefile.controllers>`
33
+ subpackage.
34
+
35
+
36
+ Key aspects
37
+ ===========
38
+
39
+ While the :mod:`evefile <evefile.boundaries.evefile>` module is the
40
+ high-level interface (facade) of the ``evefile`` package,
41
+ it is still, from a functional viewpoint, close to the actual eveH5 files,
42
+ providing a faithful representation of all information contained in an eveH5
43
+ file. Nevertheless, it is clearly an abstraction from the actual data files.
44
+ Hence, the key characteristics of the module are:
45
+
46
+ * Stable interface to eveH5 files, regardless of their version.
47
+
48
+ * Some features may only be available for newer eveH5 versions, though.
49
+
50
+ * Powerful abstractions on the device level.
51
+
52
+ * Options to devices appear as attributes of the device objects, not as
53
+ separate datasets.
54
+
55
+ * Actual **data are loaded on demand**, not when loading the file.
56
+
57
+ * This does *not* apply to the metadata of the individual datasets.
58
+ Those are read upon reading the file.
59
+ * Reading data on demand should save time and resources, particularly
60
+ for larger files.
61
+ * Often, you are only interested in a subset of the available data.
62
+
63
+
64
+ Usage
65
+ =====
66
+
67
+ Loading the contents of a data file of a measurement may be as simple as:
68
+
69
+ .. code-block::
70
+
71
+ evefile = EveFile(filename="my_measurement_file.h5")
72
+
73
+ If you are interested in a convenient overview of the contents of the
74
+ eveH5 file just loaded, try this:
75
+
76
+ .. code-block::
77
+
78
+ evefile.show_info()
79
+
80
+ This will output a human-readable summary of the file metadata,
81
+ log messages, and a list of datasets in the data, snapshot, and monitor
82
+ section. See the documentation of the :meth:`EveFile.show_info` method for
83
+ details.
84
+
85
+ Data are stored within a :class:`EveFile` object with their IDs rather than
86
+ the "given" names users are familiar with. Hence, to get an overview of all
87
+ the data(sets) contained in a file, use the :meth:`EveFile.get_data_names`
88
+ method:
89
+
90
+ .. code-block::
91
+
92
+ evefile.get_data_names()
93
+
94
+ This will return a list of "given" data names.
95
+
96
+ Similarly, if you know the "given" name of a dataset or a list of datasets,
97
+ you can retrieve the corresponding :class:`Data <evefile.entities.data.Data>`
98
+ objects by using the :meth:`EveFile.get_data` method:
99
+
100
+ .. code-block::
101
+
102
+ # Get list of datasets by name
103
+ evefile.get_data(["name1", "name2"])
104
+
105
+ # Get single dataset by name
106
+ evefile.get_data("name")
107
+
108
+ To get the data marked as preferred in the scan, use the
109
+ :meth:`EveFile.get_preferred_data` method:
110
+
111
+ .. code-block::
112
+
113
+ evefile.get_preferred_data()
114
+
115
+ This will return a list with three elements, ``[preferred_axis,
116
+ preferred_channel, preferred_normalisation_channel]``, where each of these
117
+ elements is either of type :class:`evefile.entities.data.Data` or
118
+ :obj:`None`.
119
+
120
+
121
+ Internals: What happens when reading an eveH5 file?
122
+ ===================================================
123
+
124
+ Reading an eveH5 file is not as simple as reading contents of an HDF5 file
125
+ and present its contents as Python object hierarchy. At least, if you would
126
+ like to view, process, and analyse your data more conveniently, you should
127
+ not stop here. The idea behind the ``evefile`` package, and in parts behind
128
+ the :class:`EveFile` class, is to provide you as consumer of the data with
129
+ powerful abstractions and structured information. To this end, a series of
130
+ steps are necessary:
131
+
132
+ * Read the eveH5 file (actually, an HDF5 file).
133
+ * Get the correct :class:`VersionMapper
134
+ <evefile.controllers.version_mapping.VersionMapper>` class.
135
+ * Map the file contents to the proper :mod:`data structures
136
+ <evefile.entities.data>` provided by the ``evefile`` package.
137
+
138
+
139
+ Module documentation
140
+ ====================
141
+
142
+ """
143
+
144
+ import logging
145
+ import os
146
+
147
+ import pandas as pd
148
+
149
+ from evefile.entities.file import File
150
+ from evefile.boundaries.eveh5 import HDF5File
151
+ from evefile.controllers import version_mapping, joining
152
+
153
+
154
+ logger = logging.getLogger(__name__)
155
+
156
+
157
+ class EveFile(File):
158
+ """
159
+ High-level Python object representation of eveH5 file contents.
160
+
161
+ This class serves as facade to the entire ``evefile`` package and provides
162
+ a rather high-level representation of the contents of an individual
163
+ eveH5 file.
164
+
165
+ Individual measurements are saved in HDF5 files using a particular
166
+ schema (eveH5). Besides file-level metadata, there are log messages
167
+ and the actual data.
168
+
169
+ The data are organised in three functionally different sections: data,
170
+ snapshots, and monitors. While the "data" section contains the data of
171
+ motor axes moved and detector channels read out during the scan,
172
+ the "snapshot" section contains values at distinct times (usually at
173
+ the very beginning of a scan), usually of more devices than used in
174
+ the scan. The "monitors" section contains data of all those devices
175
+ monitored during the scan, meaning that only changes in values are
176
+ recorded, together with their timestamp (rather than position count).
177
+
178
+ Values from axes contained in the "snapshot" section are used for data
179
+ joining, while all other values can be regarded as either options
180
+ of individual devices or telemetry data for the setup. All values from
181
+ the "monitors" section are strictly telemetry data for the setup.
182
+
183
+
184
+ Attributes
185
+ ----------
186
+ metadata : :class:`evefile.entities.file.Metadata`
187
+ File metadata
188
+
189
+ log_messages : :class:`list`
190
+ Log messages from an individual measurement
191
+
192
+ Each item in the list is an instance of
193
+ :class:`evefile.entities.file.LogMessage`.
194
+
195
+ data : :class:`dict`
196
+ Data recorded from the devices involved in the scan.
197
+
198
+ The keys of the dictionary are the (guaranteed to be unique) HDF
199
+ dataset names, not the "given" names usually familiar to the users.
200
+ Use the :meth:`get_data` method to retrieve data objects by their
201
+ "given" name.
202
+
203
+ Each item is an instance of
204
+ :class:`evefile.entities.data.Data`.
205
+
206
+ snapshots : :class:`dict`
207
+ Device data recorded as snapshot during a measurement.
208
+
209
+ Only those device data that are not options belonging to any of
210
+ the devices in the :attr:`data` attribute are stored here.
211
+
212
+ The keys of the dictionary are the (guaranteed to be unique) HDF
213
+ dataset names, not the "given" names usually familiar to the users.
214
+
215
+ Each item is an instance of
216
+ :class:`evefile.entities.data.Data`.
217
+
218
+ monitors : :class:`dict`
219
+ Device data monitored during a measurement.
220
+
221
+ The keys of the dictionary are the (guaranteed to be unique) HDF
222
+ dataset names, not the "given" names usually familiar to the users.
223
+
224
+ Each item is an instance of
225
+ :class:`evefile.entities.data.MonitorData`.
226
+
227
+ position_timestamps : :class:`evefile.entities.data.TimestampData`
228
+ Timestamps for each individual position.
229
+
230
+ Monitors have timestamps (milliseconds since start of the scan)
231
+ rather than positions as primary quantisation axis. This object
232
+ provides a mapping between timestamps and positions and can be used
233
+ to map monitor data to positions.
234
+
235
+
236
+ Parameters
237
+ ----------
238
+ filename : :class:`str`
239
+ Name of the file to be loaded.
240
+
241
+
242
+ Raises
243
+ ------
244
+ ValueError
245
+ Raised if no filename is provided.
246
+
247
+ FileNotFoundError
248
+ Raised if provided file could not be found.
249
+
250
+
251
+ Examples
252
+ --------
253
+ Loading the contents of a data file of a measurement may be as simple as:
254
+
255
+ .. code-block::
256
+
257
+ evefile = EveFile(filename="my_measurement_file.h5")
258
+
259
+ """
260
+
261
+ def __init__(self, filename="", load=True):
262
+ super().__init__()
263
+ self.filename = filename
264
+ self._join_factory = joining.JoinFactory(evefile=self)
265
+ if load:
266
+ if not filename:
267
+ raise ValueError("No filename given")
268
+ if not os.path.exists(filename):
269
+ raise FileNotFoundError(f"File {filename} does not exist.")
270
+ self._read_and_map_eveh5_file()
271
+
272
+ @property
273
+ def filename(self):
274
+ """
275
+ Name of the file to be loaded.
276
+
277
+ Returns
278
+ -------
279
+ filename : :class:`str`
280
+ Name of the file to be loaded.
281
+
282
+ """
283
+ return self.metadata.filename
284
+
285
+ @filename.setter
286
+ def filename(self, filename=""):
287
+ self.metadata.filename = filename
288
+
289
+ def _read_and_map_eveh5_file(self):
290
+ eveh5 = HDF5File()
291
+ eveh5.read_attributes = True
292
+ eveh5.close_file = False
293
+ eveh5.read(filename=self.metadata.filename)
294
+ mapper_factory = version_mapping.VersionMapperFactory()
295
+ mapper = mapper_factory.get_mapper(eveh5)
296
+ mapper.map(source=eveh5, destination=self)
297
+ eveh5.close()
298
+
299
+ def get_data(self, name=None):
300
+ """
301
+ Retrieve data objects by name.
302
+
303
+ While generally, you can get the data objects by accessing the
304
+ :attr:`data <evefile.entities.file.File.data>` attribute directly,
305
+ there, they are stored using their HDF5 dataset name as key.
306
+ Usually, however, data are accessed by their "given" name.
307
+
308
+ Parameters
309
+ ----------
310
+ name : :class:`str` | :class:`list`
311
+ Name or list of names of data to retrieve
312
+
313
+ Returns
314
+ -------
315
+ data : :class:`evefile.entities.data.Data` | :class:`list`
316
+ Data object(s) corresponding to the name(s).
317
+
318
+ In case of a list of data objects, each object is of type
319
+ :class:`evefile.entities.data.Data`.
320
+
321
+ """
322
+ data = []
323
+ names = {item.metadata.name: key for key, item in self.data.items()}
324
+ if isinstance(name, (list, tuple)):
325
+ for item in name:
326
+ data.append(self.data[names[item]])
327
+ else:
328
+ data.append(self.data[names[name]])
329
+ if len(data) == 1:
330
+ data = data[0]
331
+ return data
332
+
333
+ def get_data_names(self):
334
+ """
335
+ Retrieve "given" names of data objects.
336
+
337
+ Data are stored in the :attr:`data <evefile.entities.file.File.data>`
338
+ attribute using their HDF5 dataset name as key. Usually, however,
339
+ data are accessed by their "given" name.
340
+
341
+ This method returns a list of all "given" names of the datasets
342
+ stored in :attr:`data <evefile.entities.file.File.data>`.
343
+
344
+ Returns
345
+ -------
346
+ data : :class:`list`
347
+ List of names of the data object(s) in :attr:`data`.
348
+
349
+ """
350
+ names = [item.metadata.name for key, item in self.data.items()]
351
+ return names
352
+
353
+ def get_preferred_data(self):
354
+ """
355
+ Retrieve data objects marked as preferred.
356
+
357
+ Within a scan, a preferred motor axis, a preferred detector
358
+ channel, and a preferred channel for normalisation can be
359
+ named explicitly. The preferred axis and channel are used for
360
+ plotting within the eve GUI during measurement.
361
+
362
+ .. note::
363
+
364
+ The datasets returned are guaranteed to be commensurate
365
+ and compatible and can hence directly be plotted against each
366
+ other. This is due to the fact that preferred axis and channel
367
+ currently must come from the same scan module of a scan.
368
+
369
+ Returns
370
+ -------
371
+ data : :class:`list`
372
+ Data objects corresponding to the preference settings.
373
+
374
+ A list with three elements:
375
+
376
+ #. preferred axis
377
+ #. preferred channel
378
+ #. preferred normalisation channel
379
+
380
+ If the preference has been set in the scan description,
381
+ the item in the list is of type
382
+ :class:`evefile.entities.data.MeasureData`, otherwise
383
+ :obj:`None`.
384
+
385
+ """
386
+ output = [None, None, None]
387
+ if self.metadata.preferred_axis:
388
+ output[0] = self.data[self.metadata.preferred_axis]
389
+ if self.metadata.preferred_channel:
390
+ output[1] = self.data[self.metadata.preferred_channel]
391
+ if self.metadata.preferred_normalisation_channel:
392
+ output[2] = self.data[
393
+ self.metadata.preferred_normalisation_channel
394
+ ]
395
+ return output
396
+
397
+ def get_joined_data(self, data=None, mode="AxisOrChannelPositions"):
398
+ """
399
+ Retrieve data objects with commensurate dimensions.
400
+
401
+ For details on joining see the :mod:`joining
402
+ <evefile.controllers.joining>` module.
403
+
404
+ Parameters
405
+ ----------
406
+ data : :class:`list`
407
+ (Names/IDs of) data objects whose data should be joined.
408
+
409
+ You can provide either names or IDs or the actual data objects.
410
+
411
+ If no data are given, by default all data available will be
412
+ joined.
413
+
414
+ Default: :obj:`None`
415
+
416
+ mode : :class:`str`
417
+ Name of the join mode to be used. This must be a mode
418
+ understood by the :class:`JoinFactory
419
+ <evefile.controllers.joining.JoinFactory>`.
420
+
421
+ Default: "AxisOrChannelPositions"
422
+
423
+ Returns
424
+ -------
425
+ data : :class:`list`
426
+ List of data objects.
427
+
428
+ Each item in the list is of type
429
+ :class:`evefile.entities.data.MeasureData`.
430
+
431
+ """
432
+ if not data:
433
+ data = list(self.data.values())
434
+ joiner = self._join_factory.get_join(mode=mode)
435
+ return joiner.join(data)
436
+
437
+ def get_dataframe(self, data=None, mode="AxisOrChannelPositions"):
438
+ """
439
+ Retrieve Pandas DataFrame with given data objects as columns.
440
+
441
+ Internally, the :meth:`get_joined_data` method will be called with
442
+ the data provided. If the ``data`` parameter is omitted,
443
+ all datasets will be used.
444
+
445
+ The names of the columns of the returned DataFrame are the names (not
446
+ IDs) of the respective datasets.
447
+
448
+ .. important::
449
+
450
+ While working with a Pandas DataFrame may seem convenient,
451
+ you're loosing basically all the relevant metadata of the
452
+ datasets. Hence, this method is rather a convenience method to
453
+ be backwards-compatible to older interfaces, but it is
454
+ explicitly *not* suggested for extensive use.
455
+
456
+
457
+ Parameters
458
+ ----------
459
+ data : :class:`list`
460
+ (Names/IDs of) data objects whose data should be joined.
461
+
462
+ You can provide either names or IDs or the actual data objects.
463
+
464
+ If no data are given, by default all data available will be
465
+ joined.
466
+
467
+ Default: :obj:`None`
468
+
469
+ mode : :class:`str`
470
+ Name of the join mode to be used. This must be a mode
471
+ understood by the :class:`JoinFactory
472
+ <evefile.controllers.joining.JoinFactory>`.
473
+
474
+ Default: "AxisOrChannelPositions"
475
+
476
+ Returns
477
+ -------
478
+ dataframe : :class:`pandas.DataFrame`
479
+ Pandas DataFrame containing the given data objects as columns.
480
+
481
+ The names of the columns are the names (not IDs) of the
482
+ respective datasets.
483
+
484
+ """
485
+ if not data:
486
+ data = list(self.data.values())
487
+ joined_data = self.get_joined_data(data=data, mode=mode)
488
+ dataframe = pd.DataFrame(
489
+ {item.metadata.name: item.data for item in joined_data}
490
+ )
491
+ dataframe.index.name = "PosRef"
492
+ return dataframe
493
+
494
+ def show_info(self):
495
+ """
496
+ Print basic information regarding the contents of the loaded file.
497
+
498
+ Often, it is convenient to get a brief overview of the contents of
499
+ a file after it has been loaded. The output of this method
500
+ currently contains the following sections:
501
+
502
+ * metadata
503
+ * log messages
504
+ * data
505
+ * snapshots
506
+ * monitors
507
+
508
+ The output could look similar to the following:
509
+
510
+ .. code-block:: none
511
+
512
+ METADATA
513
+ filename: file.h5
514
+ eveh5_version: 7
515
+ eve_version: 2.0
516
+ xml_version: 9.2
517
+ measurement_station: Unittest
518
+ start: 2024-06-03 12:01:32
519
+ end: 2024-06-03 12:01:37
520
+ description:
521
+ simulation: False
522
+ preferred_axis: SimMot:01
523
+ preferred_channel: SimChan:01
524
+ preferred_normalisation_channel: SimChan:01
525
+
526
+ LOG MESSAGES
527
+ 20250812T09:06:05: Lorem ipsum
528
+
529
+ DATA
530
+ foo (SimMot:01) <AxisData>
531
+ bar (SimChan:01) <SinglePointChannelData>
532
+
533
+ SNAPSHOTS
534
+ bar (SimChan:01) <AxisData>
535
+ bazfoo (SimChan:03) <AxisData>
536
+ foo (SimMot:01) <AxisData>
537
+
538
+ MONITORS
539
+
540
+ """
541
+ print("METADATA")
542
+ print(self.metadata)
543
+ print("\nLOG MESSAGES")
544
+ for message in self.log_messages:
545
+ print(message)
546
+ print("\nDATA")
547
+ for item in self.data.values():
548
+ print(item)
549
+ print("\nSNAPSHOTS")
550
+ for item in self.snapshots.values():
551
+ print(item)
552
+ print("\nMONITORS")
553
+ for item in self.monitors.values():
554
+ print(item)