Skip to content

nexus_scan

NeXus Scan Classes

NexusScan - NeXus Scan class, lazy loader of scan files NexusDataHolder - Loads scan data and meta data into attributes

NexusDataHolder

Bases: DataHolder, NexusScan

Nexus data holder class - Automatically reads scannable and metadata from file - acts like the old .dat DataHolder class - has additional functions to read data from NeXus file

Example: scan = NexusDataHolder('12345.nxs') scan.eta -> returns array scan.metadata.metadata -> returns value scan('signal') -> evaluate expression

Parameters:

Name Type Description Default
filename str | None

path to Nexus file

required
hdf_map NexusMap | None

NexusMap object or None to generate

None
flatten_scannables bool

if True, flattens all scannable arrays to 1D

True
Source code in mmg_toolbox/nexus/nexus_scan.py
class NexusDataHolder(DataHolder, NexusScan):
    """
    Nexus data holder class
     - Automatically reads scannable and metadata from file
     - acts like the old .dat DataHolder class
     - has additional functions to read data from NeXus file

    Example:
        scan = NexusDataHolder('12345.nxs')
        scan.eta -> returns array
        scan.metadata.metadata -> returns value
        scan('signal') -> evaluate expression

    :param filename: path to Nexus file
    :param hdf_map: NexusMap object or None to generate
    :param flatten_scannables: if True, flattens all scannable arrays to 1D
    """
    filename: str
    map: NexusMap
    metadata: DataHolder

    def __init__(self, filename: str | None, hdf_map: NexusMap | None = None, flatten_scannables: bool = True,
                 config: dict | None = None):
        NexusScan.__init__(self, filename, hdf_map, config)

        with load_hdf(filename) as hdf:
            metadata = self.map.get_metadata(hdf)
            scannables = self.map.get_scannables(hdf, flatten=flatten_scannables)
        DataHolder.__init__(self, **scannables)
        self.metadata = DataHolder(**metadata)

    def __repr__(self):
        return f"NexusDataHolder('{self.filename}')"

NexusScan

Bases: NexusLoader

Light-weight NeXus file reader

>>> scan = NexusScan('scan.nxs')
>>> scan('scan_command')
 'scan x ...'
>>> x, y = scan('axes, signal / monitor')
>>> scan.plot()  # default plot
>>> scan.plot.image()  # other plot options
>>> result = scan.fit.multi_peak_fit()
>>> print(scan)  # print scan default metadata
>>> print(scan.info())  # print scan namespace
>>> scan.map.add_roi('name', ...)  # add ROI to namespace
>>> scan.image(0)  # return first detector image as array
>>> scan.volume()  # return image stack
>>> data = scan.get_plot_data()  # return dict of plot data

Parameters:

Name Type Description Default
nxs_filename str

path to nexus file

required
hdf_map NexusMap | None

NexusMap object or None

None
config dict | None

configuration dict

