• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

OpenCOMPES / sed / 5598521386

pending completion
5598521386

push

github

web-flow
Merge pull request #119 from OpenCOMPES/point-selector

Point selector

64 of 64 new or added lines in 3 files covered. (100.0%)

2901 of 3897 relevant lines covered (74.44%)

2.23 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

40.0
/sed/core/processor.py
1
"""This module contains the core class for the sed package
2

3
"""
4
import pathlib
3✔
5
from typing import Any
3✔
6
from typing import cast
3✔
7
from typing import Dict
3✔
8
from typing import List
3✔
9
from typing import Sequence
3✔
10
from typing import Tuple
3✔
11
from typing import Union
3✔
12

13
import dask.dataframe as ddf
3✔
14
import matplotlib.pyplot as plt
3✔
15
import numpy as np
3✔
16
import pandas as pd
3✔
17
import psutil
3✔
18
import xarray as xr
3✔
19

20
from sed.binning import bin_dataframe
3✔
21
from sed.calibrator import DelayCalibrator
3✔
22
from sed.calibrator import EnergyCalibrator
3✔
23
from sed.calibrator import MomentumCorrector
3✔
24
from sed.config import parse_config
3✔
25
from sed.core.dfops import apply_jitter
3✔
26
from sed.core.metadata import MetaHandler
3✔
27
from sed.diagnostics import grid_histogram
3✔
28
from sed.io import to_h5
3✔
29
from sed.io import to_nexus
3✔
30
from sed.io import to_tiff
3✔
31
from sed.loader import CopyTool
3✔
32
from sed.loader import get_loader
3✔
33

34
N_CPU = psutil.cpu_count()
3✔
35

36

37
class SedProcessor:
3✔
38
    """Processor class of sed. Contains wrapper functions defining a work flow for data
39
    correction, calibration and binning.
40

41
    Args:
42
        metadata (dict, optional): Dict of external Metadata. Defaults to None.
43
        config (Union[dict, str], optional): Config dictionary or config file name.
44
            Defaults to None.
45
        dataframe (Union[pd.DataFrame, ddf.DataFrame], optional): dataframe to load
46
            into the class. Defaults to None.
47
        files (List[str], optional): List of files to pass to the loader defined in
48
            the config. Defaults to None.
49
        folder (str, optional): Folder containing files to pass to the loader
50
            defined in the config. Defaults to None.
51
        collect_metadata (bool): Option to collect metadata from files.
52
            Defaults to False.
53
        **kwds: Keyword arguments passed to the reader.
54
    """
55

56
    def __init__(
3✔
57
        self,
58
        metadata: dict = None,
59
        config: Union[dict, str] = None,
60
        dataframe: Union[pd.DataFrame, ddf.DataFrame] = None,
61
        files: List[str] = None,
62
        folder: str = None,
63
        runs: Sequence[str] = None,
64
        collect_metadata: bool = False,
65
        **kwds,
66
    ):
67
        """Processor class of sed. Contains wrapper functions defining a work flow
68
        for data correction, calibration, and binning.
69

70
        Args:
71
            metadata (dict, optional): Dict of external Metadata. Defaults to None.
72
            config (Union[dict, str], optional): Config dictionary or config file name.
73
                Defaults to None.
74
            dataframe (Union[pd.DataFrame, ddf.DataFrame], optional): dataframe to load
75
                into the class. Defaults to None.
76
            files (List[str], optional): List of files to pass to the loader defined in
77
                the config. Defaults to None.
78
            folder (str, optional): Folder containing files to pass to the loader
79
                defined in the config. Defaults to None.
80
            runs (Sequence[str], optional): List of run identifiers to pass to the loader
81
                defined in the config. Defaults to None.
82
            collect_metadata (bool): Option to collect metadata from files.
83
                Defaults to False.
84
            **kwds: Keyword arguments passed to the reader.
85
        """
86
        self._config = parse_config(config)
3✔
87
        num_cores = self._config.get("binning", {}).get("num_cores", N_CPU - 1)
3✔
88
        if num_cores >= N_CPU:
3✔
89
            num_cores = N_CPU - 1
3✔
90
        self._config["binning"]["num_cores"] = num_cores
3✔
91

92
        self._dataframe: Union[pd.DataFrame, ddf.DataFrame] = None
3✔
93
        self._files: List[str] = []
3✔
94

95
        self._binned: xr.DataArray = None
3✔
96
        self._pre_binned: xr.DataArray = None
3✔
97

98
        self._dimensions: List[str] = []
3✔
99
        self._coordinates: Dict[Any, Any] = {}
3✔
100
        self.axis: Dict[Any, Any] = {}
3✔
101
        self._attributes = MetaHandler(meta=metadata)
3✔
102

103
        loader_name = self._config["core"]["loader"]
3✔
104
        self.loader = get_loader(
3✔
105
            loader_name=loader_name,
106
            config=self._config,
107
        )
108

109
        self.ec = EnergyCalibrator(
3✔
110
            loader=self.loader,
111
            config=self._config,
112
        )
113

114
        self.mc = MomentumCorrector(
3✔
115
            config=self._config,
116
        )
117

118
        self.dc = DelayCalibrator(
3✔
119
            config=self._config,
120
        )
121

122
        self.use_copy_tool = self._config.get("core", {}).get(
3✔
123
            "use_copy_tool",
124
            False,
125
        )
126
        if self.use_copy_tool:
3✔
127
            try:
×
128
                self.ct = CopyTool(
×
129
                    source=self._config["core"]["copy_tool_source"],
130
                    dest=self._config["core"]["copy_tool_dest"],
131
                    **self._config["core"].get("copy_tool_kwds", {}),
132
                )
133
            except KeyError:
×
134
                self.use_copy_tool = False
×
135

136
        # Load data if provided:
137
        if dataframe is not None or files is not None or folder is not None or runs is not None:
3✔
138
            self.load(
×
139
                dataframe=dataframe,
140
                metadata=metadata,
141
                files=files,
142
                folder=folder,
143
                runs=runs,
144
                collect_metadata=collect_metadata,
145
                **kwds,
146
            )
147

148
    def __repr__(self):
3✔
149
        if self._dataframe is None:
×
150
            df_str = "Data Frame: No Data loaded"
×
151
        else:
152
            df_str = self._dataframe.__repr__()
×
153
        coordinates_str = f"Coordinates: {self._coordinates}"
×
154
        dimensions_str = f"Dimensions: {self._dimensions}"
×
155
        pretty_str = df_str + "\n" + coordinates_str + "\n" + dimensions_str
×
156
        return pretty_str
×
157

158
    def __getitem__(self, val: str) -> pd.DataFrame:
3✔
159
        """Accessor to the underlying data structure.
160

161
        Args:
162
            val (str): Name of the dataframe column to retrieve.
163

164
        Returns:
165
            pd.DataFrame: Selected dataframe column.
166
        """
