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

OpenCOMPES / sed / 5802084663

pending completion
5802084663

Pull #136

github

web-flow
Merge db925a133 into 0e95633d0
Pull Request #136: Mpes tweaks

50 of 50 new or added lines in 4 files covered. (100.0%)

2946 of 3944 relevant lines covered (74.7%)

2.24 hits per line

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

39.76
/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
        reset: bool = True,
489
    ):
490
        """3. step of the distortion correction workflow: Generate an interactive panel
491
        to adjust affine transformations that are applied to the image. Applies first
492
        a scaling, next an x/y translation, and last a rotation around the center of
493
        the image.
494

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

519
        if self.mc.cdeform_field is None or self.mc.rdeform_field is None:
×
520
            # Generate distortion correction from config values
521
            self.mc.add_features()
×
522
            self.mc.spline_warp_estimate()
×
523

524
        if not use_correction:
×
525
            self.mc.reset_deformation()
×
526

527
        self.mc.pose_adjustment(
×
528
            scale=scale,
529
            xtrans=xtrans,
530
            ytrans=ytrans,
531
            angle=angle,
532
            apply=apply,
533
            reset=reset,
534
        )
535

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

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

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

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

615
        self.mc.select_k_range(
×
616
            point_a=point_a,
617
            point_b=point_b,
618
            k_distance=k_distance,
619
            k_coord_a=k_coord_a,
620
            k_coord_b=k_coord_b,
621
            equiscale=equiscale,
622
            apply=apply,
623
        )
624

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

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

642
            print("Adding kx/ky columns to dataframe:")
×
643
            self._dataframe, metadata = self.mc.append_k_axis(
×
644
                df=self._dataframe,
645
                calibration=calibration,
646
            )
647

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

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

673
        Args:
674
            correction_type (str, optional): Type of correction to apply to the TOF
675
                axis. Valid values are:
676

677
                - 'spherical'
678
                - 'Lorentzian'
679
                - 'Gaussian'
680
                - 'Lorentzian_asymmetric'
681

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

696
        self.ec.adjust_energy_correction(
×
697
            self._pre_binned,
698
            correction_type=correction_type,
699
            amplitude=amplitude,
700
            center=center,
701
            apply=apply,
702
            **kwds,
703
        )
704

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

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

733
            # Add Metadata
734
            self._attributes.add(
×
735
                metadata,
736
                "energy_correction",
737
            )
738
            if preview:
×
739
                print(self._dataframe.head(10))
×
740
            else:
741
                print(self._dataframe)
×
742

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

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

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

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

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

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

888
                - **'lmfit'**: Energy calibration using lmfit and 1/t^2 form.
889
                - **'lstsq'**, **'lsqr'**: Energy calibration using polynomial form.
890

891
                Defaults to config["energy"]["calibration_method"]
892
            energy_scale (str, optional): Direction of increasing energy scale.
893

894
                - **'kinetic'**: increasing energy with decreasing TOF.
895
                - **'binding'**: increasing energy with increasing TOF.
896

897
                Defaults to config["energy"]["energy_scale"]
898
        """
899
        if method is None:
×
900
            method = self._config.get("energy", {}).get(
×
901
                "calibration_method",
902
                "lmfit",
903
            )
904

905
        if energy_scale is None:
×
906
            energy_scale = self._config.get("energy", {}).get(
×
907
                "energy_scale",
908
                "kinetic",
909
            )
910

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

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

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

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

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

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

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

1034
                self._dataframe, metadata = self.dc.append_delay_axis(
×
1035
                    self._dataframe,
1036
                    datafile=datafile,
1037
                    **kwds,
1038
                )
1039

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

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

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

1064
        self._dataframe = self._dataframe.map_partitions(
×
1065
            apply_jitter,
1066
            cols=cols,
1067
            cols_jittered=cols,
1068
        )
1069
        metadata = []
×
1070
        for col in cols:
×
1071
            metadata.append(col)
×
1072
        self._attributes.add(metadata, "jittering", duplicate_policy="append")
×
1073

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

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

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

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

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

1121
        return self.compute(
3✔
1122
            bins=bins,
1123
            axes=axes,
1124
            ranges=ranges,
1125
            df_partitions=df_partitions,
1126
            **kwds,
1127
        )
1128

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