None
Source code in mmg_toolbox/nexus/nexus_scan.py
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
class NexusScan(NexusLoader):
    """
    Light-weight NeXus file reader

        >>> scan = NexusScan('scan.nxs')
        >>> scan('scan_command')
         'scan x ...'
        >>> x, y = scan('axes, signal / monitor')
        >>> scan.plot()  # default plot
        >>> scan.plot.image()  # other plot options
        >>> result = scan.fit.multi_peak_fit()
        >>> print(scan)  # print scan default metadata
        >>> print(scan.info())  # print scan namespace
        >>> scan.map.add_roi('name', ...)  # add ROI to namespace
        >>> scan.image(0)  # return first detector image as array
        >>> scan.volume()  # return image stack
        >>> data = scan.get_plot_data()  # return dict of plot data

    :param nxs_filename: path to nexus file
    :param hdf_map: NexusMap object or None
    :param config: configuration dict
    """
    MAX_STR_LEN: int = 100

    def __init__(self, nxs_filename: str, hdf_map: NexusMap | None = None, config: dict | None = None):
        super().__init__(nxs_filename, hdf_map)
        self.config: dict = config or beamline_config()
        self.beamline = self.config.get(C.beamline, None)

        # add scan number to eval namespace
        self.add_local(scan_number=self.scan_number(), beamline=self.beamline)
        # add alternate names
        self.map.add_named_expression(**self.config.get(C.replace_names, {}))
        # add ROIs
        for (name, cen_i, cen_j, wid_i, wid_j, det_name) in self.config.get(C.roi, []):
            self.map.add_roi(name, cen_i, cen_j, wid_i, wid_j, det_name)

        from mmg_toolbox.fitting import ScanFitManager, poisson_errors
        self.fit = ScanFitManager(self)
        self._error_function = poisson_errors
        from mmg_toolbox.plotting.scan_plot_manager import ScanPlotManager
        self.plot = ScanPlotManager(self)

    def __repr__(self):
        if self.beamline:
            return f"NexusScan<{self.beamline}>({self.scan_number()}: '{self.filename}')"
        return f"NexusScan('{self.filename}')"

    def __str__(self):
        try:
            return self.metadata_str()
        except Exception as ex:
            return f"{repr(self)}\n  Metadata failed with: \n{ex}\n"

    def metadata_str(self, expression: str | None = None):
        """Generate metadata string from beamline config"""
        if expression is None:
            expression = self.config.get(C.metadata_string, '')
        return self.format(expression)

    def info(self, arrays=False, values=False, combined=False,
             metadata=False, scannables=True, image_data=True,
             local=False, alternate=True) -> str:
        """Return string of namespace information"""
        local_data = (
                "Local Data:\n" +
                "\n".join(
                    f"  {name}: {value}" for name, value in self._local_data.items()
                ) +
                "\n"
        ) if local else ""
        alternate_data = (
            "Alternate Names:\n  Name: Expression" +
            "\n".join(
                f"  {name}: '{expr}'" for name, expr in self.map._alternate_names.items()
            ) +
            "\n"
        ) if alternate else ""
        map_data = self.map.info_names(arrays=arrays, values=values, combined=combined,
                                       metadata=metadata, scannables=scannables, image_data=image_data)
        return local_data + alternate_data + map_data

    def rois(self, append: str = '_total') -> list[str]:
        """
        Return ROI expressions available in scan namespace

            rois = [scan.eval(roi) for roi in scan.rois('_total')]

        :param append: str to append to each ROI name, e.g. '_total', '_max', '_min', '_mean'
        :return: list of ROI names that can be used in eval.
        """
        # search for ROIs in HdfMap expressions
        alternate_name_rois = {
            next((name.removesuffix(sfx) for sfx in ROI_SUFFIXES if name.endswith(sfx)), name)
            for name, expression in self.map._alternate_names.items()
            if expression.startswith('d_')
        }
        return [roi + append for roi in alternate_name_rois]

    def scan_number(self) -> int:
        return get_scan_number(self.filename)

    def title(self) -> str:
        return f"#{self.scan_number()}"

    def label(self) -> str:
        return f"#{self.scan_number()}"

    def start_end_duration(self) -> tuple[datetime.datetime, datetime.datetime, datetime.timedelta]:
        """Return start and end times of scan, plus duration as datetime and timedelta objects"""
        scan_start, scan_end = self.times(NX_START, NX_END, default=None)
        if scan_start is None:
            raise KeyError(f"scan start time not found in {self.filename}")
        if scan_end is None:
            scan_end = get_file_time(self.filename)
        return scan_start, scan_end, scan_end - scan_start

    def load_hdf(self) -> h5py.File:
        """Load the Hdf file"""
        return load_hdf(self.filename)

    def hdf_tree_string(self, group: str = '/', all_links: bool = True, attributes: bool = True) -> str:
        """
        Generate string of the hdf file structure, similar to h5ls. Uses h5py.visititems

        :param all_links: bool, if True, also show links
        :param group: only display tree structure of this group (default root)
        :param attributes: if True, display the attributes of groups and datasets
        :return: str
        """
        return hdfmap.hdf_tree_string(self.filename, all_links=all_links, group=group, attributes=attributes)

    def hdf_find(self, *field_or_class: str | list[str], find_all: bool = False) -> (h5py.Dataset | h5py.Group) | list[h5py.Dataset | h5py.Group]:
        """
        Find datasets and groups within hdf file

            HdfDataset = scan.hdf_find('NXentry', ['NXdata', 'measurement'], 'signal')

        Warning: HDF file stays in open state while Dataset or Group objects exist.

        :param field_or_class: names to search for, in hierarchical order. Lists treated as OR
        :param find_all: whether to return all datasets or only the first match
        :returns: matching Dataset or Group, or list of matching Datasets or Groups
        """
        with self.load_hdf() as hdf:
            if find_all:
                return nx_find_all(hdf, *field_or_class)
            return nx_find(hdf, *field_or_class)

    def datasets(self, *args) -> list[h5py.Dataset]:
        """Return HDF5 datasets from NeXus file (leaves file in open state)"""
        with self.load_hdf() as hdf:
            return [hdf[self.map.combined[name]] for name in args]

    def arrays(self, *args, units: str = '', default: np.ndarray = np.array([np.nan])) -> list[np.ndarray]:
        """Return Numpy arrays"""
        with self.load_hdf() as hdf:
            return [
                get_dataset_value(self.map.combined[name], hdf, units=units, default=default)
                for name in args
            ]

    def values(self, *args, value_func=np.mean,
               units: str = '', default: np.ndarray = np.array(np.nan)) -> list[np.floating]:
        """Return float values"""
        with self.load_hdf() as hdf:
            return [
                value_func(get_dataset_value(self.map.combined[name], hdf, units=units, default=default))
                for name in args
            ]

    def times(self, *args, default: datetime.datetime | None = None) -> list[datetime.datetime | None]:
        """Return datetime object"""
        with self.load_hdf() as hdf:
            data = [
                dataset2data(hdf[self.map.combined[name]])
                if name in self.map.combined else default
                for name in args
            ]
            dt = [
                obj if obj is None or isinstance(obj, datetime.datetime) else
                datetime.datetime.fromisoformat(obj) if isinstance(obj, str) else
                datetime.datetime.fromtimestamp(float(obj))
                for obj in data
            ]
        return dt

    def strings(self, *args, units=False) -> list[str]:
        """Return string value"""
        with self.load_hdf() as hdf:
            return [dataset2str(hdf[self.map.combined[name]], units=units) for name in args]

    def image(self, index: int | str | None = None) -> np.ndarray:
        """
        Return image or selection from default detector

            im = scan.image(0)  # First image
            im = scan.image(None)  # middle image
            im = scan.image('sum')  # sum of all images

        :param index: point in the scan to show the image, 'sum' gives sum over all images, None gives the middle image
        :return: (n,m) array
        """
        if not self.map.image_data:
            raise ValueError(f'{repr(self)} contains no image data')
        with self.load_hdf() as hdf:
            if index == 'sum':
                volume = self.map.get_image(hdf, ())
                image = volume.sum(axis=tuple(range(volume.ndim-2)))  # this will fail for filenames
            else:
                image = self.map.get_image(hdf, index)

            if issubclass(type(image), str):
                # TIFF image, NXdetector/image_data -> array('file.tif')
                file_directory = os.path.dirname(self.filename)
                image_filename = os.path.join(file_directory, image)
                if not os.path.isfile(image_filename):
                    raise FileNotFoundError(f"File not found: {image_filename}")
                image = read_tiff(image_filename)
            elif image.ndim == 0:
                # image is file path number, NXdetector/path -> arange(n_points)
                scan_number = get_scan_number(self.filename)
                file_directory = os.path.dirname(self.filename)
                detector_names = list(self.map.image_data.keys())
                for detector_name in detector_names:
                    image_filename = os.path.join(file_directory, f"{scan_number}-{detector_name}-files/{image:05.0f}.tif")
                    if os.path.isfile(image_filename):
                        break
                if not os.path.isfile(image_filename):
                    raise FileNotFoundError(f"File not found: {image_filename}")
                image = read_tiff(image_filename)
            elif image.ndim != 2:
                raise Exception(f"detector image[{index}] is the wrong shape: {image.shape}")
            return image

    def volume(self) -> np.ndarray:
        """Return complete stack of images"""
        return self.map.get_image(self.load_hdf(), ())

    def image_background(self, index: int | tuple | slice | str | None = (), n_bins: int  = 100) -> np.ndarray:
        """
        Return the modal value of the detector image,
        which usually gives the background value.

        The modal value is determined by histograming the image (or image stack) and taking
        the value of the largest bin.

        :param index: index of image to return, use () for full image stack.
        :param n_bins: number of histogram bins
        :return: modal value or values per image
        """
        image = self.map.get_image(self.load_hdf(), index)  # hdf data only
        n, bins = np.histogram(np.log10(image[image>0].flatten()), bins=n_bins)
        return 10 ** bins[np.argmax(n)]

    def get_max_index(self, expression: str = 'IMAGE') -> tuple[float, tuple[int, ...]]:
        """Return the index and value of the maximum pixel in a detector image or volume"""
        data = self.eval(expression)
        max_idx = tuple(int(v) for v in np.unravel_index(np.argmax(data), data.shape))
        return float(data[max_idx]), max_idx

    def table(self, delimiter=', ', string_spec='', format_spec='f', default_decimals=8) -> str:
        """Return data table"""
        with self.load_hdf() as hdf:
            return self.map.create_scannables_table(hdf, delimiter, string_spec, format_spec, default_decimals)

    def get_scan_time(self) -> np.ndarray:
        """Return array of datetimes for each scan point"""
        start, stop, duration = self.start_end_duration()
        count_time = self.eval('_t', default=None)  # '_t' is an alternate for count_time and others
        if count_time is None:
            # assume regular spacing in time
            shape = self.map.scannables_shape()
            time_per_point = duration.total_seconds() / np.prod(*shape)
            count_time = np.full(shape, time_per_point)
        return np.array([start + datetime.timedelta(seconds=x) for x in np.cumsum(count_time)])

    def replace_default_names(self, expression: str) -> str:
        """Replace 'axes', 'signal' and 'IMAGE' in expression with correct values"""
        axes_names, signal_names = self.map.nexus_default_names()
        detector_names = list(self.map.image_data)
        # Replace axes instances
        expression = re.sub(
            r'axes(\d*)',
            lambda m: list(axes_names)[int(m.group(1) or 0)],
            expression
        )
        # Replace signal instances
        expression = re.sub(
            r'signal(\d*)',
            lambda m: list(signal_names)[int(m.group(1) or 0)],
            expression
        )
        # Replace IMAGE instances
        expression = re.sub(
            r'IMAGE(\d*)',
            lambda m: list(detector_names)[int(m.group(1) or 0)],
            expression
        )
        # Replace other HdfMap names
        replace_names = {name: self.map.generate_ids(name)[0] for name in self.map if name in expression}
        expression = multiple_replace(replace_names, expression)
        return expression

    def _get_plot_axis(self, hdf: h5py.File | h5py.Group, axis_name: str,
                       reduce_shape: bool = True, flatten: bool = False) -> tuple[np.ndarray, str]:
        """
        Return plot axis data and label for given axis name

            data, label = scan.get_plot_axis('axes', flatten=True)

        :param hdf: h5py.File or h5py.Group
        :param axis_name: axis name as given in self.map
        :param reduce_shape: reduces shape (summing additional axes) of >2D arrays to self.map.scannables_shape
        :param flatten: flatten data array if True
        :return: (data, label) tuple
        """
        # Default scannables if not generated by hdfmap
        label = axis_name = self.replace_default_names(axis_name)
        data = self.map.eval(hdf, axis_name, local_data=self._local_data)
        if np.ndim(data) > 1 and reduce_shape:
            # reduce high dimensional arrays to the default scannable shape
            shape = self.map.scannables_shape()
            if np.ndim(data) == len(shape) + 2:
                # Image data
                data = np.sum(data, axis=(-1, -2))
            if np.shape(data) != shape:
                raise ValueError(f"2+D Arrays must have same shape: {axis_name}{np.shape(data)} != {shape}")
        if flatten:
            data = np.reshape(data, -1)
        return data, label

    def get_plot_axis(self, axis_name: str, reduce_shape: bool = True, flatten: bool = False) -> tuple[np.ndarray, str]:
        """
        Return plot axis data and label for given axis name

            data, label = scan.get_plot_axis('axes', flatten=True)

        :param axis_name: axis name as given in self.map
        :param reduce_shape: reduces shape (summing additional axes) of >2D arrays to self.map.scannables_shape
        :param flatten: flattens output if True
        :return: (data, label) tuple
        """
        with self.load_hdf() as hdf:
            return self._get_plot_axis(hdf, axis_name, reduce_shape=reduce_shape, flatten=flatten)

    def get_plot_data(self, x_axis: str | None = None, *y_axis: str | None, z_axis: str | None = None) -> dict:
        """
        Return dict of plottable data

            data = scan.get_plot_data('axes', 'signal')
            plt.plot(data['x'], data['y'])
            plt.xlabel(data['xlabel'])
            plt.ylabel(data['ylabel'])
            plt.title(data['title'])
            plt.legend(data['legend'])

        :param x_axis: axis name or expression as given in self.map
        :param y_axis: axis name or expression as given in self.map
        :param z_axis: axis name or expression as given in self.map
        :returns: {
            'xlabel': str label of first axes
            'ylabel': str label of first signal
            'xdata': flattened array of first axes
            'ydata': flattened array of first signal
            'axes_names': list of axes names,
            'signal_names': list of signal + auxiliary signal names,
            'axes_data': list of ND arrays of data for axes,
            'signal_data': list of ND array of data for signal + auxiliary signals,
            'axes_labels': list of axes labels as 'name [units]',
            'signal_labels': list of signal labels,
            'data': dict of all scannables axes,
            'title': str title as 'filename\nNXtitle'
        if dataset is a 2D grid scan, additional rows:
            'grid_xlabel': str label of grid x-axis
            'grid_ylabel': str label of grid y-axis
            'grid_label': str label of height or colour
            'grid_xdata': 2D array of x-coordinates
            'grid_ydata': 2D array of y-coordinates
            'grid_data': 2D array of height or colour
        }
        """
        with self.load_hdf() as hdf:
            data = self.map.get_plot_data(hdf)
            cmd = self.map.eval(hdf, Md.cmd)
            if len(cmd) > self.MAX_STR_LEN:
                cmd = shorten_string(cmd)
            x_data, x_lab = self._get_plot_axis(hdf, x_axis or 'axes', reduce_shape=True, flatten=True)

            y_data, y_labs = [], []
            y_axis = y_axis or [None]
            for n, _y_axis in enumerate(y_axis):
                _y_data, _y_lab = self._get_plot_axis(hdf, _y_axis or f"signal{n}", reduce_shape=True, flatten=True)
                y_data.append(_y_data)
                y_labs.append(_y_lab)
            y_data = np.array(y_data)
            y_error = self._error_function(y_data)

            additional = {
                'x': x_data,
                'y': y_data[0],
                'xdata': x_data,
                'ydata': y_data.T,
                'yerror': y_error.T,
                'xlabel': x_lab,
                'ylabel': y_labs[0],
                'title': f"#{self.scan_number()}\n{cmd}",
                'legend': y_labs
            }
            if z_axis is not None:
                z_data, z_lab = self._get_plot_axis(hdf, z_axis, reduce_shape=True, flatten=True)
                additional['zdata'] = z_data
                additional['zlabel'] = z_lab
            # 2+D data
            shape = self.map.scannables_shape()
            if len(shape) >= 2:
                x_data = x_data.reshape(shape)
                y_data, y_lab = self._get_plot_axis(hdf, y_axis[0] or 'axes1')
                z_data, z_lab = self._get_plot_axis(hdf,
                    y_axis[1] if len(y_axis) > 1 else z_axis or 'signal',
                )
                # reduce dimensions to 2
                #TODO: taking the first dimension might not always be right
                x_data = x_data[(..., ) + (0, ) * (x_data.ndim - 2)]
                y_data = y_data[(..., ) + (0, ) * (y_data.ndim - 2)]
                z_data = z_data.sum(axis=tuple(range(2, z_data.ndim)))

                if y_data.shape != x_data.shape:
                    raise ValueError(f"Shape of '{y_lab}' {y_data.shape} != '{x_lab}' {x_data.shape}")
                if z_data.shape != x_data.shape:
                    raise ValueError(f"Shape of '{z_lab}' {z_data.shape} != '{x_lab}' {x_data.shape}")

                additional['grid_xlabel'] = x_lab
                additional['grid_ylabel'] = y_lab
                additional['grid_label'] = z_lab
                additional['grid_xdata'] = x_data
                additional['grid_ydata'] = y_data
                additional['grid_data'] = z_data
            data.update(additional)
            return data

    def xas_spectra(self, sample_name: str | None = None, element_edge: str | None = None, mode: str | list[str] = 'all',
                    dls_loader: bool = False) -> SpectraContainer:
        """
        Load XAS Spectra from the scan file

            >>> spectra = scan.xas_spectra()
            >>> spectra = spectra.remove_background('slope')
            >>> spectra.plot()

        :param sample_name: sample name, e.g. 'sample1' or None to load from NeXus file
        :param element_edge: element edge, e.g. 'FeL3' or None to determine from energy range
        :param mode: detector values to load, 'all', 'default' or e.g. 'tey', 'tfy' as specified in file
        :param dls_loader: bool, if True uses explicit loading of metadata from DLS MMG beamlines
        :return: SpectraContainer
        """
        return load_xas_scans(self.filename, sample_name=sample_name, element_edge=element_edge,
                              mode=mode, dls_loader=dls_loader, hdfmap=self.map)[0]

    def instrument_model(self) -> NXInstrumentModel:
        """Build and instrument model from NeXus file"""
        with self.load_hdf() as hdf:
            return NXInstrumentModel(hdf)

    def save(self, filename: str):
        """Save object as HDF5 file"""
        from mmg_toolbox import __version__
        date = str(datetime.datetime.now())

        # Write new file
        with h5py.File(filename, 'w') as f:
            nw.add_entry_links(f, self.filename)
            entry = nw.add_nxentry(f, 'NexusScan', default=False)
            # Filename
            nw.add_nxfield(entry, 'filename', self.filename)
            # LocalData
            nw.add_nxprocess(
                root=entry,
                name='local_data',
                program='mmg_toolbox.nexus.nexus_scan',
                version=__version__,
                date=date,
                **self._local_data  # save local_data as NXparameters
            )
            nw.add_nxnote(
                root=entry,
                name='config',
                description='NexusScan configuration',
                data=self.config,
            )

    def load_local_data(self):
        """If the scan file contains previously saved NexusScan information, the local data and config will be loaded."""

        with self.load_hdf() as hdf:
            orig_filename, local_data, config = _load_local_data(hdf)

        self.__init__(orig_filename, None, config=config)
        self.add_local(**local_data)

    def save_nxdata(self, root: h5py.Group, name: str = 'data', x_axis: str | None = None, *y_axis: str | None,
                    z_axis: str | None = None, default: bool = False):
        """Save scan data as NXdata group"""
        plot_data = self.get_plot_data(x_axis, *y_axis, z_axis=z_axis)
        nx_data = nw.add_nxdata(
            root=root,
            name=name,
            axes=plot_data['axes_names'],
            signal=plot_data['signal_names'][0],
            default=default
        )
        for name, data in zip(plot_data['axes_names'], plot_data['axes_data']):
            nw.add_nxfield(nx_data, name, data)
        for name, data in zip(plot_data['signal_names'], plot_data['signal_data']):
            nw.add_nxfield(nx_data, name, data)

    def save_csv(self, filename: str, *scannables: str):
        """Save axes and signal as CSV file"""
        # Load scannables data
        names = (self.replace_default_names(scannable) for scannable in scannables)
        header = '# mmg_toolbox.nexus.nexus_scan\n'
        header += '# ' + ', '.join(names)
        scannable_arrays = self.eval(','.join(scannables))
        array_len = max(len(array) for array in scannable_arrays)
        scannable_arrays = [
            np.tile(array, array_len) if array.size == 1 else array
            for array in scannable_arrays
        ]
        with open(filename, 'w') as f:
            f.write(header)
            f.write('\n')
            for values in zip(*scannable_arrays):
                f.write(', '.join(map(str, values)))
                f.write('\n')