167
        return self._dataframe[val]
×
168

169
    @property
3✔
170
    def config(self) -> Dict[Any, Any]:
3✔
171
        """Getter attribute for the config dictionary
172

173
        Returns:
174
            Dict: The config dictionary.
175
        """
176
        return self._config
×
177

178
    @config.setter
3✔
179
    def config(self, config: Union[dict, str]):
3✔
180
        """Setter function for the config dictionary.
181

182
        Args:
183
            config (Union[dict, str]): Config dictionary or path of config file
184
                to load.
185
        """
186
        self._config = parse_config(config)
×
187
        num_cores = self._config.get("binning", {}).get("num_cores", N_CPU - 1)
×
188
        if num_cores >= N_CPU:
×
189
            num_cores = N_CPU - 1
×
190
        self._config["binning"]["num_cores"] = num_cores
×
191

192
    @property
3✔
193
    def dimensions(self) -> list:
3✔
194
        """Getter attribute for the dimensions.
195

196
        Returns:
197
            list: List of dimensions.
198
        """
199
        return self._dimensions
×
200

201
    @dimensions.setter
3✔
202
    def dimensions(self, dims: list):
3✔
203
        """Setter function for the dimensions.
204

205
        Args:
206
            dims (list): List of dimensions to set.
207
        """
208
        assert isinstance(dims, list)
×
209
        self._dimensions = dims
×
210

211
    @property
3✔
212
    def coordinates(self) -> dict:
3✔
213
        """Getter attribute for the coordinates dict.
214

215
        Returns:
216
            dict: Dictionary of coordinates.
217
        """
218
        return self._coordinates
×
219

220
    @coordinates.setter
3✔
221
    def coordinates(self, coords: dict):
3✔
222
        """Setter function for the coordinates dict
223

224
        Args:
225
            coords (dict): Dictionary of coordinates.
226
        """
227
        assert isinstance(coords, dict)
×
228
        self._coordinates = {}
×
229
        for k, v in coords.items():
×
230
            self._coordinates[k] = xr.DataArray(v)
×
231

232
    def cpy(self, path: Union[str, List[str]]) -> Union[str, List[str]]:
3✔
233
        """Function to mirror a list of files or a folder from a network drive to a
234
        local storage. Returns either the original or the copied path to the given
235
        path. The option to use this functionality is set by
236
        config["core"]["use_copy_tool"].
237

238
        Args:
239
            path (Union[str, List[str]]): Source path or path list.
240

241
        Returns:
242
            Union[str, List[str]]: Source or destination path or path list.
243
        """
244
        if self.use_copy_tool:
3✔
245
            if isinstance(path, list):
×
246
                path_out = []
×
247
                for file in path:
×
248
                    path_out.append(self.ct.copy(file))
×
249
                return path_out
×
250

251
            return self.ct.copy(path)
×
252

253
        if isinstance(path, list):
3✔
254
            return path
3✔
255

256
        return path
×
257

258
    def load(
3✔
259
        self,
260
        dataframe: Union[pd.DataFrame, ddf.DataFrame] = None,
261
        metadata: dict = None,
262
        files: List[str] = None,
263
        folder: str = None,
264
        runs: Sequence[str] = None,
265
        collect_metadata: bool = False,
266
        **kwds,
267
    ):
268
        """Load tabular data of single events into the dataframe object in the class.
269

270
        Args:
271
            dataframe (Union[pd.DataFrame, ddf.DataFrame], optional): data in tabular
272
                format. Accepts anything which can be interpreted by pd.DataFrame as
273
                an input. Defaults to None.
274
            metadata (dict, optional): Dict of external Metadata. Defaults to None.
275
            files (List[str], optional): List of file paths to pass to the loader.
276
                Defaults to None.
277
            runs (Sequence[str], optional): List of run identifiers to pass to the
278
                loader. Defaults to None.
279
            folder (str, optional): Folder path to pass to the loader.
280
                Defaults to None.
281

282
        Raises:
283
            ValueError: Raised if no valid input is provided.
284
        """
285
        if metadata is None:
3✔
286
            metadata = {}
3✔
287
        if dataframe is not None:
3✔
288
            self._dataframe = dataframe
×
289
        elif runs is not None:
3✔
290
            # If runs are provided, we only use the copy tool if also folder is provided.
291
            # In that case, we copy the whole provided base folder tree, and pass the copied
292
            # version to the loader as base folder to look for the runs.
293
            if folder is not None:
×
294
                dataframe, metadata = self.loader.read_dataframe(
×
295
                    folders=cast(str, self.cpy(folder)),
296
                    runs=runs,
297
                    metadata=metadata,
298
                    collect_metadata=collect_metadata,
299
                    **kwds,
300
                )
301
            else:
302
                dataframe, metadata = self.loader.read_dataframe(
×
303
                    runs=runs,
304
                    metadata=metadata,
305
                    collect_metadata=collect_metadata,
306
                    **kwds,
307
                )
308

309
        elif folder is not None:
3✔
310
            dataframe, metadata = self.loader.read_dataframe(
×
311
                folders=cast(str, self.cpy(folder)),
312
                metadata=metadata,
313
                collect_metadata=collect_metadata,
314
                **kwds,
315
            )
316

317
        elif files is not None:
3✔
318
            dataframe, metadata = self.loader.read_dataframe(
3✔
319
                files=cast(List[str], self.cpy(files)),
320
                metadata=metadata,
321
                collect_metadata=collect_metadata,
322
                **kwds,
323
            )
324

325
        else:
326
            raise ValueError(
×
327
                "Either 'dataframe', 'files', 'folder', or 'runs' needs to be provided!",
328
            )
329

330
        self._dataframe = dataframe
3✔
331
        self._files = self.loader.files
3✔
332

333
        for key in metadata:
3✔
334
            self._attributes.add(
×
335
                entry=metadata[key],
336
                name=key,
337
                duplicate_policy="merge",
338
            )
339

340
    # Momentum calibration workflow
341
    # 1. Bin raw detector data for distortion correction
342
    def bin_and_load_momentum_calibration(
3✔
343
        self,
344
        df_partitions: int = 100,
345
        axes: List[str] = None,
346
        bins: List[int] = None,
347
        ranges: Sequence[Tuple[float, float]] = None,
348
        plane: int = 0,
349
        width: int = 5,
350
        apply: bool = False,
351
        **kwds,
352
    ):
353
        """1st step of momentum correction work flow. Function to do an initial binning
354
        of the dataframe loaded to the class, slice a plane from it using an
355
        interactive view, and load it into the momentum corrector class.
356

357
        Args:
358
            df_partitions (int, optional): Number of dataframe partitions to use for
359
                the initial binning. Defaults to 100.
360
            axes (List[str], optional): Axes to bin.
361
                Defaults to config["momentum"]["axes"].
362
            bins (List[int], optional): Bin numbers to use for binning.
363
                Defaults to config["momentum"]["bins"].
364
            ranges (List[Tuple], optional): Ranges to use for binning.
365
                Defaults to config["momentum"]["ranges"].
366
            plane (int, optional): Initial value for the plane slider. Defaults to 0.
367
            width (int, optional): Initial value for the width slider. Defaults to 5.
368
            apply (bool, optional): Option to directly apply the values and select the
369
                slice. Defaults to False.
370
            **kwds: Keyword argument passed to the pre_binning function.
371
        """