1145
        Args:
1146
            bins (int, dict, tuple, List[int], List[np.ndarray], List[tuple], optional):
1147
                Definition of the bins. Can be any of the following cases:
1148

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

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

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

1183
                Additional kwds are passed to ``bin_dataframe``.
1184

1185
        Raises:
1186
            AssertError: Rises when no dataframe has been loaded.
1187

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

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

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

1228
        for dim in self._binned.dims:
3✔
1229
            try:
3✔
1230
                self._binned[dim].attrs["unit"] = self._config["dataframe"]["units"][dim]
3✔
1231
            except KeyError:
×
1232
                pass
×
1233

1234
        self._binned.attrs["units"] = "counts"
3✔
1235
        self._binned.attrs["long_name"] = "photoelectron counts"
3✔
1236
        self._binned.attrs["metadata"] = self._attributes.metadata
3✔
1237

1238
        return self._binned
3✔
1239

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

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

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

1286
        input_types = map(type, [axes, bins, ranges])
×
1287
        allowed_types = [list, tuple]
×
1288

1289
        df = self._dataframe
×
1290

1291
        if not set(input_types).issubset(allowed_types):
×
1292
            raise TypeError(
×
1293
                "Inputs of axes, bins, ranges need to be list or tuple!",
1294
            )
1295

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

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

1318
    def save(
3✔
1319
        self,
1320
        faddr: str,
1321
        **kwds,
1322
    ):
1323
        """Saves the binned data to the provided path and filename.
1324

1325
        Args:
1326
            faddr (str): Path and name of the file to write. Its extension determines
1327
                the file type to write. Valid file types are:
1328

1329
                - "*.tiff", "*.tif": Saves a TIFF stack.
1330
                - "*.h5", "*.hdf5": Saves an HDF5 file.
1331
                - "*.nxs", "*.nexus": Saves a NeXus file.
1332

1333
            **kwds: Keyword argumens, which are passed to the writer functions:
1334
                For TIFF writing:
1335

1336
                - **alias_dict**: Dictionary of dimension aliases to use.
1337

1338
                For HDF5 writing:
1339

1340
                - **mode**: hdf5 read/write mode. Defaults to "w".
1341

1342
                For NeXus:
1343

1344
                - **reader**: Name of the nexustools reader to use.
1345
                  Defaults to config["nexus"]["reader"]
1346
                - **definiton**: NeXus application definition to use for saving.
1347
                  Must be supported by the used ``reader``. Defaults to
1348
                  config["nexus"]["definition"]
1349
                - **input_files**: A list of input files to pass to the reader.
1350
                  Defaults to config["nexus"]["input_files"]
1351
                - **eln_data**: An electronic-lab-notebook file in '.yaml' format
1352
                  to add to the list of files to pass to the reader.
1353
        """
1354
        if self._binned is None:
×
1355
            raise NameError("Need to bin data first!")
×
1356

1357
        extension = pathlib.Path(faddr).suffix
×
1358

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

1384
            if "eln_data" in kwds:
×
1385
                input_files.append(kwds.pop("eln_data"))
×
1386

1387
            to_nexus(
×
1388
                data=self._binned,
1389
                faddr=faddr,
1390
                reader=reader,
1391
                definition=definition,
1392
                input_files=input_files,
1393
                **kwds,
1394
            )
1395

1396
        else:
1397
            raise NotImplementedError(
×
1398
                f"Unrecognized file format: {extension}.",
1399
            )
1400

1401
    def add_dimension(self, name: str, axis_range: Tuple):
3✔
1402
        """Add a dimension axis.
1403

1404
        Args:
1405
            name (str): name of the axis
1406
            axis_range (Tuple): range for the axis.
1407

1408
        Raises:
1409
            ValueError: Raised if an axis with that name already exists.
1410
        """
1411
        if name in self._coordinates:
×
1412
            raise ValueError(f"Axis {name} already exists")
×
1413

1414
        self.axis[name] = self.make_axis(axis_range)
×
1415

1416
    def make_axis(self, axis_range: Tuple) -> np.ndarray:
3✔
1417
        """Function to make an axis.
1418

1419
        Args:
1420
            axis_range (Tuple): range for the new axis.
1421
        """
1422

1423
        # TODO: What shall this function do?
1424
        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