arrays(*args, units='', default=np.array([np.nan]))

Return Numpy arrays

Source code in mmg_toolbox/nexus/nexus_scan.py
def arrays(self, *args, units: str = '', default: np.ndarray = np.array([np.nan])) -> list[np.ndarray]:
    """Return Numpy arrays"""
    with self.load_hdf() as hdf:
        return [
            get_dataset_value(self.map.combined[name], hdf, units=units, default=default)
            for name in args
        ]

datasets(*args)

Return HDF5 datasets from NeXus file (leaves file in open state)

Source code in mmg_toolbox/nexus/nexus_scan.py
def datasets(self, *args) -> list[h5py.Dataset]:
    """Return HDF5 datasets from NeXus file (leaves file in open state)"""
    with self.load_hdf() as hdf:
        return [hdf[self.map.combined[name]] for name in args]

get_max_index(expression='IMAGE')

Return the index and value of the maximum pixel in a detector image or volume

Source code in mmg_toolbox/nexus/nexus_scan.py
def get_max_index(self, expression: str = 'IMAGE') -> tuple[float, tuple[int, ...]]:
    """Return the index and value of the maximum pixel in a detector image or volume"""
    data = self.eval(expression)
    max_idx = tuple(int(v) for v in np.unravel_index(np.argmax(data), data.shape))
    return float(data[max_idx]), max_idx