372
        self._pre_binned = self.pre_binning(
3✔
373
            df_partitions=df_partitions,
374
            axes=axes,
375
            bins=bins,
376
            ranges=ranges,
377
            **kwds,
378
        )
379

380
        self.mc.load_data(data=self._pre_binned)
3✔
381
        self.mc.select_slicer(plane=plane, width=width, apply=apply)
3✔
382

383
    # 2. Generate the spline warp correction from momentum features.
384
    # Either autoselect features, or input features from view above.
385
    def define_features(
3✔
386
        self,
387
        features: np.ndarray = None,
388
        rotation_symmetry: int = 6,
389
        auto_detect: bool = False,
390
        include_center: bool = True,
391
        apply: bool = False,
392
        **kwds,
393
    ):
394
        """2. Step of the distortion correction workflow: Define feature points in
395
        momentum space. They can be either manually selected using a GUI tool, be
396
        ptovided as list of feature points, or auto-generated using a
397
        feature-detection algorithm.
398

399
        Args:
400
            features (np.ndarray, optional): np.ndarray of features. Defaults to None.
401
            rotation_symmetry (int, optional): Number of rotational symmetry axes.
402
                Defaults to 6.
403
            auto_detect (bool, optional): Whether to auto-detect the features.
404
                Defaults to False.
405
            include_center (bool, optional): Option to include a point at the center
406
                in the feature list. Defaults to True.
407
            ***kwds: Keyword arguments for MomentumCorrector.feature_extract() and
408
                MomentumCorrector.feature_select()
409
        """
410
        if auto_detect:  # automatic feature selection
×
411
            sigma = kwds.pop(
×
412
                "sigma",
413
                self._config.get("momentum", {}).get("sigma", 5),
414
            )
415
            fwhm = kwds.pop(
×
416
                "fwhm",
417
                self._config.get("momentum", {}).get("fwhm", 8),
418
            )
419
            sigma_radius = kwds.pop(
×
420
                "sigma_radius",
421
                self._config.get("momentum", {}).get("sigma_radius", 1),
422
            )
423
            self.mc.feature_extract(
×
424
                sigma=sigma,
425
                fwhm=fwhm,
426
                sigma_radius=sigma_radius,
427
                rotsym=rotation_symmetry,
428
                **kwds,
429
            )
430
            features = self.mc.peaks
×
431

432
        self.mc.feature_select(
×
433
            rotsym=rotation_symmetry,
434
            include_center=include_center,
435
            features=features,
436
            apply=apply,
437
            **kwds,
438
        )
439

440
    # 3. Generate the spline warp correction from momentum features.
441
    # If no features have been selected before, use class defaults.
442
    def generate_splinewarp(
3✔
443
        self,
444
        include_center: bool = True,
445
        **kwds,
446
    ):
447
        """3. Step of the distortion correction workflow: Generate the correction
448
        function restoring the symmetry in the image using a splinewarp algortihm.
449

450
        Args:
451
            include_center (bool, optional): Option to include the position of the
452
                center point in the correction. Defaults to True.
453
            **kwds: Keyword arguments for MomentumCorrector.spline_warp_estimate().
454
        """
455
        self.mc.spline_warp_estimate(include_center=include_center, **kwds)
×
456

457
        if self.mc.slice is not None:
×
458
            print("Original slice with reference features")
×
459
            self.mc.view(annotated=True, backend="bokeh", crosshair=True)
×
460

461
            print("Corrected slice with target features")
×
462
            self.mc.view(
×
463
                image=self.mc.slice_corrected,
464
                annotated=True,
465
                points={"feats": self.mc.ptargs},
466
                backend="bokeh",
467
                crosshair=True,
468
            )
469

470
            print("Original slice with target features")
×
471
            self.mc.view(
×
472
                image=self.mc.slice,
473
                points={"feats": self.mc.ptargs},
474
                annotated=True,
475
                backend="bokeh",
476
            )
477

478
    # 4. Pose corrections. Provide interactive interface for correcting
479
    # scaling, shift and rotation
480
    def pose_adjustment(
3✔
481
        self,
482
        scale: float = 1,
483
        xtrans: float = 0,
484
        ytrans: float = 0,
485
        angle: float = 0,
486
        apply: bool = False,
487
        use_correction: bool = True,
488
    ):
489
        """3. step of the distortion correction workflow: Generate an interactive panel
490
        to adjust affine transformations that are applied to the image. Applies first
491
        a scaling, next an x/y translation, and last a rotation around the center of
492
        the image.
493

494
        Args:
495
            scale (float, optional): Initial value of the scaling slider.
496
                Defaults to 1.
497
            xtrans (float, optional): Initial value of the xtrans slider.
498
                Defaults to 0.
499
            ytrans (float, optional): Initial value of the ytrans slider.
500
                Defaults to 0.
501
            angle (float, optional): Initial value of the angle slider.
502
                Defaults to 0.
503
            apply (bool, optional): Option to directly apply the provided
504
                transformations. Defaults to False.
505
            use_correction (bool, option): Whether to use the spline warp correction
506
                or not. Defaults to True.
507
        """
508
        # Generate homomorphy as default if no distortion correction has been applied
509
        if self.mc.slice_corrected is None:
×
510
            if self.mc.slice is None:
×
511
                raise ValueError(
×
512
                    "No slice for corrections and transformations loaded!",
513
                )
514
            self.mc.slice_corrected = self.mc.slice
×
515

516
        if self.mc.cdeform_field is None or self.mc.rdeform_field is None:
×
517
            # Generate default distortion correction
518
            self.mc.add_features()
×
519
            self.mc.spline_warp_estimate()
×
520

521
        if not use_correction:
×
522
            self.mc.reset_deformation()
×
523

524
        self.mc.pose_adjustment(
×
525
            scale=scale,
526
            xtrans=xtrans,
527
            ytrans=ytrans,
528
            angle=angle,
529
            apply=apply,
530
        )
531

532
    # 5. Apply the momentum correction to the dataframe
533
    def apply_momentum_correction(
3✔
534
        self,
535
        preview: bool = False,
536
    ):
537
        """Applies the distortion correction and pose adjustment (optional)
538
        to the dataframe.
539

540
        Args:
541
            rdeform_field (np.ndarray, optional): Row deformation field.
542
                Defaults to None.
543
            cdeform_field (np.ndarray, optional): Column deformation field.
544
                Defaults to None.
545
            inv_dfield (np.ndarray, optional): Inverse deformation field.
546
                Defaults to None.
547
            preview (bool): Option to preview the first elements of the data frame.
548
        """
549
        if self._dataframe is not None:
×
550
            print("Adding corrected X/Y columns to dataframe:")
×
551
            self._dataframe, metadata = self.mc.apply_corrections(
×
552
                df=self._dataframe,
553
            )
554
            # Add Metadata
555
            self._attributes.add(
×
556
                metadata,
557
                "momentum_correction",
558
                duplicate_policy="merge",
559
            )
560
            if preview:
×
561
                print(self._dataframe.head(10))
×
562
            else:
563
                print(self._dataframe)
×
564

565
    # Momentum calibration work flow
566
    # 1. Calculate momentum calibration
567
    def calibrate_momentum_axes(
3✔
568
        self,
569
        point_a: Union[np.ndarray, List[int]] = None,
570
        point_b: Union[np.ndarray, List[int]] = None,
571
        k_distance: float = None,
572
        k_coord_a: Union[np.ndarray, List[float]] = None,
573
        k_coord_b: Union[np.ndarray, List[float]] = np.array([0.0, 0.0]),
574
        equiscale: bool = True,
575
        apply=False,
576
    ):
577
        """1. step of the momentum calibration workflow. Calibrate momentum
578
        axes using either provided pixel coordinates of a high-symmetry point and its
579
        distance to the BZ center, or the k-coordinates of two points in the BZ
580
        (depending on the equiscale option). Opens an interactive panel for selecting
581
        the points.
582

583
        Args:
584
            point_a (Union[np.ndarray, List[int]]): Pixel coordinates of the first
585
                point used for momentum calibration.
586
            point_b (Union[np.ndarray, List[int]], optional): Pixel coordinates of the
587
                second point used for momentum calibration.
588
                Defaults to config["momentum"]["center_pixel"].
589
            k_distance (float, optional): Momentum distance between point a and b.
590
                Needs to be provided if no specific k-koordinates for the two points
591
                are given. Defaults to None.
592
            k_coord_a (Union[np.ndarray, List[float]], optional): Momentum coordinate
593
                of the first point used for calibration. Used if equiscale is False.
594
                Defaults to None.
595
            k_coord_b (Union[np.ndarray, List[float]], optional): Momentum coordinate
596
                of the second point used for calibration. Defaults to [0.0, 0.0].
597
            equiscale (bool, optional): Option to apply different scales to kx and ky.
598
                If True, the distance between points a and b, and the absolute
599
                position of point a are used for defining the scale. If False, the
600
                scale is calculated from the k-positions of both points a and b.
601
                Defaults to True.
602
            apply (bool, optional): Option to directly store the momentum calibration
603
                in the class. Defaults to False.
604
        """
605
        if point_b is None:
×
606
            point_b = self._config.get("momentum", {}).get(
×
607
                "center_pixel",
608
                [256, 256],
609
            )
610

611
        self.mc.select_k_range(
×
612
            point_a=point_a,
613
            point_b=point_b,
614
            k_distance=k_distance,
615
            k_coord_a=k_coord_a,
616
            k_coord_b=k_coord_b,
617
            equiscale=equiscale,
618
            apply=apply,
619
        )
620

621
    # 2. Apply correction and calibration to the dataframe
622
    def apply_momentum_calibration(
3✔
623
        self,
624
        calibration: dict = None,
625
        preview: bool = False,
626
    ):
627
        """2. step of the momentum calibration work flow: Apply the momentum
628
        calibration stored in the class to the dataframe. If corrected X/Y axis exist,
629
        these are used.
630

631
        Args:
632
            calibration (dict, optional): Optional dictionary with calibration data to
633
                use. Defaults to None.
634
            preview (bool): Option to preview the first elements of the data frame.
635
        """
636
        if self._dataframe is not None:
×
637

638
            print("Adding kx/ky columns to dataframe:")
×
639
            self._dataframe, metadata = self.mc.append_k_axis(
×
640
                df=self._dataframe,
641
                calibration=calibration,
642
            )
643

644
            # Add Metadata
645
            self._attributes.add(
×
646
                metadata,
647
                "momentum_calibration",
648
                duplicate_policy="merge",
649
            )
650
            if preview:
×
651
                print(self._dataframe.head(10))
×
652
            else:
653
                print(self._dataframe)
×
654

655
    # Energy correction workflow
656
    # 1. Adjust the energy correction parameters
657
    def adjust_energy_correction(
3✔
658
        self,
659
        correction_type: str = None,
660
        amplitude: float = None,
661
        center: Tuple[float, float] = None,
662
        apply=False,
663
        **kwds,
664
    ):
665
        """1. step of the energy crrection workflow: Opens an interactive plot to
666
        adjust the parameters for the TOF/energy correction. Also pre-bins the data if
667
        they are not present yet.
668

669
        Args:
670
            correction_type (str, optional): Type of correction to apply to the TOF
671
                axis. Valid values are:
672

673
                - 'spherical'
674
                - 'Lorentzian'
675
                - 'Gaussian'
676
                - 'Lorentzian_asymmetric'
677

678
                Defaults to config["energy"]["correction_type"].
679
            amplitude (float, optional): Amplitude of the correction.
680
                Defaults to config["energy"]["correction"]["amplitude"].
681
            center (Tuple[float, float], optional): Center X/Y coordinates for the
682
                correction. Defaults to config["energy"]["correction"]["center"].
683
            apply (bool, optional): Option to directly apply the provided or default
684
                correction parameters. Defaults to False.
685
        """
686
        if self._pre_binned is None:
×
687
            print(
×
688
                "Pre-binned data not present, binning using defaults from config...",
689
            )
690
            self._pre_binned = self.pre_binning()
×
691

692
        self.ec.adjust_energy_correction(
×
693
            self._pre_binned,
694
            correction_type=correction_type,
695
            amplitude=amplitude,
696
            center=center,
697
            apply=apply,
698
            **kwds,
699
        )
700

701
    # 2. Apply energy correction to dataframe
702
    def apply_energy_correction(
3✔
703
        self,
704
        correction: dict = None,
705
        preview: bool = False,
706
        **kwds,
707
    ):
708
        """2. step of the energy correction workflow: Apply the enery correction
709
        parameters stored in the class to the dataframe.
710

711
        Args:
712
            correction (dict, optional): Dictionary containing the correction
713
                parameters. Defaults to config["energy"]["calibration"].
714
            preview (bool): Option to preview the first elements of the data frame.
715
            **kwds:
716
                Keyword args passed to ``EnergyCalibrator.apply_energy_correction``.
717
            preview (bool): Option to preview the first elements of the data frame.
718
            **kwds:
719
                Keyword args passed to ``EnergyCalibrator.apply_energy_correction``.
720
        """
721
        if self._dataframe is not None:
×
722
            print("Applying energy correction to dataframe...")