get_plot_axis(axis_name, reduce_shape=True, flatten=False)

Return plot axis data and label for given axis name

data, label = scan.get_plot_axis('axes', flatten=True)

Parameters:

Name Type Description Default
axis_name str

axis name as given in self.map

required
reduce_shape bool

reduces shape (summing additional axes) of >2D arrays to self.map.scannables_shape

True
flatten bool

flattens output if True

False

Returns:

Type Description
tuple[ndarray, str]

(data, label) tuple

Source code in mmg_toolbox/nexus/nexus_scan.py
def get_plot_axis(self, axis_name: str, reduce_shape: bool = True, flatten: bool = False) -> tuple[np.ndarray, str]:
    """
    Return plot axis data and label for given axis name

        data, label = scan.get_plot_axis('axes', flatten=True)

    :param axis_name: axis name as given in self.map
    :param reduce_shape: reduces shape (summing additional axes) of >2D arrays to self.map.scannables_shape
    :param flatten: flattens output if True
    :return: (data, label) tuple
    """
    with self.load_hdf() as hdf:
        return self._get_plot_axis(hdf, axis_name, reduce_shape=reduce_shape, flatten=flatten)

get_plot_data(x_axis=None, *y_axis, z_axis=None)

    Return dict of plottable data

        data = scan.get_plot_data('axes', 'signal')
        plt.plot(data['x'], data['y'])
        plt.xlabel(data['xlabel'])
        plt.ylabel(data['ylabel'])
        plt.title(data['title'])
        plt.legend(data['legend'])

    :param x_axis: axis name or expression as given in self.map
    :param y_axis: axis name or expression as given in self.map
    :param z_axis: axis name or expression as given in self.map
    :returns: {
        'xlabel': str label of first axes
        'ylabel': str label of first signal
        'xdata': flattened array of first axes
        'ydata': flattened array of first signal
        'axes_names': list of axes names,
        'signal_names': list of signal + auxiliary signal names,
        'axes_data': list of ND arrays of data for axes,
        'signal_data': list of ND array of data for signal + auxiliary signals,
        'axes_labels': list of axes labels as 'name [units]',
        'signal_labels': list of signal labels,
        'data': dict of all scannables axes,
        'title': str title as 'filename

NXtitle' if dataset is a 2D grid scan, additional rows: 'grid_xlabel': str label of grid x-axis 'grid_ylabel': str label of grid y-axis 'grid_label': str label of height or colour 'grid_xdata': 2D array of x-coordinates 'grid_ydata': 2D array of y-coordinates 'grid_data': 2D array of height or colour }

Source code in mmg_toolbox/nexus/nexus_scan.py
def get_plot_data(self, x_axis: str | None = None, *y_axis: str | None, z_axis: str | None = None) -> dict:
    """
    Return dict of plottable data

        data = scan.get_plot_data('axes', 'signal')
        plt.plot(data['x'], data['y'])
        plt.xlabel(data['xlabel'])
        plt.ylabel(data['ylabel'])
        plt.title(data['title'])
        plt.legend(data['legend'])

    :param x_axis: axis name or expression as given in self.map
    :param y_axis: axis name or expression as given in self.map
    :param z_axis: axis name or expression as given in self.map
    :returns: {
        'xlabel': str label of first axes
        'ylabel': str label of first signal
        'xdata': flattened array of first axes
        'ydata': flattened array of first signal
        'axes_names': list of axes names,
        'signal_names': list of signal + auxiliary signal names,
        'axes_data': list of ND arrays of data for axes,
        'signal_data': list of ND array of data for signal + auxiliary signals,
        'axes_labels': list of axes labels as 'name [units]',
        'signal_labels': list of signal labels,
        'data': dict of all scannables axes,
        'title': str title as 'filename\nNXtitle'
    if dataset is a 2D grid scan, additional rows:
        'grid_xlabel': str label of grid x-axis
        'grid_ylabel': str label of grid y-axis
        'grid_label': str label of height or colour
        'grid_xdata': 2D array of x-coordinates
        'grid_ydata': 2D array of y-coordinates
        'grid_data': 2D array of height or colour
    }
    """
    with self.load_hdf() as hdf:
        data = self.map.get_plot_data(hdf)
        cmd = self.map.eval(hdf, Md.cmd)
        if len(cmd) > self.MAX_STR_LEN:
            cmd = shorten_string(cmd)
        x_data, x_lab = self._get_plot_axis(hdf, x_axis or 'axes', reduce_shape=True, flatten=True)

        y_data, y_labs = [], []
        y_axis = y_axis or [None]
        for n, _y_axis in enumerate(y_axis):
            _y_data, _y_lab = self._get_plot_axis(hdf, _y_axis or f"signal{n}", reduce_shape=True, flatten=True)
            y_data.append(_y_data)
            y_labs.append(_y_lab)
        y_data = np.array(y_data)
        y_error = self._error_function(y_data)

        additional = {
            'x': x_data,
            'y': y_data[0],
            'xdata': x_data,
            'ydata': y_data.T,
            'yerror': y_error.T,
            'xlabel': x_lab,
            'ylabel': y_labs[0],
            'title': f"#{self.scan_number()}\n{cmd}",
            'legend': y_labs
        }
        if z_axis is not None:
            z_data, z_lab = self._get_plot_axis(hdf, z_axis, reduce_shape=True, flatten=True)
            additional['zdata'] = z_data
            additional['zlabel'] = z_lab
        # 2+D data
        shape = self.map.scannables_shape()
        if len(shape) >= 2:
            x_data = x_data.reshape(shape)
            y_data, y_lab = self._get_plot_axis(hdf, y_axis[0] or 'axes1')
            z_data, z_lab = self._get_plot_axis(hdf,
                y_axis[1] if len(y_axis) > 1 else z_axis or 'signal',
            )
            # reduce dimensions to 2
            #TODO: taking the first dimension might not always be right
            x_data = x_data[(..., ) + (0, ) * (x_data.ndim - 2)]
            y_data = y_data[(..., ) + (0, ) * (y_data.ndim - 2)]
            z_data = z_data.sum(axis=tuple(range(2, z_data.ndim)))

            if y_data.shape != x_data.shape:
                raise ValueError(f"Shape of '{y_lab}' {y_data.shape} != '{x_lab}' {x_data.shape}")
            if z_data.shape != x_data.shape:
                raise ValueError(f"Shape of '{z_lab}' {z_data.shape} != '{x_lab}' {x_data.shape}")

            additional['grid_xlabel'] = x_lab
            additional['grid_ylabel'] = y_lab
            additional['grid_label'] = z_lab
            additional['grid_xdata'] = x_data
            additional['grid_ydata'] = y_data
            additional['grid_data'] = z_data
        data.update(additional)
        return data

get_scan_time()

Return array of datetimes for each scan point

Source code in mmg_toolbox/nexus/nexus_scan.py
def get_scan_time(self) -> np.ndarray:
    """Return array of datetimes for each scan point"""
    start, stop, duration = self.start_end_duration()
    count_time = self.eval('_t', default=None)  # '_t' is an alternate for count_time and others
    if count_time is None:
        # assume regular spacing in time
        shape = self.map.scannables_shape()
        time_per_point = duration.total_seconds() / np.prod(*shape)
        count_time = np.full(shape, time_per_point)
    return np.array([start + datetime.timedelta(seconds=x) for x in np.cumsum(count_time)])

hdf_find(*field_or_class, find_all=False)

Find datasets and groups within hdf file

HdfDataset = scan.hdf_find('NXentry', ['NXdata', 'measurement'], 'signal')

Warning: HDF file stays in open state while Dataset or Group objects exist.

Parameters:

Name Type Description Default
field_or_class str | list[str]

names to search for, in hierarchical order. Lists treated as OR

()
find_all bool

whether to return all datasets or only the first match

False

Returns:

Type Description
Dataset | Group | list[Dataset | Group]

matching Dataset or Group, or list of matching Datasets or Groups

Source code in mmg_toolbox/nexus/nexus_scan.py
def hdf_find(self, *field_or_class: str | list[str], find_all: bool = False) -> (h5py.Dataset | h5py.Group) | list[h5py.Dataset | h5py.Group]:
    """
    Find datasets and groups within hdf file

        HdfDataset = scan.hdf_find('NXentry', ['NXdata', 'measurement'], 'signal')

    Warning: HDF file stays in open state while Dataset or Group objects exist.

    :param field_or_class: names to search for, in hierarchical order. Lists treated as OR
    :param find_all: whether to return all datasets or only the first match
    :returns: matching Dataset or Group, or list of matching Datasets or Groups
    """
    with self.load_hdf() as hdf:
        if find_all:
            return nx_find_all(hdf, *field_or_class)
        return nx_find(hdf, *field_or_class)

hdf_tree_string(group='/', all_links=True, attributes=True)

Generate string of the hdf file structure, similar to h5ls. Uses h5py.visititems

Parameters:

Name Type Description Default
all_links bool

bool, if True, also show links

True
group str

only display tree structure of this group (default root)

'/'
attributes bool

if True, display the attributes of groups and datasets

True

Returns:

Type Description
str

str

Source code in mmg_toolbox/nexus/nexus_scan.py
def hdf_tree_string(self, group: str = '/', all_links: bool = True, attributes: bool = True) -> str:
    """
    Generate string of the hdf file structure, similar to h5ls. Uses h5py.visititems

    :param all_links: bool, if True, also show links
    :param group: only display tree structure of this group (default root)
    :param attributes: if True, display the attributes of groups and datasets
    :return: str
    """
    return hdfmap.hdf_tree_string(self.filename, all_links=all_links, group=group, attributes=attributes)

image(index=None)

Return image or selection from default detector

im = scan.image(0)  # First image
im = scan.image(None)  # middle image
im = scan.image('sum')  # sum of all images

Parameters:

Name Type Description Default
index int | str | None

point in the scan to show the image, 'sum' gives sum over all images, None gives the middle image

None

Returns:

Type Description
ndarray

(n,m) array

Source code in mmg_toolbox/nexus/nexus_scan.py
def image(self, index: int | str | None = None) -> np.ndarray:
    """
    Return image or selection from default detector

        im = scan.image(0)  # First image
        im = scan.image(None)  # middle image
        im = scan.image('sum')  # sum of all images

    :param index: point in the scan to show the image, 'sum' gives sum over all images, None gives the middle image
    :return: (n,m) array
    """
    if not self.map.image_data:
        raise ValueError(f'{repr(self)} contains no image data')
    with self.load_hdf() as hdf:
        if index == 'sum':
            volume = self.map.get_image(hdf, ())
            image = volume.sum(axis=tuple(range(volume.ndim-2)))  # this will fail for filenames
        else:
            image = self.map.get_image(hdf, index)

        if issubclass(type(image), str):
            # TIFF image, NXdetector/image_data -> array('file.tif')
            file_directory = os.path.dirname(self.filename)
            image_filename = os.path.join(file_directory, image)
            if not os.path.isfile(image_filename):
                raise FileNotFoundError(f"File not found: {image_filename}")
            image = read_tiff(image_filename)
        elif image.ndim == 0:
            # image is file path number, NXdetector/path -> arange(n_points)
            scan_number = get_scan_number(self.filename)
            file_directory = os.path.dirname(self.filename)
            detector_names = list(self.map.image_data.keys())
            for detector_name in detector_names:
                image_filename = os.path.join(file_directory, f"{scan_number}-{detector_name}-files/{image:05.0f}.tif")
                if os.path.isfile(image_filename):
                    break
            if not os.path.isfile(image_filename):
                raise FileNotFoundError(f"File not found: {image_filename}")
            image = read_tiff(image_filename)
        elif image.ndim != 2:
            raise Exception(f"detector image[{index}] is the wrong shape: {image.shape}")
        return image

image_background(index=(), n_bins=100)

Return the modal value of the detector image, which usually gives the background value.

The modal value is determined by histograming the image (or image stack) and taking the value of the largest bin.

Parameters:

Name Type Description Default
index int | tuple | slice | str | None

index of image to return, use () for full image stack.

()
n_bins int

number of histogram bins

100

Returns:

Type Description
ndarray

modal value or values per image

Source code in mmg_toolbox/nexus/nexus_scan.py
def image_background(self, index: int | tuple | slice | str | None = (), n_bins: int  = 100) -> np.ndarray:
    """
    Return the modal value of the detector image,
    which usually gives the background value.

    The modal value is determined by histograming the image (or image stack) and taking
    the value of the largest bin.

    :param index: index of image to return, use () for full image stack.
    :param n_bins: number of histogram bins
    :return: modal value or values per image
    """
    image = self.map.get_image(self.load_hdf(), index)  # hdf data only
    n, bins = np.histogram(np.log10(image[image>0].flatten()), bins=n_bins)
    return 10 ** bins[np.argmax(n)]

info(arrays=False, values=False, combined=False, metadata=False, scannables=True, image_data=True, local=False, alternate=True)

Return string of namespace information

Source code in mmg_toolbox/nexus/nexus_scan.py
def info(self, arrays=False, values=False, combined=False,
         metadata=False, scannables=True, image_data=True,
         local=False, alternate=True) -> str:
    """Return string of namespace information"""
    local_data = (
            "Local Data:\n" +
            "\n".join(
                f"  {name}: {value}" for name, value in self._local_data.items()
            ) +
            "\n"
    ) if local else ""
    alternate_data = (
        "Alternate Names:\n  Name: Expression" +
        "\n".join(
            f"  {name}: '{expr}'" for name, expr in self.map._alternate_names.items()
        ) +
        "\n"
    ) if alternate else ""
    map_data = self.map.info_names(arrays=arrays, values=values, combined=combined,
                                   metadata=metadata, scannables=scannables, image_data=image_data)
    return local_data + alternate_data + map_data

instrument_model()

Build and instrument model from NeXus file

Source code in mmg_toolbox/nexus/nexus_scan.py
def instrument_model(self) -> NXInstrumentModel:
    """Build and instrument model from NeXus file"""
    with self.load_hdf() as hdf:
        return NXInstrumentModel(hdf)

load_hdf()

Load the Hdf file

Source code in mmg_toolbox/nexus/nexus_scan.py
def load_hdf(self) -> h5py.File:
    """Load the Hdf file"""
    return load_hdf(self.filename)

load_local_data()

If the scan file contains previously saved NexusScan information, the local data and config will be loaded.

Source code in mmg_toolbox/nexus/nexus_scan.py
def load_local_data(self):
    """If the scan file contains previously saved NexusScan information, the local data and config will be loaded."""

    with self.load_hdf() as hdf:
        orig_filename, local_data, config = _load_local_data(hdf)

    self.__init__(orig_filename, None, config=config)
    self.add_local(**local_data)

metadata_str(expression=None)

Generate metadata string from beamline config

Source code in mmg_toolbox/nexus/nexus_scan.py
def metadata_str(self, expression: str | None = None):
    """Generate metadata string from beamline config"""
    if expression is None:
        expression = self.config.get(C.metadata_string, '')
    return self.format(expression)

replace_default_names(expression)

Replace 'axes', 'signal' and 'IMAGE' in expression with correct values

Source code in mmg_toolbox/nexus/nexus_scan.py
def replace_default_names(self, expression: str) -> str:
    """Replace 'axes', 'signal' and 'IMAGE' in expression with correct values"""
    axes_names, signal_names = self.map.nexus_default_names()
    detector_names = list(self.map.image_data)
    # Replace axes instances
    expression = re.sub(
        r'axes(\d*)',
        lambda m: list(axes_names)[int(m.group(1) or 0)],
        expression
    )
    # Replace signal instances
    expression = re.sub(
        r'signal(\d*)',
        lambda m: list(signal_names)[int(m.group(1) or 0)],
        expression
    )
    # Replace IMAGE instances
    expression = re.sub(
        r'IMAGE(\d*)',
        lambda m: list(detector_names)[int(m.group(1) or 0)],
        expression
    )
    # Replace other HdfMap names
    replace_names = {name: self.map.generate_ids(name)[0] for name in self.map if name in expression}
    expression = multiple_replace(replace_names, expression)
    return expression

rois(append='_total')

Return ROI expressions available in scan namespace

rois = [scan.eval(roi) for roi in scan.rois('_total')]

Parameters:

Name Type Description Default
append str

str to append to each ROI name, e.g. '_total', '_max', '_min', '_mean'

'_total'

Returns:

Type Description
list[str]

list of ROI names that can be used in eval.

Source code in mmg_toolbox/nexus/nexus_scan.py
def rois(self, append: str = '_total') -> list[str]:
    """
    Return ROI expressions available in scan namespace

        rois = [scan.eval(roi) for roi in scan.rois('_total')]

    :param append: str to append to each ROI name, e.g. '_total', '_max', '_min', '_mean'
    :return: list of ROI names that can be used in eval.
    """
    # search for ROIs in HdfMap expressions
    alternate_name_rois = {
        next((name.removesuffix(sfx) for sfx in ROI_SUFFIXES if name.endswith(sfx)), name)
        for name, expression in self.map._alternate_names.items()
        if expression.startswith('d_')
    }
    return [roi + append for roi in alternate_name_rois]

save(filename)

Save object as HDF5 file

Source code in mmg_toolbox/nexus/nexus_scan.py
def save(self, filename: str):
    """Save object as HDF5 file"""
    from mmg_toolbox import __version__
    date = str(datetime.datetime.now())

    # Write new file
    with h5py.File(filename, 'w') as f:
        nw.add_entry_links(f, self.filename)
        entry = nw.add_nxentry(f, 'NexusScan', default=False)
        # Filename
        nw.add_nxfield(entry, 'filename', self.filename)
        # LocalData
        nw.add_nxprocess(
            root=entry,
            name='local_data',
            program='mmg_toolbox.nexus.nexus_scan',
            version=__version__,
            date=date,
            **self._local_data  # save local_data as NXparameters
        )
        nw.add_nxnote(
            root=entry,
            name='config',
            description='NexusScan configuration',
            data=self.config,
        )

save_csv(filename, *scannables)

Save axes and signal as CSV file

Source code in mmg_toolbox/nexus/nexus_scan.py
def save_csv(self, filename: str, *scannables: str):
    """Save axes and signal as CSV file"""
    # Load scannables data
    names = (self.replace_default_names(scannable) for scannable in scannables)
    header = '# mmg_toolbox.nexus.nexus_scan\n'
    header += '# ' + ', '.join(names)
    scannable_arrays = self.eval(','.join(scannables))
    array_len = max(len(array) for array in scannable_arrays)
    scannable_arrays = [
        np.tile(array, array_len) if array.size == 1 else array
        for array in scannable_arrays
    ]
    with open(filename, 'w') as f:
        f.write(header)
        f.write('\n')
        for values in zip(*scannable_arrays):
            f.write(', '.join(map(str, values)))
            f.write('\n')

save_nxdata(root, name='data', x_axis=None, *y_axis, z_axis=None, default=False)

Save scan data as NXdata group

Source code in mmg_toolbox/nexus/nexus_scan.py
def save_nxdata(self, root: h5py.Group, name: str = 'data', x_axis: str | None = None, *y_axis: str | None,
                z_axis: str | None = None, default: bool = False):
    """Save scan data as NXdata group"""
    plot_data = self.get_plot_data(x_axis, *y_axis, z_axis=z_axis)
    nx_data = nw.add_nxdata(
        root=root,
        name=name,
        axes=plot_data['axes_names'],
        signal=plot_data['signal_names'][0],
        default=default
    )
    for name, data in zip(plot_data['axes_names'], plot_data['axes_data']):
        nw.add_nxfield(nx_data, name, data)
    for name, data in zip(plot_data['signal_names'], plot_data['signal_data']):
        nw.add_nxfield(nx_data, name, data)

start_end_duration()

Return start and end times of scan, plus duration as datetime and timedelta objects

Source code in mmg_toolbox/nexus/nexus_scan.py
def start_end_duration(self) -> tuple[datetime.datetime, datetime.datetime, datetime.timedelta]:
    """Return start and end times of scan, plus duration as datetime and timedelta objects"""
    scan_start, scan_end = self.times(NX_START, NX_END, default=None)
    if scan_start is None:
        raise KeyError(f"scan start time not found in {self.filename}")
    if scan_end is None:
        scan_end = get_file_time(self.filename)
    return scan_start, scan_end, scan_end - scan_start

strings(*args, units=False)

Return string value

Source code in mmg_toolbox/nexus/nexus_scan.py
def strings(self, *args, units=False) -> list[str]:
    """Return string value"""
    with self.load_hdf() as hdf:
        return [dataset2str(hdf[self.map.combined[name]], units=units) for name in args]

table(delimiter=', ', string_spec='', format_spec='f', default_decimals=8)

Return data table

Source code in mmg_toolbox/nexus/nexus_scan.py
def table(self, delimiter=', ', string_spec='', format_spec='f', default_decimals=8) -> str:
    """Return data table"""
    with self.load_hdf() as hdf:
        return self.map.create_scannables_table(hdf, delimiter, string_spec, format_spec, default_decimals)

times(*args, default=None)

Return datetime object

Source code in mmg_toolbox/nexus/nexus_scan.py
def times(self, *args, default: datetime.datetime | None = None) -> list[datetime.datetime | None]:
    """Return datetime object"""
    with self.load_hdf() as hdf:
        data = [
            dataset2data(hdf[self.map.combined[name]])
            if name in self.map.combined else default
            for name in args
        ]
        dt = [
            obj if obj is None or isinstance(obj, datetime.datetime) else
            datetime.datetime.fromisoformat(obj) if isinstance(obj, str) else
            datetime.datetime.fromtimestamp(float(obj))
            for obj in data
        ]
    return dt

values(*args, value_func=np.mean, units='', default=np.array(np.nan))

Return float values

Source code in mmg_toolbox/nexus/nexus_scan.py
def values(self, *args, value_func=np.mean,
           units: str = '', default: np.ndarray = np.array(np.nan)) -> list[np.floating]:
    """Return float values"""
    with self.load_hdf() as hdf:
        return [
            value_func(get_dataset_value(self.map.combined[name], hdf, units=units, default=default))
            for name in args
        ]

volume()

Return complete stack of images

Source code in mmg_toolbox/nexus/nexus_scan.py
def volume(self) -> np.ndarray:
    """Return complete stack of images"""
    return self.map.get_image(self.load_hdf(), ())

xas_spectra(sample_name=None, element_edge=None, mode='all', dls_loader=False)

Load XAS Spectra from the scan file

>>> spectra = scan.xas_spectra()
>>> spectra = spectra.remove_background('slope')
>>> spectra.plot()

Parameters:

Name Type Description Default
sample_name str | None

sample name, e.g. 'sample1' or None to load from NeXus file

None
element_edge str | None

element edge, e.g. 'FeL3' or None to determine from energy range

None
mode str | list[str]

detector values to load, 'all', 'default' or e.g. 'tey', 'tfy' as specified in file

'all'
dls_loader bool

bool, if True uses explicit loading of metadata from DLS MMG beamlines

False

Returns:

Type Description
SpectraContainer

SpectraContainer

Source code in mmg_toolbox/nexus/nexus_scan.py
def xas_spectra(self, sample_name: str | None = None, element_edge: str | None = None, mode: str | list[str] = 'all',
                dls_loader: bool = False) -> SpectraContainer:
    """
    Load XAS Spectra from the scan file

        >>> spectra = scan.xas_spectra()
        >>> spectra = spectra.remove_background('slope')
        >>> spectra.plot()

    :param sample_name: sample name, e.g. 'sample1' or None to load from NeXus file
    :param element_edge: element edge, e.g. 'FeL3' or None to determine from energy range
    :param mode: detector values to load, 'all', 'default' or e.g. 'tey', 'tfy' as specified in file
    :param dls_loader: bool, if True uses explicit loading of metadata from DLS MMG beamlines
    :return: SpectraContainer
    """
    return load_xas_scans(self.filename, sample_name=sample_name, element_edge=element_edge,
                          mode=mode, dls_loader=dls_loader, hdfmap=self.map)[0]

load_nexus_scan(filename)

Loads a NexusScan object from a NeXus file, if it includes the NexusScan entry

Source code in mmg_toolbox/nexus/nexus_scan.py
def load_nexus_scan(filename: str) -> NexusScan:
    """Loads a NexusScan object from a NeXus file, if it includes the NexusScan entry"""
    with load_hdf(filename) as hdf:
        if 'NexusScan' not in hdf:
            print(f"File {filename} does not contain a NexusScan entry, loading NexusScan as normal")
            hdf_map = NexusMap()  # save re-opening the file
            hdf_map.populate(hdf)
            return NexusScan(filename, hdf_map)

        orig_filename, local_data, config = _load_local_data(hdf)

    # create NexusScan
    nexus_scan = NexusScan(orig_filename, config=config)
    nexus_scan.add_local(**local_data)
    return nexus_scan