×
723
            self._dataframe, metadata = self.ec.apply_energy_correction(
×
724
                df=self._dataframe,
725
                correction=correction,
726
                **kwds,
727
            )
728

729
            # Add Metadata
730
            self._attributes.add(
×
731
                metadata,
732
                "energy_correction",
733
            )
734
            if preview:
×
735
                print(self._dataframe.head(10))
×
736
            else:
737
                print(self._dataframe)
×
738

739
    # Energy calibrator workflow
740
    # 1. Load and normalize data
741
    def load_bias_series(
3✔
742
        self,
743
        data_files: List[str],
744
        axes: List[str] = None,
745
        bins: List = None,
746
        ranges: Sequence[Tuple[float, float]] = None,
747
        biases: np.ndarray = None,
748
        bias_key: str = None,
749
        normalize: bool = None,
750
        span: int = None,
751
        order: int = None,
752
    ):
753
        """1. step of the energy calibration workflow: Load and bin data from
754
        single-event files.
755

756
        Args:
757
            data_files (List[str]): list of file paths to bin
758
            axes (List[str], optional): bin axes.
759
                Defaults to config["dataframe"]["tof_column"].
760
            bins (List, optional): number of bins.
761
                Defaults to config["energy"]["bins"].
762
            ranges (Sequence[Tuple[float, float]], optional): bin ranges.
763
                Defaults to config["energy"]["ranges"].
764
            biases (np.ndarray, optional): Bias voltages used. If missing, bias
765
                voltages are extracted from the data files.
766
            bias_key (str, optional): hdf5 path where bias values are stored.
767
                Defaults to config["energy"]["bias_key"].
768
            normalize (bool, optional): Option to normalize traces.
769
                Defaults to config["energy"]["normalize"].
770
            span (int, optional): span smoothing parameters of the LOESS method
771
                (see ``scipy.signal.savgol_filter()``).
772
                Defaults to config["energy"]["normalize_span"].
773
            order (int, optional): order smoothing parameters of the LOESS method
774
                (see ``scipy.signal.savgol_filter()``).
775
                Defaults to config["energy"]["normalize_order"].
776
        """
777
        self.ec.bin_data(
×
778
            data_files=cast(List[str], self.cpy(data_files)),
779
            axes=axes,
780
            bins=bins,
781
            ranges=ranges,
782
            biases=biases,
783
            bias_key=bias_key,
784
        )
785
        if (normalize is not None and normalize is True) or (
×
786
            normalize is None and self._config.get("energy", {}).get("normalize", True)
787
        ):
788
            if span is None:
×
789
                span = self._config.get("energy", {}).get("normalize_span", 7)
×
790
            if order is None:
×
791
                order = self._config.get("energy", {}).get(
×
792
                    "normalize_order",
793
                    1,
794
                )
795
            self.ec.normalize(smooth=True, span=span, order=order)
×
796
        self.ec.view(
×
797
            traces=self.ec.traces_normed,
798
            xaxis=self.ec.tof,
799
            backend="bokeh",
800
        )
801

802
    # 2. extract ranges and get peak positions
803
    def find_bias_peaks(
3✔
804
        self,
805
        ranges: Union[List[Tuple], Tuple],
806
        ref_id: int = 0,
807
        infer_others: bool = True,
808
        mode: str = "replace",
809
        radius: int = None,
810
        peak_window: int = None,
811
    ):
812
        """2. step of the energy calibration workflow: Find a peak within a given range
813
        for the indicated reference trace, and tries to find the same peak for all
814
        other traces. Uses fast_dtw to align curves, which might not be too good if the
815
        shape of curves changes qualitatively. Ideally, choose a reference trace in the
816
        middle of the set, and don't choose the range too narrow around the peak.
817
        Alternatively, a list of ranges for all traces can be provided.
818

819
        Args:
820
            ranges (Union[List[Tuple], Tuple]): Tuple of TOF values indicating a range.
821
                Alternatively, a list of ranges for all traces can be given.
822
            refid (int, optional): The id of the trace the range refers to.
823
                Defaults to 0.
824
            infer_others (bool, optional): Whether to determine the range for the other
825
                traces. Defaults to True.
826
            mode (str, optional): Whether to "add" or "replace" existing ranges.
827
                Defaults to "replace".
828
            radius (int, optional): Radius parameter for fast_dtw.
829
                Defaults to config["energy"]["fastdtw_radius"].
830
            peak_window (int, optional): Peak_window parameter for the peak detection
831
                algorthm. amount of points that have to have to behave monotoneously
832
                around a peak. Defaults to config["energy"]["peak_window"].
833
        """
834
        if radius is None:
×
835
            radius = self._config.get("energy", {}).get("fastdtw_radius", 2)
×
836
        self.ec.add_features(
×
837
            ranges=ranges,
838
            ref_id=ref_id,
839
            infer_others=infer_others,
840
            mode=mode,
841
            radius=radius,
842
        )
843
        self.ec.view(
×
844
            traces=self.ec.traces_normed,
845
            segs=self.ec.featranges,
846
            xaxis=self.ec.tof,
847
            backend="bokeh",
848
        )
849
        print(self.ec.featranges)
×
850
        if peak_window is None:
×
851
            peak_window = self._config.get("energy", {}).get("peak_window", 7)
×
852
        try:
×
853
            self.ec.feature_extract(peak_window=peak_window)
×
854
            self.ec.view(
×
855
                traces=self.ec.traces_normed,
856
                peaks=self.ec.peaks,
857
                backend="bokeh",
858
            )
859
        except IndexError:
×
860
            print("Could not determine all peaks!")
×
861
            raise
×
862

863
    # 3. Fit the energy calibration relation
864
    def calibrate_energy_axis(
3✔
865
        self,
866
        ref_id: int,
867
        ref_energy: float,
868
        method: str = None,
869
        energy_scale: str = None,
870
        **kwds,
871
    ):
872
        """3. Step of the energy calibration workflow: Calculate the calibration
873
        function for the energy axis, and apply it to the dataframe. Two
874
        approximations are implemented, a (normally 3rd order) polynomial
875
        approximation, and a d^2/(t-t0)^2 relation.
876

877
        Args:
878
            ref_id (int): id of the trace at the bias where the reference energy is
879
                given.
880
            ref_energy (float): Absolute energy of the detected feature at the bias
881
                of ref_id
882
            method (str, optional): Method for determining the energy calibration.
883

884
                - **'lmfit'**: Energy calibration using lmfit and 1/t^2 form.
885
                - **'lstsq'**, **'lsqr'**: Energy calibration using polynomial form.
886

887
                Defaults to config["energy"]["calibration_method"]
888
            energy_scale (str, optional): Direction of increasing energy scale.
889

890
                - **'kinetic'**: increasing energy with decreasing TOF.
891
                - **'binding'**: increasing energy with increasing TOF.
892

893
                Defaults to config["energy"]["energy_scale"]
894
        """
895
        if method is None:
×
896
            method = self._config.get("energy", {}).get(
×
897
                "calibration_method",
898
                "lmfit",
899
            )
900

901
        if energy_scale is None:
×
902
            energy_scale = self._config.get("energy", {}).get(
×
903
                "energy_scale",
904
                "kinetic",
905
            )
906

907
        self.ec.calibrate(
×
908
            ref_id=ref_id,
909
            ref_energy=ref_energy,
910
            method=method,
911
            energy_scale=energy_scale,
912
            **kwds,
913
        )
914
        print("Quality of Calibration:")
×
915
        self.ec.view(
×
916
            traces=self.ec.traces_normed,
917
            xaxis=self.ec.calibration["axis"],
918
            align=True,
919
            energy_scale=energy_scale,
920
            backend="bokeh",
921
        )
922
        print("E/TOF relationship:")
×
923
        self.ec.view(
×
924
            traces=self.ec.calibration["axis"][None, :],
925
            xaxis=self.ec.tof,
926
            backend="matplotlib",
927
            show_legend=False,
928
        )
929
        if energy_scale == "kinetic":
×
930
            plt.scatter(
×
931
                self.ec.peaks[:, 0],
932
                -(self.ec.biases - self.ec.biases[ref_id]) + ref_energy,
933
                s=50,
934
                c="k",
935
            )
936
        elif energy_scale == "binding":
×
937
            plt.scatter(
×
938
                self.ec.peaks[:, 0],
939
                self.ec.biases - self.ec.biases[ref_id] + ref_energy,
940
                s=50,
941
                c="k",
942
            )
943
        else:
944
            raise ValueError(
×
945
                'energy_scale needs to be either "binding" or "kinetic"',
946
                f", got {energy_scale}.",
947
            )
948
        plt.xlabel("Time-of-flight", fontsize=15)
×
949
        plt.ylabel("Energy (eV)", fontsize=15)
×
950
        plt.show()
×
951

952
    # 4. Apply energy calibration to the dataframe
953
    def append_energy_axis(
3✔
954
        self,
955
        calibration: dict = None,
956
        preview: bool = False,
957
        **kwds,
958
    ):
959
        """4. step of the energy calibration workflow: Apply the calibration function
960
        to to the dataframe. Two approximations are implemented, a (normally 3rd order)
961
        polynomial approximation, and a d^2/(t-t0)^2 relation. a calibration dictionary
962
        can be provided.
963

964
        Args:
965
            calibration (dict, optional): Calibration dict containing calibration
966
                parameters. Overrides calibration from class or config.
967
                Defaults to None.
968
            preview (bool): Option to preview the first elements of the data frame.
969
            **kwds:
970
                Keyword args passed to ``EnergyCalibrator.append_energy_axis``.
971
        """
972
        if self._dataframe is not None:
×
973
            print("Adding energy column to dataframe:")
×
974
            self._dataframe, metadata = self.ec.append_energy_axis(
×
975
                df=self._dataframe,
976
                calibration=calibration,
977
                **kwds,
978
            )
979

980
            # Add Metadata
981
            self._attributes.add(
×
982
                metadata,
983
                "energy_calibration",
984
                duplicate_policy="merge",
985
            )
986
            if preview:
×
987
                print(self._dataframe.head(10))
×
988
            else:
989
                print(self._dataframe)
×
990

991
    # Delay calibration function
992
    def calibrate_delay_axis(
3✔
993
        self,
994
        delay_range: Tuple[float, float] = None,
995
        datafile: str = None,
996
        preview: bool = False,
997
        **kwds,
998
    ):
999
        """Append delay column to dataframe. Either provide delay ranges, or read
1000
        them from a file.
1001

1002
        Args:
1003
            delay_range (Tuple[float, float], optional): The scanned delay range in
1004
                picoseconds. Defaults to None.
1005
            datafile (str, optional): The file from which to read the delay ranges.
1006
                Defaults to None.
1007
            preview (bool): Option to preview the first elements of the data frame.
1008
            **kwds: Keyword args passed to ``DelayCalibrator.append_delay_axis``.
1009
        """
1010
        if self._dataframe is not None:
×
1011
            print("Adding delay column to dataframe:")
×
1012

1013
            if delay_range is not None:
×
1014
                self._dataframe, metadata = self.dc.append_delay_axis(
×
1015
                    self._dataframe,
1016
                    delay_range=delay_range,
1017
                    **kwds,
1018
                )
1019
            else:
1020
                if datafile is None:
×
1021
                    try:
×
1022
                        datafile = self._files[0]
×
1023
                    except IndexError:
×
1024
                        print(
×
1025
                            "No datafile available, specify eihter",
1026
                            " 'datafile' or 'delay_range'",
1027
                        )
1028
                        raise
×
1029

1030
                self._dataframe, metadata = self.dc.append_delay_axis(
×
1031
                    self._dataframe,
1032
                    datafile=datafile,
1033
                    **kwds,
1034
                )
1035

1036
            # Add Metadata
1037
            self._attributes.add(
×
1038
                metadata,
1039
                "delay_calibration",
1040
                duplicate_policy="merge",
1041
            )
1042
            if preview:
×
1043
                print(self._dataframe.head(10))
×
1044
            else:
1045
                print(self._dataframe)
×
1046

1047
    def add_jitter(self, cols: Sequence[str] = None):
3✔
1048
        """Add jitter to the selected dataframe columns.
1049

1050
        Args:
1051
            cols (Sequence[str], optional): The colums onto which to apply jitter.
1052
                Defaults to config["dataframe"]["jitter_cols"].
1053
        """
1054
        if cols is None:
×
1055
            cols = self._config.get("dataframe", {}).get(
×
1056
                "jitter_cols",
1057
                self._dataframe.columns,
1058
            )  # jitter all columns
1059

1060
        self._dataframe = self._dataframe.map_partitions(
×
1061
            apply_jitter,
1062
            cols=cols,
1063
            cols_jittered=cols,
1064
        )
1065
        metadata = []
×
1066
        for col in cols:
×
1067
            metadata.append(col)
×
1068
        self._attributes.add(metadata, "jittering", duplicate_policy="append")
×
1069

1070
    def pre_binning(
3✔
1071
        self,
1072
        df_partitions: int = 100,
1073
        axes: List[str] = None,
1074
        bins: List[int] = None,
1075
        ranges: Sequence[Tuple[float, float]] = None,
1076
        **kwds,
1077
    ) -> xr.DataArray:
1078
        """Function to do an initial binning of the dataframe loaded to the class.
1079

1080
        Args:
1081
            df_partitions (int, optional): Number of dataframe partitions to use for
1082
                the initial binning. Defaults to 100.
1083
            axes (List[str], optional): Axes to bin.
1084
                Defaults to config["momentum"]["axes"].
1085
            bins (List[int], optional): Bin numbers to use for binning.
1086
                Defaults to config["momentum"]["bins"].
1087
            ranges (List[Tuple], optional): Ranges to use for binning.
1088
                Defaults to config["momentum"]["ranges"].
1089
            **kwds: Keyword argument passed to ``compute``.
1090

1091
        Returns:
1092
            xr.DataArray: pre-binned data-array.
1093
        """
1094
        if axes is None:
3✔
1095
            axes = self._config.get("momentum", {}).get(
3✔
1096
                "axes",
1097
                ["@x_column, @y_column, @tof_column"],
1098
            )
1099
        for loc, axis in enumerate(axes):
3✔
1100
            if axis.startswith("@"):
3✔
1101
                axes[loc] = self._config.get("dataframe").get(axis.strip("@"))
3✔
1102

1103
        if bins is None:
3✔
1104
            bins = self._config.get("momentum", {}).get(
3✔
1105
                "bins",
1106
                [512, 512, 300],
1107
            )
1108
        if ranges is None:
3✔
1109
            ranges_ = self._config.get("momentum", {}).get(
3✔
1110
                "ranges",
1111
                [[-256, 1792], [-256, 1792], [128000, 138000]],
1112
            )
1113
            ranges = [cast(Tuple[float, float], tuple(v)) for v in ranges_]
3✔
1114

1115
        assert self._dataframe is not None, "dataframe needs to be loaded first!"
3✔
1116

1117
        return self.compute(
3✔
1118
            bins=bins,
1119
            axes=axes,
1120
            ranges=ranges,
1121
            df_partitions=df_partitions,
1122
            **kwds,
1123
        )
1124

1125
    def compute(
3✔
1126
        self,
1127
        bins: Union[
1128
            int,
1129
            dict,
1130
            tuple,
1131
            List[int],
1132
            List[np.ndarray],
1133
            List[tuple],
1134
        ] = 100,
1135
        axes: Union[str, Sequence[str]] = None,
1136
        ranges: Sequence[Tuple[float, float]] = None,
1137
        **kwds,
1138
    ) -> xr.DataArray:
1139
        """Compute the histogram along the given dimensions.
1140

1141
        Args:
1142
            bins (int, dict, tuple, List[int], List[np.ndarray], List[tuple], optional):
1143
                Definition of the bins. Can be any of the following cases:
1144

1145
                - an integer describing the number of bins in on all dimensions
1146
                - a tuple of 3 numbers describing start, end and step of the binning
1147
                  range
1148
                - a np.arrays defining the binning edges
1149
                - a list (NOT a tuple) of any of the above (int, tuple or np.ndarray)
1150
                - a dictionary made of the axes as keys and any of the above as values.
1151

1152
                This takes priority over the axes and range arguments. Defaults to 100.
1153
            axes (Union[str, Sequence[str]], optional): The names of the axes (columns)
1154
                on which to calculate the histogram. The order will be the order of the
1155
                dimensions in the resulting array. Defaults to None.
1156
            ranges (Sequence[Tuple[float, float]], optional): list of tuples containing
1157
                the start and end point of the binning range. Defaults to None.
1158
            **kwds: Keyword arguments:
1159

1160
                - **hist_mode**: Histogram calculation method. "numpy" or "numba". See
1161
                  ``bin_dataframe`` for details. Defaults to
1162
                  config["binning"]["hist_mode"].
1163
                - **mode**: Defines how the results from each partition are combined.
1164
                  "fast", "lean" or "legacy". See ``bin_dataframe`` for details.
1165
                  Defaults to config["binning"]["mode"].
1166
                - **pbar**: Option to show the tqdm progress bar. Defaults to
1167
                  config["binning"]["pbar"].
1168
                - **n_cores**: Number of CPU cores to use for parallelization.
1169
                  Defaults to config["binning"]["num_cores"] or N_CPU-1.
1170
                - **threads_per_worker**: Limit the number of threads that
1171
                  multiprocessing can spawn per binning thread. Defaults to
1172
                  config["binning"]["threads_per_worker"].
1173
                - **threadpool_api**: The API to use for multiprocessing. "blas",
1174
                  "openmp" or None. See ``threadpool_limit`` for details. Defaults to
1175
                  config["binning"]["threadpool_API"].
1176
                - **df_partitions**: A list of dataframe partitions. Defaults to all
1177
                  partitions.
1178

1179
                Additional kwds are passed to ``bin_dataframe``.
1180

1181
        Raises:
1182
            AssertError: Rises when no dataframe has been loaded.
1183

1184
        Returns:
1185
            xr.DataArray: The result of the n-dimensional binning represented in an
1186
            xarray object, combining the data with the axes.
1187
        """
1188
        assert self._dataframe is not None, "dataframe needs to be loaded first!"
3✔
1189

1190
        hist_mode = kwds.pop("hist_mode", self._config["binning"]["hist_mode"])
3✔
1191
        mode = kwds.pop("mode", self._config["binning"]["mode"])
3✔
1192
        pbar = kwds.pop("pbar", self._config["binning"]["pbar"])
3✔
1193
        num_cores = kwds.pop("num_cores", self._config["binning"]["num_cores"])
3✔
1194
        threads_per_worker = kwds.pop(
3✔
1195
            "threads_per_worker",
1196
            self._config["binning"]["threads_per_worker"],
1197
        )
1198
        threadpool_api = kwds.pop(
3✔
1199
            "threadpool_API",
1200
            self._config["binning"]["threadpool_API"],
1201
        )
1202
        df_partitions = kwds.pop("df_partitions", None)
3✔
1203
        if df_partitions is not None:
3✔
1204
            dataframe = self._dataframe.partitions[
3✔
1205
                0 : min(df_partitions, self._dataframe.npartitions)
1206
            ]
1207
        else:
1208
            dataframe = self._dataframe
×
1209

1210
        self._binned = bin_dataframe(
3✔
1211
            df=dataframe,
1212
            bins=bins,
1213
            axes=axes,
1214
            ranges=ranges,
1215
            hist_mode=hist_mode,
1216
            mode=mode,
1217
            pbar=pbar,
1218
            n_cores=num_cores,
1219
            threads_per_worker=threads_per_worker,
1220
            threadpool_api=threadpool_api,
1221
            **kwds,
1222
        )
1223

1224
        for dim in self._binned.dims:
3✔
1225
            try:
3✔
1226
                self._binned[dim].attrs["unit"] = self._config["dataframe"]["units"][dim]
3✔
1227
            except KeyError:
×
1228
                pass
×
1229

1230
        self._binned.attrs["units"] = "counts"
3✔
1231
        self._binned.attrs["long_name"] = "photoelectron counts"
3✔
1232
        self._binned.attrs["metadata"] = self._attributes.metadata
3✔
1233

1234
        return self._binned
3✔
1235

1236
    def view_event_histogram(
3✔
1237
        self,
1238
        dfpid: int,
1239
        ncol: int = 2,
1240
        bins: Sequence[int] = None,
1241
        axes: Sequence[str] = None,
1242
        ranges: Sequence[Tuple[float, float]] = None,
1243
        backend: str = "bokeh",
1244
        legend: bool = True,
1245
        histkwds: dict = None,
1246
        legkwds: dict = None,
1247
        **kwds,
1248
    ):
1249
        """Plot individual histograms of specified dimensions (axes) from a substituent
1250
        dataframe partition.
1251

1252
        Args:
1253
            dfpid (int): Number of the data frame partition to look at.
1254
            ncol (int, optional): Number of columns in the plot grid. Defaults to 2.
1255
            bins (Sequence[int], optional): Number of bins to use for the speicified
1256
                axes. Defaults to config["histogram"]["bins"].
1257
            axes (Sequence[str], optional): Names of the axes to display.
1258
                Defaults to config["histogram"]["axes"].
1259
            ranges (Sequence[Tuple[float, float]], optional): Value ranges of all
1260
                specified axes. Defaults toconfig["histogram"]["ranges"].
1261
            backend (str, optional): Backend of the plotting library
1262
                ('matplotlib' or 'bokeh'). Defaults to "bokeh".
1263
            legend (bool, optional): Option to include a legend in the histogram plots.
1264
                Defaults to True.
1265
            histkwds (dict, optional): Keyword arguments for histograms
1266
                (see ``matplotlib.pyplot.hist()``). Defaults to {}.
1267
            legkwds (dict, optional): Keyword arguments for legend
1268
                (see ``matplotlib.pyplot.legend()``). Defaults to {}.
1269
            **kwds: Extra keyword arguments passed to
1270
                ``sed.diagnostics.grid_histogram()``.
1271

1272
        Raises:
1273
            TypeError: Raises when the input values are not of the correct type.
1274
        """
1275
        if bins is None:
×
1276
            bins = self._config["histogram"]["bins"]
×
1277
        if axes is None:
×
1278
            axes = self._config["histogram"]["axes"]
×
1279
        if ranges is None:
×
1280
            ranges = self._config["histogram"]["ranges"]
×
1281

1282
        input_types = map(type, [axes, bins, ranges])
×
1283
        allowed_types = [list, tuple]
×
1284

1285
        df = self._dataframe
×
1286

1287
        if not set(input_types).issubset(allowed_types):
×
1288
            raise TypeError(
×
1289
                "Inputs of axes, bins, ranges need to be list or tuple!",
1290
            )
1291

1292
        # Read out the values for the specified groups
1293
        group_dict_dd = {}
×
1294
        dfpart = df.get_partition(dfpid)
×
1295
        cols = dfpart.columns
×
1296
        for ax in axes:
×
1297
            group_dict_dd[ax] = dfpart.values[:, cols.get_loc(ax)]
×
1298
        group_dict = ddf.compute(group_dict_dd)[0]
×
1299

1300
        # Plot multiple histograms in a grid
1301
        grid_histogram(
×
1302
            group_dict,
1303
            ncol=ncol,
1304
            rvs=axes,
1305
            rvbins=bins,
1306
            rvranges=ranges,
1307
            backend=backend,
1308
            legend=legend,
1309
            histkwds=histkwds,
1310
            legkwds=legkwds,
1311
            **kwds,
1312
        )
1313

1314
    def save(
3✔
1315
        self,
1316
        faddr: str,
1317
        **kwds,
1318
    ):
1319
        """Saves the binned data to the provided path and filename.
1320

1321
        Args:
1322
            faddr (str): Path and name of the file to write. Its extension determines
1323
                the file type to write. Valid file types are:
1324

1325
                - "*.tiff", "*.tif": Saves a TIFF stack.
1326
                - "*.h5", "*.hdf5": Saves an HDF5 file.
1327
                - "*.nxs", "*.nexus": Saves a NeXus file.
1328

1329
            **kwds: Keyword argumens, which are passed to the writer functions:
1330
                For TIFF writing:
1331

1332
                - **alias_dict**: Dictionary of dimension aliases to use.
1333

1334
                For HDF5 writing:
1335

1336
                - **mode**: hdf5 read/write mode. Defaults to "w".
1337

1338
                For NeXus:
1339

1340
                - **reader**: Name of the nexustools reader to use.
1341
                  Defaults to config["nexus"]["reader"]
1342
                - **definiton**: NeXus application definition to use for saving.
1343
                  Must be supported by the used ``reader``. Defaults to
1344
                  config["nexus"]["definition"]
1345
                - **input_files**: A list of input files to pass to the reader.
1346
                  Defaults to config["nexus"]["input_files"]
1347
        """
1348
        if self._binned is None:
×
1349
            raise NameError("Need to bin data first!")
×
1350

1351
        extension = pathlib.Path(faddr).suffix
×
1352

1353
        if extension in (".tif", ".tiff"):
×
1354
            to_tiff(
×
1355
                data=self._binned,
1356
                faddr=faddr,
1357
                **kwds,
1358
            )
1359
        elif extension in (".h5", ".hdf5"):
×
1360
            to_h5(
×
1361
                data=self._binned,
1362
                faddr=faddr,
1363
                **kwds,
1364
            )
1365
        elif extension in (".nxs", ".nexus"):
×
1366
            reader = kwds.pop("reader", self._config["nexus"]["reader"])
×
1367
            definition = kwds.pop(
×
1368
                "definition",
1369
                self._config["nexus"]["definition"],
1370
            )
1371
            input_files = kwds.pop(
×
1372
                "input_files",
1373
                self._config["nexus"]["input_files"],
1374
            )
1375
            if isinstance(input_files, str):
×
1376
                input_files = [input_files]
×
1377

1378
            to_nexus(
×
1379
                data=self._binned,
1380
                faddr=faddr,
1381
                reader=reader,
1382
                definition=definition,
1383
                input_files=input_files,
1384
                **kwds,
1385
            )
1386

1387
        else:
1388
            raise NotImplementedError(
×
1389
                f"Unrecognized file format: {extension}.",
1390
            )
1391

1392
    def add_dimension(self, name: str, axis_range: Tuple):
3✔
1393
        """Add a dimension axis.
1394

1395
        Args:
1396
            name (str): name of the axis
1397
            axis_range (Tuple): range for the axis.
1398

1399
        Raises:
1400
            ValueError: Raised if an axis with that name already exists.
1401
        """
1402
        if name in self._coordinates:
×
1403
            raise ValueError(f"Axis {name} already exists")
×
1404

1405
        self.axis[name] = self.make_axis(axis_range)
×
1406

1407
    def make_axis(self, axis_range: Tuple) -> np.ndarray:
3✔
1408
        """Function to make an axis.
1409

1410
        Args:
1411
            axis_range (Tuple): range for the new axis.
1412
        """
1413

1414
        # TODO: What shall this function do?
1415
        return np.arange(*axis_range)
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc