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

OpenCOMPES / sed / 7991449446

21 Feb 2024 03:23PM UTC coverage: 91.137% (+0.6%) from 90.56%
7991449446

Pull #321

github

web-flow
Merge branch 'main' into calibration_creation_date
Pull Request #321: Calibration creation date

597 of 652 new or added lines in 8 files covered. (91.56%)

1 existing line in 1 file now uncovered.

5933 of 6510 relevant lines covered (91.14%)

0.91 hits per line

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

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

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

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

21
from sed.binning import bin_dataframe
1✔
22
from sed.binning.binning import normalization_histogram_from_timed_dataframe
1✔
23
from sed.binning.binning import normalization_histogram_from_timestamps
1✔
24
from sed.calibrator import DelayCalibrator
1✔
25
from sed.calibrator import EnergyCalibrator
1✔
26
from sed.calibrator import MomentumCorrector
1✔
27
from sed.core.config import parse_config
1✔
28
from sed.core.config import save_config
1✔
29
from sed.core.dfops import add_time_stamped_data
1✔
30
from sed.core.dfops import apply_filter
1✔
31
from sed.core.dfops import apply_jitter
1✔
32
from sed.core.metadata import MetaHandler
1✔
33
from sed.diagnostics import grid_histogram
1✔
34
from sed.io import to_h5
1✔
35
from sed.io import to_nexus
1✔
36
from sed.io import to_tiff
1✔
37
from sed.loader import CopyTool
1✔
38
from sed.loader import get_loader
1✔
39
from sed.loader.mpes.loader import get_archiver_data
1✔
40
from sed.loader.mpes.loader import MpesLoader
1✔
41

42
N_CPU = psutil.cpu_count()
1✔
43

44

45
class SedProcessor:
1✔
46
    """Processor class of sed. Contains wrapper functions defining a work flow for data
47
    correction, calibration and binning.
48

49
    Args:
50
        metadata (dict, optional): Dict of external Metadata. Defaults to None.
51
        config (Union[dict, str], optional): Config dictionary or config file name.
52
            Defaults to None.
53
        dataframe (Union[pd.DataFrame, ddf.DataFrame], optional): dataframe to load
54
            into the class. Defaults to None.
55
        files (List[str], optional): List of files to pass to the loader defined in
56
            the config. Defaults to None.
57
        folder (str, optional): Folder containing files to pass to the loader
58
            defined in the config. Defaults to None.
59
        runs (Sequence[str], optional): List of run identifiers to pass to the loader
60
            defined in the config. Defaults to None.
61
        collect_metadata (bool): Option to collect metadata from files.
62
            Defaults to False.
63
        verbose (bool, optional): Option to print out diagnostic information.
64
            Defaults to config["core"]["verbose"] or False.
65
        **kwds: Keyword arguments passed to the reader.
66
    """
67

68
    def __init__(
1✔
69
        self,
70
        metadata: dict = None,
71
        config: Union[dict, str] = None,
72
        dataframe: Union[pd.DataFrame, ddf.DataFrame] = None,
73
        files: List[str] = None,
74
        folder: str = None,
75
        runs: Sequence[str] = None,
76
        collect_metadata: bool = False,
77
        verbose: bool = None,
78
        **kwds,
79
    ):
80
        """Processor class of sed. Contains wrapper functions defining a work flow
81
        for data correction, calibration, and binning.
82

83
        Args:
84
            metadata (dict, optional): Dict of external Metadata. Defaults to None.
85
            config (Union[dict, str], optional): Config dictionary or config file name.
86
                Defaults to None.
87
            dataframe (Union[pd.DataFrame, ddf.DataFrame], optional): dataframe to load
88
                into the class. Defaults to None.
89
            files (List[str], optional): List of files to pass to the loader defined in
90
                the config. Defaults to None.
91
            folder (str, optional): Folder containing files to pass to the loader
92
                defined in the config. Defaults to None.
93
            runs (Sequence[str], optional): List of run identifiers to pass to the loader
94
                defined in the config. Defaults to None.
95
            collect_metadata (bool, optional): Option to collect metadata from files.
96
                Defaults to False.
97
            verbose (bool, optional): Option to print out diagnostic information.
98
                Defaults to config["core"]["verbose"] or False.
99
            **kwds: Keyword arguments passed to parse_config and to the reader.
100
        """
101
        config_kwds = {
1✔
102
            key: value for key, value in kwds.items() if key in parse_config.__code__.co_varnames
103
        }
104
        for key in config_kwds.keys():
1✔
105
            del kwds[key]
1✔
106
        self._config = parse_config(config, **config_kwds)
1✔
107
        num_cores = self._config.get("binning", {}).get("num_cores", N_CPU - 1)
1✔
108
        if num_cores >= N_CPU:
1✔
109
            num_cores = N_CPU - 1
1✔
110
        self._config["binning"]["num_cores"] = num_cores
1✔
111

112
        if verbose is None:
1✔
113
            self.verbose = self._config["core"].get("verbose", False)
1✔
114
        else:
115
            self.verbose = verbose
1✔
116

117
        self._dataframe: Union[pd.DataFrame, ddf.DataFrame] = None
1✔
118
        self._timed_dataframe: Union[pd.DataFrame, ddf.DataFrame] = None
1✔
119
        self._files: List[str] = []
1✔
120

121
        self._binned: xr.DataArray = None
1✔
122
        self._pre_binned: xr.DataArray = None
1✔
123
        self._normalization_histogram: xr.DataArray = None
1✔
124
        self._normalized: xr.DataArray = None
1✔
125

126
        self._attributes = MetaHandler(meta=metadata)
1✔
127

128
        loader_name = self._config["core"]["loader"]
1✔
129
        self.loader = get_loader(
1✔
130
            loader_name=loader_name,
131
            config=self._config,
132
        )
133

134
        self.ec = EnergyCalibrator(
1✔
135
            loader=get_loader(
136
                loader_name=loader_name,
137
                config=self._config,
138
            ),
139
            config=self._config,
140
        )
141

142
        self.mc = MomentumCorrector(
1✔
143
            config=self._config,
144
        )
145

146
        self.dc = DelayCalibrator(
1✔
147
            config=self._config,
148
        )
149

150
        self.use_copy_tool = self._config.get("core", {}).get(
1✔
151
            "use_copy_tool",
152
            False,
153
        )
154
        if self.use_copy_tool:
1✔
155
            try:
1✔
156
                self.ct = CopyTool(
1✔
157
                    source=self._config["core"]["copy_tool_source"],
158
                    dest=self._config["core"]["copy_tool_dest"],
159
                    **self._config["core"].get("copy_tool_kwds", {}),
160
                )
161
            except KeyError:
1✔
162
                self.use_copy_tool = False
1✔
163

164
        # Load data if provided:
165
        if dataframe is not None or files is not None or folder is not None or runs is not None:
1✔
166
            self.load(
1✔
167
                dataframe=dataframe,
168
                metadata=metadata,
169
                files=files,
170
                folder=folder,
171
                runs=runs,
172
                collect_metadata=collect_metadata,
173
                **kwds,
174
            )
175

176
    def __repr__(self):
1✔
177
        if self._dataframe is None:
1✔
178
            df_str = "Data Frame: No Data loaded"
1✔
179
        else:
180
            df_str = self._dataframe.__repr__()
1✔
181
        attributes_str = f"Metadata: {self._attributes.metadata}"
1✔
182
        pretty_str = df_str + "\n" + attributes_str
1✔
183
        return pretty_str
1✔
184

185
    @property
1✔
186
    def dataframe(self) -> Union[pd.DataFrame, ddf.DataFrame]:
1✔
187
        """Accessor to the underlying dataframe.
188

189
        Returns:
190
            Union[pd.DataFrame, ddf.DataFrame]: Dataframe object.
191
        """
192
        return self._dataframe
1✔
193

194
    @dataframe.setter
1✔
195
    def dataframe(self, dataframe: Union[pd.DataFrame, ddf.DataFrame]):
1✔
196
        """Setter for the underlying dataframe.
197

198
        Args:
199
            dataframe (Union[pd.DataFrame, ddf.DataFrame]): The dataframe object to set.
200
        """
201
        if not isinstance(dataframe, (pd.DataFrame, ddf.DataFrame)) or not isinstance(
1✔
202
            dataframe,
203
            self._dataframe.__class__,
204
        ):
205
            raise ValueError(
1✔
206
                "'dataframe' has to be a Pandas or Dask dataframe and has to be of the same kind "
207
                "as the dataframe loaded into the SedProcessor!.\n"
208
                f"Loaded type: {self._dataframe.__class__}, provided type: {dataframe}.",
209
            )
210
        self._dataframe = dataframe
1✔
211

212
    @property
1✔
213
    def timed_dataframe(self) -> Union[pd.DataFrame, ddf.DataFrame]:
1✔
214
        """Accessor to the underlying timed_dataframe.
215

216
        Returns:
217
            Union[pd.DataFrame, ddf.DataFrame]: Timed Dataframe object.
218
        """
219
        return self._timed_dataframe
1✔
220

221
    @timed_dataframe.setter
1✔
222
    def timed_dataframe(self, timed_dataframe: Union[pd.DataFrame, ddf.DataFrame]):
1✔
223
        """Setter for the underlying timed dataframe.
224

225
        Args:
226
            timed_dataframe (Union[pd.DataFrame, ddf.DataFrame]): The timed dataframe object to set
227
        """
228
        if not isinstance(timed_dataframe, (pd.DataFrame, ddf.DataFrame)) or not isinstance(
×
229
            timed_dataframe,
230
            self._timed_dataframe.__class__,
231
        ):
232
            raise ValueError(
×
233
                "'timed_dataframe' has to be a Pandas or Dask dataframe and has to be of the same "
234
                "kind as the dataframe loaded into the SedProcessor!.\n"
235
                f"Loaded type: {self._timed_dataframe.__class__}, "
236
                f"provided type: {timed_dataframe}.",
237
            )
238
        self._timed_dataframe = timed_dataframe
×
239

240
    @property
1✔
241
    def attributes(self) -> dict:
1✔
242
        """Accessor to the metadata dict.
243

244
        Returns:
245
            dict: The metadata dict.
246
        """
247
        return self._attributes.metadata
1✔
248

249
    def add_attribute(self, attributes: dict, name: str, **kwds):
1✔
250
        """Function to add element to the attributes dict.
251

252
        Args:
253
            attributes (dict): The attributes dictionary object to add.
254
            name (str): Key under which to add the dictionary to the attributes.
255
        """
256
        self._attributes.add(
1✔
257
            entry=attributes,
258
            name=name,
259
            **kwds,
260
        )
261

262
    @property
1✔
263
    def config(self) -> Dict[Any, Any]:
1✔
264
        """Getter attribute for the config dictionary
265

266
        Returns:
267
            Dict: The config dictionary.
268
        """
269
        return self._config
1✔
270

271
    @property
1✔
272
    def files(self) -> List[str]:
1✔
273
        """Getter attribute for the list of files
274

275
        Returns:
276
            List[str]: The list of loaded files
277
        """
278
        return self._files
1✔
279

280
    @property
1✔
281
    def binned(self) -> xr.DataArray:
1✔
282
        """Getter attribute for the binned data array
283

284
        Returns:
285
            xr.DataArray: The binned data array
286
        """
287
        if self._binned is None:
1✔
288
            raise ValueError("No binned data available, need to compute histogram first!")
×
289
        return self._binned
1✔
290

291
    @property
1✔
292
    def normalized(self) -> xr.DataArray:
1✔
293
        """Getter attribute for the normalized data array
294

295
        Returns:
296
            xr.DataArray: The normalized data array
297
        """
298
        if self._normalized is None:
1✔
299
            raise ValueError(
×
300
                "No normalized data available, compute data with normalization enabled!",
301
            )
302
        return self._normalized
1✔
303

304
    @property
1✔
305
    def normalization_histogram(self) -> xr.DataArray:
1✔
306
        """Getter attribute for the normalization histogram
307

308
        Returns:
309
            xr.DataArray: The normalizazion histogram
310
        """
311
        if self._normalization_histogram is None:
1✔
312
            raise ValueError("No normalization histogram available, generate histogram first!")
×
313
        return self._normalization_histogram
1✔
314

315
    def cpy(self, path: Union[str, List[str]]) -> Union[str, List[str]]:
1✔
316
        """Function to mirror a list of files or a folder from a network drive to a
317
        local storage. Returns either the original or the copied path to the given
318
        path. The option to use this functionality is set by
319
        config["core"]["use_copy_tool"].
320

321
        Args:
322
            path (Union[str, List[str]]): Source path or path list.
323

324
        Returns:
325
            Union[str, List[str]]: Source or destination path or path list.
326
        """
327
        if self.use_copy_tool:
1✔
328
            if isinstance(path, list):
1✔
329
                path_out = []
1✔
330
                for file in path:
1✔
331
                    path_out.append(self.ct.copy(file))
1✔
332
                return path_out
1✔
333

334
            return self.ct.copy(path)
×
335

336
        if isinstance(path, list):
1✔
337
            return path
1✔
338

339
        return path
1✔
340

341
    def load(
1✔
342
        self,
343
        dataframe: Union[pd.DataFrame, ddf.DataFrame] = None,
344
        metadata: dict = None,
345
        files: List[str] = None,
346
        folder: str = None,
347
        runs: Sequence[str] = None,
348
        collect_metadata: bool = False,
349
        **kwds,
350
    ):
351
        """Load tabular data of single events into the dataframe object in the class.
352

353
        Args:
354
            dataframe (Union[pd.DataFrame, ddf.DataFrame], optional): data in tabular
355
                format. Accepts anything which can be interpreted by pd.DataFrame as
356
                an input. Defaults to None.
357
            metadata (dict, optional): Dict of external Metadata. Defaults to None.
358
            files (List[str], optional): List of file paths to pass to the loader.
359
                Defaults to None.
360
            runs (Sequence[str], optional): List of run identifiers to pass to the
361
                loader. Defaults to None.
362
            folder (str, optional): Folder path to pass to the loader.
363
                Defaults to None.
364
            collect_metadata (bool, optional): Option for collecting metadata in the reader.
365
            **kwds: Keyword parameters passed to the reader.
366

367
        Raises:
368
            ValueError: Raised if no valid input is provided.
369
        """
370
        if metadata is None:
1✔
371
            metadata = {}
1✔
372
        if dataframe is not None:
1✔
373
            timed_dataframe = kwds.pop("timed_dataframe", None)
1✔
374
        elif runs is not None:
1✔
375
            # If runs are provided, we only use the copy tool if also folder is provided.
376
            # In that case, we copy the whole provided base folder tree, and pass the copied
377
            # version to the loader as base folder to look for the runs.
378
            if folder is not None:
1✔
379
                dataframe, timed_dataframe, metadata = self.loader.read_dataframe(
1✔
380
                    folders=cast(str, self.cpy(folder)),
381
                    runs=runs,
382
                    metadata=metadata,
383
                    collect_metadata=collect_metadata,
384
                    **kwds,
385
                )
386
            else:
387
                dataframe, timed_dataframe, metadata = self.loader.read_dataframe(
×
388
                    runs=runs,
389
                    metadata=metadata,
390
                    collect_metadata=collect_metadata,
391
                    **kwds,
392
                )
393

394
        elif folder is not None:
1✔
395
            dataframe, timed_dataframe, metadata = self.loader.read_dataframe(
1✔
396
                folders=cast(str, self.cpy(folder)),
397
                metadata=metadata,
398
                collect_metadata=collect_metadata,
399
                **kwds,
400
            )
401
        elif files is not None:
1✔
402
            dataframe, timed_dataframe, metadata = self.loader.read_dataframe(
1✔
403
                files=cast(List[str], self.cpy(files)),
404
                metadata=metadata,
405
                collect_metadata=collect_metadata,
406
                **kwds,
407
            )
408
        else:
409
            raise ValueError(
1✔
410
                "Either 'dataframe', 'files', 'folder', or 'runs' needs to be provided!",
411
            )
412

413
        self._dataframe = dataframe
1✔
414
        self._timed_dataframe = timed_dataframe
1✔
415
        self._files = self.loader.files
1✔
416

417
        for key in metadata:
1✔
418
            self._attributes.add(
1✔
419
                entry=metadata[key],
420
                name=key,
421
                duplicate_policy="merge",
422
            )
423

424
    def filter_column(
1✔
425
        self,
426
        column: str,
427
        min_value: float = -np.inf,
428
        max_value: float = np.inf,
429
    ) -> None:
430
        """Filter values in a column which are outside of a given range
431

432
        Args:
433
            column (str): Name of the column to filter
434
            min_value (float, optional): Minimum value to keep. Defaults to None.
435
            max_value (float, optional): Maximum value to keep. Defaults to None.
436
        """
437
        if column != "index" and column not in self._dataframe.columns:
1✔
438
            raise KeyError(f"Column {column} not found in dataframe!")
1✔
439
        if min_value >= max_value:
1✔
440
            raise ValueError("min_value has to be smaller than max_value!")
1✔
441
        if self._dataframe is not None:
1✔
442
            self._dataframe = apply_filter(
1✔
443
                self._dataframe,
444
                col=column,
445
                lower_bound=min_value,
446
                upper_bound=max_value,
447
            )
448
        if self._timed_dataframe is not None and column in self._timed_dataframe.columns:
1✔
449
            self._timed_dataframe = apply_filter(
1✔
450
                self._timed_dataframe,
451
                column,
452
                lower_bound=min_value,
453
                upper_bound=max_value,
454
            )
455
        metadata = {
1✔
456
            "filter": {
457
                "column": column,
458
                "min_value": min_value,
459
                "max_value": max_value,
460
            },
461
        }
462
        self._attributes.add(metadata, "filter", duplicate_policy="merge")
1✔
463

464
    # Momentum calibration workflow
465
    # 1. Bin raw detector data for distortion correction
466
    def bin_and_load_momentum_calibration(
1✔
467
        self,
468
        df_partitions: Union[int, Sequence[int]] = 100,
469
        axes: List[str] = None,
470
        bins: List[int] = None,
471
        ranges: Sequence[Tuple[float, float]] = None,
472
        plane: int = 0,
473
        width: int = 5,
474
        apply: bool = False,
475
        **kwds,
476
    ):
477
        """1st step of momentum correction work flow. Function to do an initial binning
478
        of the dataframe loaded to the class, slice a plane from it using an
479
        interactive view, and load it into the momentum corrector class.
480

481
        Args:
482
            df_partitions (Union[int, Sequence[int]], optional): Number of dataframe partitions
483
                to use for the initial binning. Defaults to 100.
484
            axes (List[str], optional): Axes to bin.
485
                Defaults to config["momentum"]["axes"].
486
            bins (List[int], optional): Bin numbers to use for binning.
487
                Defaults to config["momentum"]["bins"].
488
            ranges (List[Tuple], optional): Ranges to use for binning.
489
                Defaults to config["momentum"]["ranges"].
490
            plane (int, optional): Initial value for the plane slider. Defaults to 0.
491
            width (int, optional): Initial value for the width slider. Defaults to 5.
492
            apply (bool, optional): Option to directly apply the values and select the
493
                slice. Defaults to False.
494
            **kwds: Keyword argument passed to the pre_binning function.
495
        """
496
        self._pre_binned = self.pre_binning(
1✔
497
            df_partitions=df_partitions,
498
            axes=axes,
499
            bins=bins,
500
            ranges=ranges,
501
            **kwds,
502
        )
503

504
        self.mc.load_data(data=self._pre_binned)
1✔
505
        self.mc.select_slicer(plane=plane, width=width, apply=apply)
1✔
506

507
    # 2. Generate the spline warp correction from momentum features.
508
    # Either autoselect features, or input features from view above.
509
    def define_features(
1✔
510
        self,
511
        features: np.ndarray = None,
512
        rotation_symmetry: int = 6,
513
        auto_detect: bool = False,
514
        include_center: bool = True,
515
        apply: bool = False,
516
        **kwds,
517
    ):
518
        """2. Step of the distortion correction workflow: Define feature points in
519
        momentum space. They can be either manually selected using a GUI tool, be
520
        ptovided as list of feature points, or auto-generated using a
521
        feature-detection algorithm.
522

523
        Args:
524
            features (np.ndarray, optional): np.ndarray of features. Defaults to None.
525
            rotation_symmetry (int, optional): Number of rotational symmetry axes.
526
                Defaults to 6.
527
            auto_detect (bool, optional): Whether to auto-detect the features.
528
                Defaults to False.
529
            include_center (bool, optional): Option to include a point at the center
530
                in the feature list. Defaults to True.
531
            apply (bool, optional): Option to directly apply the values and select the
532
                slice. Defaults to False.
533
            **kwds: Keyword arguments for ``MomentumCorrector.feature_extract()`` and
534
                ``MomentumCorrector.feature_select()``.
535
        """
536
        if auto_detect:  # automatic feature selection
1✔
537
            sigma = kwds.pop("sigma", self._config["momentum"]["sigma"])
×
538
            fwhm = kwds.pop("fwhm", self._config["momentum"]["fwhm"])
×
539
            sigma_radius = kwds.pop(
×
540
                "sigma_radius",
541
                self._config["momentum"]["sigma_radius"],
542
            )
543
            self.mc.feature_extract(
×
544
                sigma=sigma,
545
                fwhm=fwhm,
546
                sigma_radius=sigma_radius,
547
                rotsym=rotation_symmetry,
548
                **kwds,
549
            )
550
            features = self.mc.peaks
×
551

552
        self.mc.feature_select(
1✔
553
            rotsym=rotation_symmetry,
554
            include_center=include_center,
555
            features=features,
556
            apply=apply,
557
            **kwds,
558
        )
559

560
    # 3. Generate the spline warp correction from momentum features.
561
    # If no features have been selected before, use class defaults.
562
    def generate_splinewarp(
1✔
563
        self,
564
        use_center: bool = None,
565
        verbose: bool = None,
566
        **kwds,
567
    ):
568
        """3. Step of the distortion correction workflow: Generate the correction
569
        function restoring the symmetry in the image using a splinewarp algortihm.
570

571
        Args:
572
            use_center (bool, optional): Option to use the position of the
573
                center point in the correction. Default is read from config, or set to True.
574
            verbose (bool, optional): Option to print out diagnostic information.
575
                Defaults to config["core"]["verbose"].
576
            **kwds: Keyword arguments for MomentumCorrector.spline_warp_estimate().
577
        """
578
        if verbose is None:
1✔
579
            verbose = self.verbose
1✔
580

581
        self.mc.spline_warp_estimate(use_center=use_center, verbose=verbose, **kwds)
1✔
582

583
        if self.mc.slice is not None and verbose:
1✔
584
            print("Original slice with reference features")
1✔
585
            self.mc.view(annotated=True, backend="bokeh", crosshair=True)
1✔
586

587
            print("Corrected slice with target features")
1✔
588
            self.mc.view(
1✔
589
                image=self.mc.slice_corrected,
590
                annotated=True,
591
                points={"feats": self.mc.ptargs},
592
                backend="bokeh",
593
                crosshair=True,
594
            )
595

596
            print("Original slice with target features")
1✔
597
            self.mc.view(
1✔
598
                image=self.mc.slice,
599
                points={"feats": self.mc.ptargs},
600
                annotated=True,
601
                backend="bokeh",
602
            )
603

604
    # 3a. Save spline-warp parameters to config file.
605
    def save_splinewarp(
1✔
606
        self,
607
        filename: str = None,
608
        overwrite: bool = False,
609
    ):
610
        """Save the generated spline-warp parameters to the folder config file.
611

612
        Args:
613
            filename (str, optional): Filename of the config dictionary to save to.
614
                Defaults to "sed_config.yaml" in the current folder.
615
            overwrite (bool, optional): Option to overwrite the present dictionary.
616
                Defaults to False.
617
        """
618
        if filename is None:
1✔
619
            filename = "sed_config.yaml"
×
620
        if len(self.mc.correction) == 0:
1✔
NEW
621
            raise ValueError("No momentum correction parameters to save!")
×
622
        correction = {}
1✔
623
        for key, value in self.mc.correction.items():
1✔
624
            if key in ["reference_points", "target_points", "cdeform_field", "rdeform_field"]:
1✔
625
                continue
1✔
626
            if key in ["use_center", "rotation_symmetry"]:
1✔
627
                correction[key] = value
1✔
628
            elif key == "center_point":
1✔
629
                correction[key] = [float(i) for i in value]
1✔
630
            elif key in ["outer_points", "feature_points"]:
1✔
631
                correction[key] = []
1✔
632
                for point in value:
1✔
633
                    correction[key].append([float(i) for i in point])
1✔
634
            else:
635
                correction[key] = float(value)
1✔
636

637
        if "creation_date" not in correction:
1✔
NEW
638
            correction["creation_date"] = datetime.now().timestamp()
×
639

640
        config = {
1✔
641
            "momentum": {
642
                "correction": correction,
643
            },
644
        }
645
        save_config(config, filename, overwrite)
1✔
646
        print(f'Saved momentum correction parameters to "{filename}".')
1✔
647

648
    # 4. Pose corrections. Provide interactive interface for correcting
649
    # scaling, shift and rotation
650
    def pose_adjustment(
1✔
651
        self,
652
        transformations: Dict[str, Any] = None,
653
        apply: bool = False,
654
        use_correction: bool = True,
655
        reset: bool = True,
656
        verbose: bool = None,
657
        **kwds,
658
    ):
659
        """3. step of the distortion correction workflow: Generate an interactive panel
660
        to adjust affine transformations that are applied to the image. Applies first
661
        a scaling, next an x/y translation, and last a rotation around the center of
662
        the image.
663

664
        Args:
665
            transformations (dict, optional): Dictionary with transformations.
666
                Defaults to self.transformations or config["momentum"]["transformtions"].
667
            apply (bool, optional): Option to directly apply the provided
668
                transformations. Defaults to False.
669
            use_correction (bool, option): Whether to use the spline warp correction
670
                or not. Defaults to True.
671
            reset (bool, optional): Option to reset the correction before transformation.
672
                Defaults to True.
673
            verbose (bool, optional): Option to print out diagnostic information.
674
                Defaults to config["core"]["verbose"].
675
            **kwds: Keyword parameters defining defaults for the transformations:
676

677
                - **scale** (float): Initial value of the scaling slider.
678
                - **xtrans** (float): Initial value of the xtrans slider.
679
                - **ytrans** (float): Initial value of the ytrans slider.
680
                - **angle** (float): Initial value of the angle slider.
681
        """
682
        if verbose is None:
1✔
683
            verbose = self.verbose
1✔
684

685
        # Generate homomorphy as default if no distortion correction has been applied
686
        if self.mc.slice_corrected is None:
1✔
687
            if self.mc.slice is None:
1✔
688
                self.mc.slice = np.zeros(self._config["momentum"]["bins"][0:2])
1✔
689
            self.mc.slice_corrected = self.mc.slice
1✔
690

691
        if not use_correction:
1✔
692
            self.mc.reset_deformation()
1✔
693

694
        if self.mc.cdeform_field is None or self.mc.rdeform_field is None:
1✔
695
            # Generate distortion correction from config values
NEW
696
            self.mc.spline_warp_estimate(verbose=verbose)
×
697

698
        self.mc.pose_adjustment(
1✔
699
            transformations=transformations,
700
            apply=apply,
701
            reset=reset,
702
            verbose=verbose,
703
            **kwds,
704
        )
705

706
    # 4a. Save pose adjustment parameters to config file.
707
    def save_transformations(
1✔
708
        self,
709
        filename: str = None,
710
        overwrite: bool = False,
711
    ):
712
        """Save the pose adjustment parameters to the folder config file.
713

714
        Args:
715
            filename (str, optional): Filename of the config dictionary to save to.
716
                Defaults to "sed_config.yaml" in the current folder.
717
            overwrite (bool, optional): Option to overwrite the present dictionary.
718
                Defaults to False.
719
        """
720
        if filename is None:
1✔
NEW
721
            filename = "sed_config.yaml"
×
722
        if len(self.mc.transformations) == 0:
1✔
NEW
723
            raise ValueError("No momentum transformation parameters to save!")
×
724
        transformations = {}
1✔
725
        for key, value in self.mc.transformations.items():
1✔
726
            transformations[key] = float(value)
1✔
727

728
        if "creation_date" not in transformations:
1✔
NEW
729
            transformations["creation_date"] = datetime.now().timestamp()
×
730

731
        config = {
1✔
732
            "momentum": {
733
                "transformations": transformations,
734
            },
735
        }
736
        save_config(config, filename, overwrite)
1✔
737
        print(f'Saved momentum transformation parameters to "{filename}".')
1✔
738

739
    # 5. Apply the momentum correction to the dataframe
740
    def apply_momentum_correction(
1✔
741
        self,
742
        preview: bool = False,
743
        verbose: bool = None,
744
        **kwds,
745
    ):
746
        """Applies the distortion correction and pose adjustment (optional)
747
        to the dataframe.
748

749
        Args:
750
            preview (bool, optional): Option to preview the first elements of the data frame.
751
                Defaults to False.
752
            verbose (bool, optional): Option to print out diagnostic information.
753
                Defaults to config["core"]["verbose"].
754
            **kwds: Keyword parameters for ``MomentumCorrector.apply_correction``:
755

756
                - **rdeform_field** (np.ndarray, optional): Row deformation field.
757
                - **cdeform_field** (np.ndarray, optional): Column deformation field.
758
                - **inv_dfield** (np.ndarray, optional): Inverse deformation field.
759

760
        """
761
        if verbose is None:
1✔
762
            verbose = self.verbose
1✔
763

764
        x_column = self._config["dataframe"]["x_column"]
1✔
765
        y_column = self._config["dataframe"]["y_column"]
1✔
766

767
        if self._dataframe is not None:
1✔
768
            if verbose:
1✔
769
                print("Adding corrected X/Y columns to dataframe:")
1✔
770
            df, metadata = self.mc.apply_corrections(
1✔
771
                df=self._dataframe,
772
                verbose=verbose,
773
                **kwds,
774
            )
775
            if (
1✔
776
                self._timed_dataframe is not None
777
                and x_column in self._timed_dataframe.columns
778
                and y_column in self._timed_dataframe.columns
779
            ):
780
                tdf, _ = self.mc.apply_corrections(
1✔
781
                    self._timed_dataframe,
782
                    verbose=False,
783
                    **kwds,
784
                )
785

786
            # Add Metadata
787
            self._attributes.add(
1✔
788
                metadata,
789
                "momentum_correction",
790
                duplicate_policy="merge",
791
            )
792
            self._dataframe = df
1✔
793
            if (
1✔
794
                self._timed_dataframe is not None
795
                and x_column in self._timed_dataframe.columns
796
                and y_column in self._timed_dataframe.columns
797
            ):
798
                self._timed_dataframe = tdf
1✔
799
        else:
NEW
800
            raise ValueError("No dataframe loaded!")
×
801
        if preview:
1✔
NEW
802
            print(self._dataframe.head(10))
×
803
        else:
804
            if self.verbose:
1✔
805
                print(self._dataframe)
1✔
806

807
    # Momentum calibration work flow
808
    # 1. Calculate momentum calibration
809
    def calibrate_momentum_axes(
1✔
810
        self,
811
        point_a: Union[np.ndarray, List[int]] = None,
812
        point_b: Union[np.ndarray, List[int]] = None,
813
        k_distance: float = None,
814
        k_coord_a: Union[np.ndarray, List[float]] = None,
815
        k_coord_b: Union[np.ndarray, List[float]] = np.array([0.0, 0.0]),
816
        equiscale: bool = True,
817
        apply=False,
818
    ):
819
        """1. step of the momentum calibration workflow. Calibrate momentum
820
        axes using either provided pixel coordinates of a high-symmetry point and its
821
        distance to the BZ center, or the k-coordinates of two points in the BZ
822
        (depending on the equiscale option). Opens an interactive panel for selecting
823
        the points.
824

825
        Args:
826
            point_a (Union[np.ndarray, List[int]]): Pixel coordinates of the first
827
                point used for momentum calibration.
828
            point_b (Union[np.ndarray, List[int]], optional): Pixel coordinates of the
829
                second point used for momentum calibration.
830
                Defaults to config["momentum"]["center_pixel"].
831
            k_distance (float, optional): Momentum distance between point a and b.
832
                Needs to be provided if no specific k-koordinates for the two points
833
                are given. Defaults to None.
834
            k_coord_a (Union[np.ndarray, List[float]], optional): Momentum coordinate
835
                of the first point used for calibration. Used if equiscale is False.
836
                Defaults to None.
837
            k_coord_b (Union[np.ndarray, List[float]], optional): Momentum coordinate
838
                of the second point used for calibration. Defaults to [0.0, 0.0].
839
            equiscale (bool, optional): Option to apply different scales to kx and ky.
840
                If True, the distance between points a and b, and the absolute
841
                position of point a are used for defining the scale. If False, the
842
                scale is calculated from the k-positions of both points a and b.
843
                Defaults to True.
844
            apply (bool, optional): Option to directly store the momentum calibration
845
                in the class. Defaults to False.
846
        """
847
        if point_b is None:
1✔
848
            point_b = self._config["momentum"]["center_pixel"]
1✔
849

850
        self.mc.select_k_range(
1✔
851
            point_a=point_a,
852
            point_b=point_b,
853
            k_distance=k_distance,
854
            k_coord_a=k_coord_a,
855
            k_coord_b=k_coord_b,
856
            equiscale=equiscale,
857
            apply=apply,
858
        )
859

860
    # 1a. Save momentum calibration parameters to config file.
861
    def save_momentum_calibration(
1✔
862
        self,
863
        filename: str = None,
864
        overwrite: bool = False,
865
    ):
866
        """Save the generated momentum calibration parameters to the folder config file.
867

868
        Args:
869
            filename (str, optional): Filename of the config dictionary to save to.
870
                Defaults to "sed_config.yaml" in the current folder.
871
            overwrite (bool, optional): Option to overwrite the present dictionary.
872
                Defaults to False.
873
        """
874
        if filename is None:
1✔
875
            filename = "sed_config.yaml"
×
876
        if len(self.mc.calibration) == 0:
1✔
NEW
877
            raise ValueError("No momentum calibration parameters to save!")
×
878
        calibration = {}
1✔
879
        for key, value in self.mc.calibration.items():
1✔
880
            if key in ["kx_axis", "ky_axis", "grid", "extent"]:
1✔
881
                continue
1✔
882

883
            calibration[key] = float(value)
1✔
884

885
        if "creation_date" not in calibration:
1✔
NEW
886
            calibration["creation_date"] = datetime.now().timestamp()
×
887

888
        config = {"momentum": {"calibration": calibration}}
1✔
889
        save_config(config, filename, overwrite)
1✔
890
        print(f"Saved momentum calibration parameters to {filename}")
1✔
891

892
    # 2. Apply correction and calibration to the dataframe
893
    def apply_momentum_calibration(
1✔
894
        self,
895
        calibration: dict = None,
896
        preview: bool = False,
897
        verbose: bool = None,
898
        **kwds,
899
    ):
900
        """2. step of the momentum calibration work flow: Apply the momentum
901
        calibration stored in the class to the dataframe. If corrected X/Y axis exist,
902
        these are used.
903

904
        Args:
905
            calibration (dict, optional): Optional dictionary with calibration data to
906
                use. Defaults to None.
907
            preview (bool, optional): Option to preview the first elements of the data frame.
908
                Defaults to False.
909
            verbose (bool, optional): Option to print out diagnostic information.
910
                Defaults to config["core"]["verbose"].
911
            **kwds: Keyword args passed to ``DelayCalibrator.append_delay_axis``.
912
        """
913
        if verbose is None:
1✔
914
            verbose = self.verbose
1✔
915

916
        x_column = self._config["dataframe"]["x_column"]
1✔
917
        y_column = self._config["dataframe"]["y_column"]
1✔
918

919
        if self._dataframe is not None:
1✔
920
            if verbose:
1✔
921
                print("Adding kx/ky columns to dataframe:")
1✔
922
            df, metadata = self.mc.append_k_axis(
1✔
923
                df=self._dataframe,
924
                calibration=calibration,
925
                **kwds,
926
            )
927
            if (
1✔
928
                self._timed_dataframe is not None
929
                and x_column in self._timed_dataframe.columns
930
                and y_column in self._timed_dataframe.columns
931
            ):
932
                tdf, _ = self.mc.append_k_axis(
1✔
933
                    df=self._timed_dataframe,
934
                    calibration=calibration,
935
                    **kwds,
936
                )
937

938
            # Add Metadata
939
            self._attributes.add(
1✔
940
                metadata,
941
                "momentum_calibration",
942
                duplicate_policy="merge",
943
            )
944
            self._dataframe = df
1✔
945
            if (
1✔
946
                self._timed_dataframe is not None
947
                and x_column in self._timed_dataframe.columns
948
                and y_column in self._timed_dataframe.columns
949
            ):
950
                self._timed_dataframe = tdf
1✔
951
        else:
NEW
952
            raise ValueError("No dataframe loaded!")
×
953
        if preview:
1✔
NEW
954
            print(self._dataframe.head(10))
×
955
        else:
956
            if self.verbose:
1✔
957
                print(self._dataframe)
1✔
958

959
    # Energy correction workflow
960
    # 1. Adjust the energy correction parameters
961
    def adjust_energy_correction(
1✔
962
        self,
963
        correction_type: str = None,
964
        amplitude: float = None,
965
        center: Tuple[float, float] = None,
966
        apply=False,
967
        **kwds,
968
    ):
969
        """1. step of the energy crrection workflow: Opens an interactive plot to
970
        adjust the parameters for the TOF/energy correction. Also pre-bins the data if
971
        they are not present yet.
972

973
        Args:
974
            correction_type (str, optional): Type of correction to apply to the TOF
975
                axis. Valid values are:
976

977
                - 'spherical'
978
                - 'Lorentzian'
979
                - 'Gaussian'
980
                - 'Lorentzian_asymmetric'
981

982
                Defaults to config["energy"]["correction_type"].
983
            amplitude (float, optional): Amplitude of the correction.
984
                Defaults to config["energy"]["correction"]["amplitude"].
985
            center (Tuple[float, float], optional): Center X/Y coordinates for the
986
                correction. Defaults to config["energy"]["correction"]["center"].
987
            apply (bool, optional): Option to directly apply the provided or default
988
                correction parameters. Defaults to False.
989
            **kwds: Keyword parameters passed to ``EnergyCalibrator.adjust_energy_correction()``.
990
        """
991
        if self._pre_binned is None:
1✔
992
            print(
1✔
993
                "Pre-binned data not present, binning using defaults from config...",
994
            )
995
            self._pre_binned = self.pre_binning()
1✔
996

997
        self.ec.adjust_energy_correction(
1✔
998
            self._pre_binned,
999
            correction_type=correction_type,
1000
            amplitude=amplitude,
1001
            center=center,
1002
            apply=apply,
1003
            **kwds,
1004
        )
1005

1006
    # 1a. Save energy correction parameters to config file.
1007
    def save_energy_correction(
1✔
1008
        self,
1009
        filename: str = None,
1010
        overwrite: bool = False,
1011
    ):
1012
        """Save the generated energy correction parameters to the folder config file.
1013

1014
        Args:
1015
            filename (str, optional): Filename of the config dictionary to save to.
1016
                Defaults to "sed_config.yaml" in the current folder.
1017
            overwrite (bool, optional): Option to overwrite the present dictionary.
1018
                Defaults to False.
1019
        """
1020
        if filename is None:
1✔
1021
            filename = "sed_config.yaml"
1✔
1022
        if len(self.ec.correction) == 0:
1✔
NEW
1023
            raise ValueError("No energy correction parameters to save!")
×
1024
        correction = {}
1✔
1025
        for key, val in self.ec.correction.items():
1✔
1026
            if key == "correction_type":
1✔
1027
                correction[key] = val
1✔
1028
            elif key == "center":
1✔
1029
                correction[key] = [float(i) for i in val]
1✔
1030
            else:
1031
                correction[key] = float(val)
1✔
1032

1033
        if "creation_date" not in correction:
1✔
NEW
1034
            correction["creation_date"] = datetime.now().timestamp()
×
1035

1036
        config = {"energy": {"correction": correction}}
1✔
1037
        save_config(config, filename, overwrite)
1✔
1038
        print(f"Saved energy correction parameters to {filename}")
1✔
1039

1040
    # 2. Apply energy correction to dataframe
1041
    def apply_energy_correction(
1✔
1042
        self,
1043
        correction: dict = None,
1044
        preview: bool = False,
1045
        verbose: bool = None,
1046
        **kwds,
1047
    ):
1048
        """2. step of the energy correction workflow: Apply the enery correction
1049
        parameters stored in the class to the dataframe.
1050

1051
        Args:
1052
            correction (dict, optional): Dictionary containing the correction
1053
                parameters. Defaults to config["energy"]["calibration"].
1054
            preview (bool, optional): Option to preview the first elements of the data frame.
1055
                Defaults to False.
1056
            verbose (bool, optional): Option to print out diagnostic information.
1057
                Defaults to config["core"]["verbose"].
1058
            **kwds:
1059
                Keyword args passed to ``EnergyCalibrator.apply_energy_correction()``.
1060
        """
1061
        if verbose is None:
1✔
1062
            verbose = self.verbose
1✔
1063

1064
        tof_column = self._config["dataframe"]["tof_column"]
1✔
1065

1066
        if self._dataframe is not None:
1✔
1067
            if verbose:
1✔
1068
                print("Applying energy correction to dataframe...")
1✔
1069
            df, metadata = self.ec.apply_energy_correction(
1✔
1070
                df=self._dataframe,
1071
                correction=correction,
1072
                verbose=verbose,
1073
                **kwds,
1074
            )
1075
            if self._timed_dataframe is not None and tof_column in self._timed_dataframe.columns:
1✔
1076
                tdf, _ = self.ec.apply_energy_correction(
1✔
1077
                    df=self._timed_dataframe,
1078
                    correction=correction,
1079
                    verbose=False,
1080
                    **kwds,
1081
                )
1082

1083
            # Add Metadata
1084
            self._attributes.add(
1✔
1085
                metadata,
1086
                "energy_correction",
1087
            )
1088
            self._dataframe = df
1✔
1089
            if self._timed_dataframe is not None and tof_column in self._timed_dataframe.columns:
1✔
1090
                self._timed_dataframe = tdf
1✔
1091
        else:
NEW
1092
            raise ValueError("No dataframe loaded!")
×
1093
        if preview:
1✔
NEW
1094
            print(self._dataframe.head(10))
×
1095
        else:
1096
            if verbose:
1✔
NEW
1097
                print(self._dataframe)
×
1098

1099
    # Energy calibrator workflow
1100
    # 1. Load and normalize data
1101
    def load_bias_series(
1✔
1102
        self,
1103
        binned_data: Union[xr.DataArray, Tuple[np.ndarray, np.ndarray, np.ndarray]] = None,
1104
        data_files: List[str] = None,
1105
        axes: List[str] = None,
1106
        bins: List = None,
1107
        ranges: Sequence[Tuple[float, float]] = None,
1108
        biases: np.ndarray = None,
1109
        bias_key: str = None,
1110
        normalize: bool = None,
1111
        span: int = None,
1112
        order: int = None,
1113
    ):
1114
        """1. step of the energy calibration workflow: Load and bin data from
1115
        single-event files, or load binned bias/TOF traces.
1116

1117
        Args:
1118
            binned_data (Union[xr.DataArray, Tuple[np.ndarray, np.ndarray, np.ndarray]], optional):
1119
                Binned data If provided as DataArray, Needs to contain dimensions
1120
                config["dataframe"]["tof_column"] and config["dataframe"]["bias_column"]. If
1121
                provided as tuple, needs to contain elements tof, biases, traces.
1122
            data_files (List[str], optional): list of file paths to bin
1123
            axes (List[str], optional): bin axes.
1124
                Defaults to config["dataframe"]["tof_column"].
1125
            bins (List, optional): number of bins.
1126
                Defaults to config["energy"]["bins"].
1127
            ranges (Sequence[Tuple[float, float]], optional): bin ranges.
1128
                Defaults to config["energy"]["ranges"].
1129
            biases (np.ndarray, optional): Bias voltages used. If missing, bias
1130
                voltages are extracted from the data files.
1131
            bias_key (str, optional): hdf5 path where bias values are stored.
1132
                Defaults to config["energy"]["bias_key"].
1133
            normalize (bool, optional): Option to normalize traces.
1134
                Defaults to config["energy"]["normalize"].
1135
            span (int, optional): span smoothing parameters of the LOESS method
1136
                (see ``scipy.signal.savgol_filter()``).
1137
                Defaults to config["energy"]["normalize_span"].
1138
            order (int, optional): order smoothing parameters of the LOESS method
1139
                (see ``scipy.signal.savgol_filter()``).
1140
                Defaults to config["energy"]["normalize_order"].
1141
        """
1142
        if binned_data is not None:
1✔
1143
            if isinstance(binned_data, xr.DataArray):
1✔
1144
                if (
1✔
1145
                    self._config["dataframe"]["tof_column"] not in binned_data.dims
1146
                    or self._config["dataframe"]["bias_column"] not in binned_data.dims
1147
                ):
1148
                    raise ValueError(
1✔
1149
                        "If binned_data is provided as an xarray, it needs to contain dimensions "
1150
                        f"'{self._config['dataframe']['tof_column']}' and "
1151
                        f"'{self._config['dataframe']['bias_column']}'!.",
1152
                    )
1153
                tof = binned_data.coords[self._config["dataframe"]["tof_column"]].values
1✔
1154
                biases = binned_data.coords[self._config["dataframe"]["bias_column"]].values
1✔
1155
                traces = binned_data.values[:, :]
1✔
1156
            else:
1157
                try:
1✔
1158
                    (tof, biases, traces) = binned_data
1✔
1159
                except ValueError as exc:
1✔
1160
                    raise ValueError(
1✔
1161
                        "If binned_data is provided as tuple, it needs to contain "
1162
                        "(tof, biases, traces)!",
1163
                    ) from exc
1164
            self.ec.load_data(biases=biases, traces=traces, tof=tof)
1✔
1165

1166
        elif data_files is not None:
1✔
1167
            self.ec.bin_data(
1✔
1168
                data_files=cast(List[str], self.cpy(data_files)),
1169
                axes=axes,
1170
                bins=bins,
1171
                ranges=ranges,
1172
                biases=biases,
1173
                bias_key=bias_key,
1174
            )
1175

1176
        else:
1177
            raise ValueError("Either binned_data or data_files needs to be provided!")
1✔
1178

1179
        if (normalize is not None and normalize is True) or (
1✔
1180
            normalize is None and self._config["energy"]["normalize"]
1181
        ):
1182
            if span is None:
1✔
1183
                span = self._config["energy"]["normalize_span"]
1✔
1184
            if order is None:
1✔
1185
                order = self._config["energy"]["normalize_order"]
1✔
1186
            self.ec.normalize(smooth=True, span=span, order=order)
1✔
1187
        self.ec.view(
1✔
1188
            traces=self.ec.traces_normed,
1189
            xaxis=self.ec.tof,
1190
            backend="bokeh",
1191
        )
1192

1193
    # 2. extract ranges and get peak positions
1194
    def find_bias_peaks(
1✔
1195
        self,
1196
        ranges: Union[List[Tuple], Tuple],
1197
        ref_id: int = 0,
1198
        infer_others: bool = True,
1199
        mode: str = "replace",
1200
        radius: int = None,
1201
        peak_window: int = None,
1202
        apply: bool = False,
1203
    ):
1204
        """2. step of the energy calibration workflow: Find a peak within a given range
1205
        for the indicated reference trace, and tries to find the same peak for all
1206
        other traces. Uses fast_dtw to align curves, which might not be too good if the
1207
        shape of curves changes qualitatively. Ideally, choose a reference trace in the
1208
        middle of the set, and don't choose the range too narrow around the peak.
1209
        Alternatively, a list of ranges for all traces can be provided.
1210

1211
        Args:
1212
            ranges (Union[List[Tuple], Tuple]): Tuple of TOF values indicating a range.
1213
                Alternatively, a list of ranges for all traces can be given.
1214
            ref_id (int, optional): The id of the trace the range refers to.
1215
                Defaults to 0.
1216
            infer_others (bool, optional): Whether to determine the range for the other
1217
                traces. Defaults to True.
1218
            mode (str, optional): Whether to "add" or "replace" existing ranges.
1219
                Defaults to "replace".
1220
            radius (int, optional): Radius parameter for fast_dtw.
1221
                Defaults to config["energy"]["fastdtw_radius"].
1222
            peak_window (int, optional): Peak_window parameter for the peak detection
1223
                algorthm. amount of points that have to have to behave monotoneously
1224
                around a peak. Defaults to config["energy"]["peak_window"].
1225
            apply (bool, optional): Option to directly apply the provided parameters.
1226
                Defaults to False.
1227
        """
1228
        if radius is None:
1✔
1229
            radius = self._config["energy"]["fastdtw_radius"]
1✔
1230
        if peak_window is None:
1✔
1231
            peak_window = self._config["energy"]["peak_window"]
1✔
1232
        if not infer_others:
1✔
1233
            self.ec.add_ranges(
1✔
1234
                ranges=ranges,
1235
                ref_id=ref_id,
1236
                infer_others=infer_others,
1237
                mode=mode,
1238
                radius=radius,
1239
            )
1240
            print(self.ec.featranges)
1✔
1241
            try:
1✔
1242
                self.ec.feature_extract(peak_window=peak_window)
1✔
1243
                self.ec.view(
1✔
1244
                    traces=self.ec.traces_normed,
1245
                    segs=self.ec.featranges,
1246
                    xaxis=self.ec.tof,
1247
                    peaks=self.ec.peaks,
1248
                    backend="bokeh",
1249
                )
1250
            except IndexError:
×
1251
                print("Could not determine all peaks!")
×
1252
                raise
×
1253
        else:
1254
            # New adjustment tool
1255
            assert isinstance(ranges, tuple)
1✔
1256
            self.ec.adjust_ranges(
1✔
1257
                ranges=ranges,
1258
                ref_id=ref_id,
1259
                traces=self.ec.traces_normed,
1260
                infer_others=infer_others,
1261
                radius=radius,
1262
                peak_window=peak_window,
1263
                apply=apply,
1264
            )
1265

1266
    # 3. Fit the energy calibration relation
1267
    def calibrate_energy_axis(
1✔
1268
        self,
1269
        ref_id: int,
1270
        ref_energy: float,
1271
        method: str = None,
1272
        energy_scale: str = None,
1273
        verbose: bool = None,
1274
        **kwds,
1275
    ):
1276
        """3. Step of the energy calibration workflow: Calculate the calibration
1277
        function for the energy axis, and apply it to the dataframe. Two
1278
        approximations are implemented, a (normally 3rd order) polynomial
1279
        approximation, and a d^2/(t-t0)^2 relation.
1280

1281
        Args:
1282
            ref_id (int): id of the trace at the bias where the reference energy is
1283
                given.
1284
            ref_energy (float): Absolute energy of the detected feature at the bias
1285
                of ref_id
1286
            method (str, optional): Method for determining the energy calibration.
1287

1288
                - **'lmfit'**: Energy calibration using lmfit and 1/t^2 form.
1289
                - **'lstsq'**, **'lsqr'**: Energy calibration using polynomial form.
1290

1291
                Defaults to config["energy"]["calibration_method"]
1292
            energy_scale (str, optional): Direction of increasing energy scale.
1293

1294
                - **'kinetic'**: increasing energy with decreasing TOF.
1295
                - **'binding'**: increasing energy with increasing TOF.
1296

1297
                Defaults to config["energy"]["energy_scale"]
1298
            verbose (bool, optional): Option to print out diagnostic information.
1299
                Defaults to config["core"]["verbose"].
1300
            **kwds**: Keyword parameters passed to ``EnergyCalibrator.calibrate()``.
1301
        """
1302
        if verbose is None:
1✔
1303
            verbose = self.verbose
1✔
1304

1305
        if method is None:
1✔
1306
            method = self._config["energy"]["calibration_method"]
1✔
1307

1308
        if energy_scale is None:
1✔
1309
            energy_scale = self._config["energy"]["energy_scale"]
1✔
1310

1311
        self.ec.calibrate(
1✔
1312
            ref_id=ref_id,
1313
            ref_energy=ref_energy,
1314
            method=method,
1315
            energy_scale=energy_scale,
1316
            verbose=verbose,
1317
            **kwds,
1318
        )
1319
        if verbose:
1✔
1320
            print("Quality of Calibration:")
1✔
1321
            self.ec.view(
1✔
1322
                traces=self.ec.traces_normed,
1323
                xaxis=self.ec.calibration["axis"],
1324
                align=True,
1325
                energy_scale=energy_scale,
1326
                backend="bokeh",
1327
            )
1328
            print("E/TOF relationship:")
1✔
1329
            self.ec.view(
1✔
1330
                traces=self.ec.calibration["axis"][None, :],
1331
                xaxis=self.ec.tof,
1332
                backend="matplotlib",
1333
                show_legend=False,
1334
            )
1335
            if energy_scale == "kinetic":
1✔
1336
                plt.scatter(
1✔
1337
                    self.ec.peaks[:, 0],
1338
                    -(self.ec.biases - self.ec.biases[ref_id]) + ref_energy,
1339
                    s=50,
1340
                    c="k",
1341
                )
1342
            elif energy_scale == "binding":
1✔
1343
                plt.scatter(
1✔
1344
                    self.ec.peaks[:, 0],
1345
                    self.ec.biases - self.ec.biases[ref_id] + ref_energy,
1346
                    s=50,
1347
                    c="k",
1348
                )
1349
            else:
NEW
1350
                raise ValueError(
×
1351
                    'energy_scale needs to be either "binding" or "kinetic"',
1352
                    f", got {energy_scale}.",
1353
                )
1354
            plt.xlabel("Time-of-flight", fontsize=15)
1✔
1355
            plt.ylabel("Energy (eV)", fontsize=15)
1✔
1356
            plt.show()
1✔
1357

1358
    # 3a. Save energy calibration parameters to config file.
1359
    def save_energy_calibration(
1✔
1360
        self,
1361
        filename: str = None,
1362
        overwrite: bool = False,
1363
    ):
1364
        """Save the generated energy calibration parameters to the folder config file.
1365

1366
        Args:
1367
            filename (str, optional): Filename of the config dictionary to save to.
1368
                Defaults to "sed_config.yaml" in the current folder.
1369
            overwrite (bool, optional): Option to overwrite the present dictionary.
1370
                Defaults to False.
1371
        """
1372
        if filename is None:
1✔
1373
            filename = "sed_config.yaml"
×
1374
        if len(self.ec.calibration) == 0:
1✔
NEW
1375
            raise ValueError("No energy calibration parameters to save!")
×
1376
        calibration = {}
1✔
1377
        for key, value in self.ec.calibration.items():
1✔
1378
            if key in ["axis", "refid", "Tmat", "bvec"]:
1✔
1379
                continue
1✔
1380
            if key == "energy_scale":
1✔
1381
                calibration[key] = value
1✔
1382
            elif key == "coeffs":
1✔
1383
                calibration[key] = [float(i) for i in value]
1✔
1384
            else:
1385
                calibration[key] = float(value)
1✔
1386

1387
        if "creation_date" not in calibration:
1✔
NEW
1388
            calibration["creation_date"] = datetime.now().timestamp()
×
1389

1390
        config = {"energy": {"calibration": calibration}}
1✔
1391
        save_config(config, filename, overwrite)
1✔
1392
        print(f'Saved energy calibration parameters to "{filename}".')
1✔
1393

1394
    # 4. Apply energy calibration to the dataframe
1395
    def append_energy_axis(
1✔
1396
        self,
1397
        calibration: dict = None,
1398
        preview: bool = False,
1399
        verbose: bool = None,
1400
        **kwds,
1401
    ):
1402
        """4. step of the energy calibration workflow: Apply the calibration function
1403
        to to the dataframe. Two approximations are implemented, a (normally 3rd order)
1404
        polynomial approximation, and a d^2/(t-t0)^2 relation. a calibration dictionary
1405
        can be provided.
1406

1407
        Args:
1408
            calibration (dict, optional): Calibration dict containing calibration
1409
                parameters. Overrides calibration from class or config.
1410
                Defaults to None.
1411
            preview (bool): Option to preview the first elements of the data frame.
1412
            verbose (bool, optional): Option to print out diagnostic information.
1413
                Defaults to config["core"]["verbose"].
1414
            **kwds:
1415
                Keyword args passed to ``EnergyCalibrator.append_energy_axis()``.
1416
        """
1417
        if verbose is None:
1✔
1418
            verbose = self.verbose
1✔
1419

1420
        tof_column = self._config["dataframe"]["tof_column"]
1✔
1421

1422
        if self._dataframe is not None:
1✔
1423
            if verbose:
1✔
1424
                print("Adding energy column to dataframe:")
1✔
1425
            df, metadata = self.ec.append_energy_axis(
1✔
1426
                df=self._dataframe,
1427
                calibration=calibration,
1428
                verbose=verbose,
1429
                **kwds,
1430
            )
1431
            if self._timed_dataframe is not None and tof_column in self._timed_dataframe.columns:
1✔
1432
                tdf, _ = self.ec.append_energy_axis(
1✔
1433
                    df=self._timed_dataframe,
1434
                    calibration=calibration,
1435
                    verbose=False,
1436
                    **kwds,
1437
                )
1438

1439
            # Add Metadata
1440
            self._attributes.add(
1✔
1441
                metadata,
1442
                "energy_calibration",
1443
                duplicate_policy="merge",
1444
            )
1445
            self._dataframe = df
1✔
1446
            if self._timed_dataframe is not None and tof_column in self._timed_dataframe.columns:
1✔
1447
                self._timed_dataframe = tdf
1✔
1448

1449
        else:
NEW
1450
            raise ValueError("No dataframe loaded!")
×
1451
        if preview:
1✔
NEW
1452
            print(self._dataframe.head(10))
×
1453
        else:
1454
            if verbose:
1✔
1455
                print(self._dataframe)
1✔
1456

1457
    def add_energy_offset(
1✔
1458
        self,
1459
        constant: float = None,
1460
        columns: Union[str, Sequence[str]] = None,
1461
        weights: Union[float, Sequence[float]] = None,
1462
        reductions: Union[str, Sequence[str]] = None,
1463
        preserve_mean: Union[bool, Sequence[bool]] = None,
1464
        preview: bool = False,
1465
        verbose: bool = None,
1466
    ) -> None:
1467
        """Shift the energy axis of the dataframe by a given amount.
1468

1469
        Args:
1470
            constant (float, optional): The constant to shift the energy axis by.
1471
            columns (Union[str, Sequence[str]]): Name of the column(s) to apply the shift from.
1472
            weights (Union[float, Sequence[float]]): weights to apply to the columns.
1473
                Can also be used to flip the sign (e.g. -1). Defaults to 1.
1474
            preserve_mean (bool): Whether to subtract the mean of the column before applying the
1475
                shift. Defaults to False.
1476
            reductions (str): The reduction to apply to the column. Should be an available method
1477
                of dask.dataframe.Series. For example "mean". In this case the function is applied
1478
                to the column to generate a single value for the whole dataset. If None, the shift
1479
                is applied per-dataframe-row. Defaults to None. Currently only "mean" is supported.
1480
            preview (bool, optional): Option to preview the first elements of the data frame.
1481
                Defaults to False.
1482
            verbose (bool, optional): Option to print out diagnostic information.
1483
                Defaults to config["core"]["verbose"].
1484

1485
        Raises:
1486
            ValueError: If the energy column is not in the dataframe.
1487
        """
1488
        if verbose is None:
1✔
1489
            verbose = self.verbose
1✔
1490

1491
        energy_column = self._config["dataframe"]["energy_column"]
1✔
1492
        if energy_column not in self._dataframe.columns:
1✔
1493
            raise ValueError(
1✔
1494
                f"Energy column {energy_column} not found in dataframe! "
1495
                "Run `append_energy_axis()` first.",
1496
            )
1497
        if self.dataframe is not None:
1✔
1498
            if verbose:
1✔
1499
                print("Adding energy offset to dataframe:")
1✔
1500
            df, metadata = self.ec.add_offsets(
1✔
1501
                df=self._dataframe,
1502
                constant=constant,
1503
                columns=columns,
1504
                energy_column=energy_column,
1505
                weights=weights,
1506
                reductions=reductions,
1507
                preserve_mean=preserve_mean,
1508
                verbose=verbose,
1509
            )
1510
            if self._timed_dataframe is not None and energy_column in self._timed_dataframe.columns:
1✔
1511
                tdf, _ = self.ec.add_offsets(
1✔
1512
                    df=self._timed_dataframe,
1513
                    constant=constant,
1514
                    columns=columns,
1515
                    energy_column=energy_column,
1516
                    weights=weights,
1517
                    reductions=reductions,
1518
                    preserve_mean=preserve_mean,
1519
                )
1520

1521
            self._attributes.add(
1✔
1522
                metadata,
1523
                "add_energy_offset",
1524
                # TODO: allow only appending when no offset along this column(s) was applied
1525
                # TODO: clear memory of modifications if the energy axis is recalculated
1526
                duplicate_policy="append",
1527
            )
1528
            self._dataframe = df
1✔
1529
            if self._timed_dataframe is not None and energy_column in self._timed_dataframe.columns:
1✔
1530
                self._timed_dataframe = tdf
1✔
1531
        else:
1532
            raise ValueError("No dataframe loaded!")
×
1533
        if preview:
1✔
NEW
1534
            print(self._dataframe.head(10))
×
1535
        elif verbose:
1✔
1536
            print(self._dataframe)
1✔
1537

1538
    def save_energy_offset(
1✔
1539
        self,
1540
        filename: str = None,
1541
        overwrite: bool = False,
1542
    ):
1543
        """Save the generated energy calibration parameters to the folder config file.
1544

1545
        Args:
1546
            filename (str, optional): Filename of the config dictionary to save to.
1547
                Defaults to "sed_config.yaml" in the current folder.
1548
            overwrite (bool, optional): Option to overwrite the present dictionary.
1549
                Defaults to False.
1550
        """
1551
        if filename is None:
×
1552
            filename = "sed_config.yaml"
×
1553
        if len(self.ec.offsets) == 0:
×
1554
            raise ValueError("No energy offset parameters to save!")
×
1555

NEW
1556
        if "creation_date" not in self.ec.offsets.keys():
×
NEW
1557
            self.ec.offsets["creation_date"] = datetime.now().timestamp()
×
1558

1559
        config = {"energy": {"offsets": self.ec.offsets}}
×
1560
        save_config(config, filename, overwrite)
×
1561
        print(f'Saved energy offset parameters to "{filename}".')
×
1562

1563
    def append_tof_ns_axis(
1✔
1564
        self,
1565
        preview: bool = False,
1566
        verbose: bool = None,
1567
        **kwds,
1568
    ):
1569
        """Convert time-of-flight channel steps to nanoseconds.
1570

1571
        Args:
1572
            tof_ns_column (str, optional): Name of the generated column containing the
1573
                time-of-flight in nanosecond.
1574
                Defaults to config["dataframe"]["tof_ns_column"].
1575
            preview (bool, optional): Option to preview the first elements of the data frame.
1576
                Defaults to False.
1577
            verbose (bool, optional): Option to print out diagnostic information.
1578
                Defaults to config["core"]["verbose"].
1579
            **kwds: additional arguments are passed to ``EnergyCalibrator.tof_step_to_ns()``.
1580

1581
        """
1582
        if verbose is None:
1✔
1583
            verbose = self.verbose
1✔
1584

1585
        tof_column = self._config["dataframe"]["tof_column"]
1✔
1586

1587
        if self._dataframe is not None:
1✔
1588
            if verbose:
1✔
1589
                print("Adding time-of-flight column in nanoseconds to dataframe:")
1✔
1590
            # TODO assert order of execution through metadata
1591

1592
            df, metadata = self.ec.append_tof_ns_axis(
1✔
1593
                df=self._dataframe,
1594
                **kwds,
1595
            )
1596
            if self._timed_dataframe is not None and tof_column in self._timed_dataframe.columns:
1✔
1597
                tdf, _ = self.ec.append_tof_ns_axis(
1✔
1598
                    df=self._timed_dataframe,
1599
                    **kwds,
1600
                )
1601

1602
            self._attributes.add(
1✔
1603
                metadata,
1604
                "tof_ns_conversion",
1605
                duplicate_policy="overwrite",
1606
            )
1607
            self._dataframe = df
1✔
1608
            if self._timed_dataframe is not None and tof_column in self._timed_dataframe.columns:
1✔
1609
                self._timed_dataframe = tdf
1✔
1610
        else:
NEW
1611
            raise ValueError("No dataframe loaded!")
×
1612
        if preview:
1✔
NEW
1613
            print(self._dataframe.head(10))
×
1614
        else:
1615
            if verbose:
1✔
1616
                print(self._dataframe)
1✔
1617

1618
    def align_dld_sectors(
1✔
1619
        self,
1620
        sector_delays: np.ndarray = None,
1621
        preview: bool = False,
1622
        verbose: bool = None,
1623
        **kwds,
1624
    ):
1625
        """Align the 8s sectors of the HEXTOF endstation.
1626

1627
        Args:
1628
            sector_delays (np.ndarray, optional): Array containing the sector delays. Defaults to
1629
                config["dataframe"]["sector_delays"].
1630
            preview (bool, optional): Option to preview the first elements of the data frame.
1631
                Defaults to False.
1632
            verbose (bool, optional): Option to print out diagnostic information.
1633
                Defaults to config["core"]["verbose"].
1634
            **kwds: additional arguments are passed to ``EnergyCalibrator.align_dld_sectors()``.
1635
        """
1636
        if verbose is None:
1✔
1637
            verbose = self.verbose
1✔
1638

1639
        tof_column = self._config["dataframe"]["tof_column"]
1✔
1640

1641
        if self._dataframe is not None:
1✔
1642
            if verbose:
1✔
1643
                print("Aligning 8s sectors of dataframe")
1✔
1644
            # TODO assert order of execution through metadata
1645

1646
            df, metadata = self.ec.align_dld_sectors(
1✔
1647
                df=self._dataframe,
1648
                sector_delays=sector_delays,
1649
                **kwds,
1650
            )
1651
            if self._timed_dataframe is not None and tof_column in self._timed_dataframe.columns:
1✔
NEW
1652
                tdf, _ = self.ec.align_dld_sectors(
×
1653
                    df=self._timed_dataframe,
1654
                    sector_delays=sector_delays,
1655
                    **kwds,
1656
                )
1657

1658
            self._attributes.add(
1✔
1659
                metadata,
1660
                "dld_sector_alignment",
1661
                duplicate_policy="raise",
1662
            )
1663
            self._dataframe = df
1✔
1664
            if self._timed_dataframe is not None and tof_column in self._timed_dataframe.columns:
1✔
NEW
1665
                self._timed_dataframe = tdf
×
1666
        else:
NEW
1667
            raise ValueError("No dataframe loaded!")
×
1668
        if preview:
1✔
NEW
1669
            print(self._dataframe.head(10))
×
1670
        else:
1671
            if verbose:
1✔
1672
                print(self._dataframe)
1✔
1673

1674
    # Delay calibration function
1675
    def calibrate_delay_axis(
1✔
1676
        self,
1677
        delay_range: Tuple[float, float] = None,
1678
        datafile: str = None,
1679
        preview: bool = False,
1680
        verbose: bool = None,
1681
        **kwds,
1682
    ):
1683
        """Append delay column to dataframe. Either provide delay ranges, or read
1684
        them from a file.
1685

1686
        Args:
1687
            delay_range (Tuple[float, float], optional): The scanned delay range in
1688
                picoseconds. Defaults to None.
1689
            datafile (str, optional): The file from which to read the delay ranges.
1690
                Defaults to None.
1691
            preview (bool, optional): Option to preview the first elements of the data frame.
1692
                Defaults to False.
1693
            verbose (bool, optional): Option to print out diagnostic information.
1694
                Defaults to config["core"]["verbose"].
1695
            **kwds: Keyword args passed to ``DelayCalibrator.append_delay_axis``.
1696
        """
1697
        if verbose is None:
1✔
1698
            verbose = self.verbose
1✔
1699

1700
        adc_column = self._config["dataframe"]["adc_column"]
1✔
1701
        if adc_column not in self._dataframe.columns:
1✔
NEW
1702
            raise ValueError(f"ADC column {adc_column} not found in dataframe, cannot calibrate!")
×
1703

1704
        if self._dataframe is not None:
1✔
1705
            if verbose:
1✔
1706
                print("Adding delay column to dataframe:")
1✔
1707

1708
            if datafile is None:
1✔
1709
                if len(self.dc.calibration) == 0:
1✔
1710
                    try:
1✔
1711
                        datafile = self._files[0]
1✔
1712
                    except IndexError:
×
1713
                        print(
×
1714
                            "No datafile available, specify either",
1715
                            " 'datafile' or 'delay_range'",
1716
                        )
1717
                        raise
×
1718

1719
            df, metadata = self.dc.append_delay_axis(
1✔
1720
                self._dataframe,
1721
                delay_range=delay_range,
1722
                datafile=datafile,
1723
                verbose=verbose,
1724
                **kwds,
1725
            )
1726
            if self._timed_dataframe is not None and adc_column in self._timed_dataframe.columns:
1✔
1727
                tdf, _ = self.dc.append_delay_axis(
1✔
1728
                    self._timed_dataframe,
1729
                    delay_range=delay_range,
1730
                    datafile=datafile,
1731
                    verbose=False,
1732
                    **kwds,
1733
                )
1734

1735
            # Add Metadata
1736
            self._attributes.add(
1✔
1737
                metadata,
1738
                "delay_calibration",
1739
                duplicate_policy="overwrite",
1740
            )
1741
            self._dataframe = df
1✔
1742
            if self._timed_dataframe is not None and adc_column in self._timed_dataframe.columns:
1✔
1743
                self._timed_dataframe = tdf
1✔
1744
        else:
NEW
1745
            raise ValueError("No dataframe loaded!")
×
1746
        if preview:
1✔
1747
            print(self._dataframe.head(10))
1✔
1748
        else:
1749
            if self.verbose:
1✔
1750
                print(self._dataframe)
1✔
1751

1752
    def save_delay_calibration(
1✔
1753
        self,
1754
        filename: str = None,
1755
        overwrite: bool = False,
1756
    ) -> None:
1757
        """Save the generated delay calibration parameters to the folder config file.
1758

1759
        Args:
1760
            filename (str, optional): Filename of the config dictionary to save to.
1761
                Defaults to "sed_config.yaml" in the current folder.
1762
            overwrite (bool, optional): Option to overwrite the present dictionary.
1763
                Defaults to False.
1764
        """
1765
        if filename is None:
1✔
1766
            filename = "sed_config.yaml"
×
1767

1768
        if len(self.dc.calibration) == 0:
1✔
NEW
1769
            raise ValueError("No delay calibration parameters to save!")
×
1770
        calibration = {}
1✔
1771
        for key, value in self.dc.calibration.items():
1✔
1772
            if key == "datafile":
1✔
1773
                calibration[key] = value
1✔
1774
            elif key in ["adc_range", "delay_range", "delay_range_mm"]:
1✔
1775
                calibration[key] = [float(i) for i in value]
1✔
1776
            else:
1777
                calibration[key] = float(value)
1✔
1778

1779
        if "creation_date" not in calibration:
1✔
NEW
1780
            calibration["creation_date"] = datetime.now().timestamp()
×
1781

1782
        config = {
1✔
1783
            "delay": {
1784
                "calibration": calibration,
1785
            },
1786
        }
1787
        save_config(config, filename, overwrite)
1✔
1788

1789
    def add_delay_offset(
1✔
1790
        self,
1791
        constant: float = None,
1792
        flip_delay_axis: bool = None,
1793
        columns: Union[str, Sequence[str]] = None,
1794
        weights: Union[float, Sequence[float]] = 1.0,
1795
        reductions: Union[str, Sequence[str]] = None,
1796
        preserve_mean: Union[bool, Sequence[bool]] = False,
1797
        preview: bool = False,
1798
        verbose: bool = None,
1799
    ) -> None:
1800
        """Shift the delay axis of the dataframe by a constant or other columns.
1801

1802
        Args:
1803
            constant (float, optional): The constant to shift the delay axis by.
1804
            flip_delay_axis (bool, optional): Option to reverse the direction of the delay axis.
1805
            columns (Union[str, Sequence[str]]): Name of the column(s) to apply the shift from.
1806
            weights (Union[float, Sequence[float]]): weights to apply to the columns.
1807
                Can also be used to flip the sign (e.g. -1). Defaults to 1.
1808
            preserve_mean (bool): Whether to subtract the mean of the column before applying the
1809
                shift. Defaults to False.
1810
            reductions (str): The reduction to apply to the column. Should be an available method
1811
                of dask.dataframe.Series. For example "mean". In this case the function is applied
1812
                to the column to generate a single value for the whole dataset. If None, the shift
1813
                is applied per-dataframe-row. Defaults to None. Currently only "mean" is supported.
1814
            preview (bool, optional): Option to preview the first elements of the data frame.
1815
                Defaults to False.
1816
            verbose (bool, optional): Option to print out diagnostic information.
1817
                Defaults to config["core"]["verbose"].
1818

1819
        Raises:
1820
            ValueError: If the delay column is not in the dataframe.
1821
        """
1822
        if verbose is None:
1✔
1823
            verbose = self.verbose
1✔
1824

1825
        delay_column = self._config["dataframe"]["delay_column"]
1✔
1826
        if delay_column not in self._dataframe.columns:
1✔
1827
            raise ValueError(f"Delay column {delay_column} not found in dataframe! ")
1✔
1828

1829
        if self.dataframe is not None:
1✔
1830
            if verbose:
1✔
1831
                print("Adding delay offset to dataframe:")
1✔
1832
            df, metadata = self.dc.add_offsets(
1✔
1833
                df=self._dataframe,
1834
                constant=constant,
1835
                flip_delay_axis=flip_delay_axis,
1836
                columns=columns,
1837
                delay_column=delay_column,
1838
                weights=weights,
1839
                reductions=reductions,
1840
                preserve_mean=preserve_mean,
1841
                verbose=verbose,
1842
            )
1843
            if self._timed_dataframe is not None and delay_column in self._timed_dataframe.columns:
1✔
1844
                tdf, _ = self.dc.add_offsets(
1✔
1845
                    df=self._timed_dataframe,
1846
                    constant=constant,
1847
                    flip_delay_axis=flip_delay_axis,
1848
                    columns=columns,
1849
                    delay_column=delay_column,
1850
                    weights=weights,
1851
                    reductions=reductions,
1852
                    preserve_mean=preserve_mean,
1853
                    verbose=False,
1854
                )
1855

1856
            self._attributes.add(
1✔
1857
                metadata,
1858
                "delay_offset",
1859
                duplicate_policy="append",
1860
            )
1861
            self._dataframe = df
1✔
1862
            if self._timed_dataframe is not None and delay_column in self._timed_dataframe.columns:
1✔
1863
                self._timed_dataframe = tdf
1✔
1864
        else:
1865
            raise ValueError("No dataframe loaded!")
×
1866
        if preview:
1✔
1867
            print(self._dataframe.head(10))
1✔
1868
        else:
1869
            if verbose:
1✔
1870
                print(self._dataframe)
1✔
1871

1872
    def save_delay_offsets(
1✔
1873
        self,
1874
        filename: str = None,
1875
        overwrite: bool = False,
1876
    ) -> None:
1877
        """Save the generated delay calibration parameters to the folder config file.
1878

1879
        Args:
1880
            filename (str, optional): Filename of the config dictionary to save to.
1881
                Defaults to "sed_config.yaml" in the current folder.
1882
            overwrite (bool, optional): Option to overwrite the present dictionary.
1883
                Defaults to False.
1884
        """
1885
        if filename is None:
1✔
1886
            filename = "sed_config.yaml"
×
1887
        if len(self.dc.offsets) == 0:
1✔
1888
            raise ValueError("No delay offset parameters to save!")
×
1889

1890
        if "creation_date" not in self.ec.offsets.keys():
1✔
1891
            self.ec.offsets["creation_date"] = datetime.now().timestamp()
1✔
1892

1893
        config = {
1✔
1894
            "delay": {
1895
                "offsets": self.dc.offsets,
1896
            },
1897
        }
1898
        save_config(config, filename, overwrite)
1✔
1899
        print(f'Saved delay offset parameters to "{filename}".')
1✔
1900

1901
    def save_workflow_params(
1✔
1902
        self,
1903
        filename: str = None,
1904
        overwrite: bool = False,
1905
    ) -> None:
1906
        """run all save calibration parameter methods
1907

1908
        Args:
1909
            filename (str, optional): Filename of the config dictionary to save to.
1910
                Defaults to "sed_config.yaml" in the current folder.
1911
            overwrite (bool, optional): Option to overwrite the present dictionary.
1912
                Defaults to False.
1913
        """
1914
        for method in [
×
1915
            self.save_splinewarp,
1916
            self.save_transformations,
1917
            self.save_momentum_calibration,
1918
            self.save_energy_correction,
1919
            self.save_energy_calibration,
1920
            self.save_energy_offset,
1921
            self.save_delay_calibration,
1922
            self.save_delay_offsets,
1923
        ]:
1924
            try:
×
1925
                method(filename, overwrite)
×
1926
            except (ValueError, AttributeError, KeyError):
×
1927
                pass
×
1928

1929
    def add_jitter(
1✔
1930
        self,
1931
        cols: List[str] = None,
1932
        amps: Union[float, Sequence[float]] = None,
1933
        **kwds,
1934
    ):
1935
        """Add jitter to the selected dataframe columns.
1936

1937
        Args:
1938
            cols (List[str], optional): The colums onto which to apply jitter.
1939
                Defaults to config["dataframe"]["jitter_cols"].
1940
            amps (Union[float, Sequence[float]], optional): Amplitude scalings for the
1941
                jittering noise. If one number is given, the same is used for all axes.
1942
                For uniform noise (default) it will cover the interval [-amp, +amp].
1943
                Defaults to config["dataframe"]["jitter_amps"].
1944
            **kwds: additional keyword arguments passed to ``apply_jitter``.
1945
        """
1946
        if cols is None:
1✔
1947
            cols = self._config["dataframe"]["jitter_cols"]
1✔
1948
        for loc, col in enumerate(cols):
1✔
1949
            if col.startswith("@"):
1✔
1950
                cols[loc] = self._config["dataframe"].get(col.strip("@"))
1✔
1951

1952
        if amps is None:
1✔
1953
            amps = self._config["dataframe"]["jitter_amps"]
1✔
1954

1955
        self._dataframe = self._dataframe.map_partitions(
1✔
1956
            apply_jitter,
1957
            cols=cols,
1958
            cols_jittered=cols,
1959
            amps=amps,
1960
            **kwds,
1961
        )
1962
        if self._timed_dataframe is not None:
1✔
1963
            cols_timed = cols.copy()
1✔
1964
            for col in cols:
1✔
1965
                if col not in self._timed_dataframe.columns:
1✔
1966
                    cols_timed.remove(col)
×
1967

1968
            if cols_timed:
1✔
1969
                self._timed_dataframe = self._timed_dataframe.map_partitions(
1✔
1970
                    apply_jitter,
1971
                    cols=cols_timed,
1972
                    cols_jittered=cols_timed,
1973
                )
1974
        metadata = []
1✔
1975
        for col in cols:
1✔
1976
            metadata.append(col)
1✔
1977
        # TODO: allow only appending if columns are not jittered yet
1978
        self._attributes.add(metadata, "jittering", duplicate_policy="append")
1✔
1979

1980
    def add_time_stamped_data(
1✔
1981
        self,
1982
        dest_column: str,
1983
        time_stamps: np.ndarray = None,
1984
        data: np.ndarray = None,
1985
        archiver_channel: str = None,
1986
        **kwds,
1987
    ):
1988
        """Add data in form of timestamp/value pairs to the dataframe using interpolation to the
1989
        timestamps in the dataframe. The time-stamped data can either be provided, or fetched from
1990
        an EPICS archiver instance.
1991

1992
        Args:
1993
            dest_column (str): destination column name
1994
            time_stamps (np.ndarray, optional): Time stamps of the values to add. If omitted,
1995
                time stamps are retrieved from the epics archiver
1996
            data (np.ndarray, optional): Values corresponding at the time stamps in time_stamps.
1997
                If omitted, data are retrieved from the epics archiver.
1998
            archiver_channel (str, optional): EPICS archiver channel from which to retrieve data.
1999
                Either this or data and time_stamps have to be present.
2000
            **kwds: additional keyword arguments passed to ``add_time_stamped_data``.
2001
        """
2002
        time_stamp_column = kwds.pop(
1✔
2003
            "time_stamp_column",
2004
            self._config["dataframe"].get("time_stamp_alias", ""),
2005
        )
2006

2007
        if time_stamps is None and data is None:
1✔
2008
            if archiver_channel is None:
×
2009
                raise ValueError(
×
2010
                    "Either archiver_channel or both time_stamps and data have to be present!",
2011
                )
2012
            if self.loader.__name__ != "mpes":
×
2013
                raise NotImplementedError(
×
2014
                    "This function is currently only implemented for the mpes loader!",
2015
                )
2016
            ts_from, ts_to = cast(MpesLoader, self.loader).get_start_and_end_time()
×
2017
            # get channel data with +-5 seconds safety margin
2018
            time_stamps, data = get_archiver_data(
×
2019
                archiver_url=self._config["metadata"].get("archiver_url", ""),
2020
                archiver_channel=archiver_channel,
2021
                ts_from=ts_from - 5,
2022
                ts_to=ts_to + 5,
2023
            )
2024

2025
        self._dataframe = add_time_stamped_data(
1✔
2026
            self._dataframe,
2027
            time_stamps=time_stamps,
2028
            data=data,
2029
            dest_column=dest_column,
2030
            time_stamp_column=time_stamp_column,
2031
            **kwds,
2032
        )
2033
        if self._timed_dataframe is not None:
1✔
2034
            if time_stamp_column in self._timed_dataframe:
1✔
2035
                self._timed_dataframe = add_time_stamped_data(
1✔
2036
                    self._timed_dataframe,
2037
                    time_stamps=time_stamps,
2038
                    data=data,
2039
                    dest_column=dest_column,
2040
                    time_stamp_column=time_stamp_column,
2041
                    **kwds,
2042
                )
2043
        metadata: List[Any] = []
1✔
2044
        metadata.append(dest_column)
1✔
2045
        metadata.append(time_stamps)
1✔
2046
        metadata.append(data)
1✔
2047
        self._attributes.add(metadata, "time_stamped_data", duplicate_policy="append")
1✔
2048

2049
    def pre_binning(
1✔
2050
        self,
2051
        df_partitions: Union[int, Sequence[int]] = 100,
2052
        axes: List[str] = None,
2053
        bins: List[int] = None,
2054
        ranges: Sequence[Tuple[float, float]] = None,
2055
        **kwds,
2056
    ) -> xr.DataArray:
2057
        """Function to do an initial binning of the dataframe loaded to the class.
2058

2059
        Args:
2060
            df_partitions (Union[int, Sequence[int]], optional): Number of dataframe partitions to
2061
                use for the initial binning. Defaults to 100.
2062
            axes (List[str], optional): Axes to bin.
2063
                Defaults to config["momentum"]["axes"].
2064
            bins (List[int], optional): Bin numbers to use for binning.
2065
                Defaults to config["momentum"]["bins"].
2066
            ranges (List[Tuple], optional): Ranges to use for binning.
2067
                Defaults to config["momentum"]["ranges"].
2068
            **kwds: Keyword argument passed to ``compute``.
2069

2070
        Returns:
2071
            xr.DataArray: pre-binned data-array.
2072
        """
2073
        if axes is None:
1✔
2074
            axes = self._config["momentum"]["axes"]
1✔
2075
        for loc, axis in enumerate(axes):
1✔
2076
            if axis.startswith("@"):
1✔
2077
                axes[loc] = self._config["dataframe"].get(axis.strip("@"))
1✔
2078

2079
        if bins is None:
1✔
2080
            bins = self._config["momentum"]["bins"]
1✔
2081
        if ranges is None:
1✔
2082
            ranges_ = list(self._config["momentum"]["ranges"])
1✔
2083
            ranges_[2] = np.asarray(ranges_[2]) / 2 ** (
1✔
2084
                self._config["dataframe"]["tof_binning"] - 1
2085
            )
2086
            ranges = [cast(Tuple[float, float], tuple(v)) for v in ranges_]
1✔
2087

2088
        assert self._dataframe is not None, "dataframe needs to be loaded first!"
1✔
2089

2090
        return self.compute(
1✔
2091
            bins=bins,
2092
            axes=axes,
2093
            ranges=ranges,
2094
            df_partitions=df_partitions,
2095
            **kwds,
2096
        )
2097

2098
    def compute(
1✔
2099
        self,
2100
        bins: Union[
2101
            int,
2102
            dict,
2103
            tuple,
2104
            List[int],
2105
            List[np.ndarray],
2106
            List[tuple],
2107
        ] = 100,
2108
        axes: Union[str, Sequence[str]] = None,
2109
        ranges: Sequence[Tuple[float, float]] = None,
2110
        normalize_to_acquisition_time: Union[bool, str] = False,
2111
        **kwds,
2112
    ) -> xr.DataArray:
2113
        """Compute the histogram along the given dimensions.
2114

2115
        Args:
2116
            bins (int, dict, tuple, List[int], List[np.ndarray], List[tuple], optional):
2117
                Definition of the bins. Can be any of the following cases:
2118

2119
                - an integer describing the number of bins in on all dimensions
2120
                - a tuple of 3 numbers describing start, end and step of the binning
2121
                  range
2122
                - a np.arrays defining the binning edges
2123
                - a list (NOT a tuple) of any of the above (int, tuple or np.ndarray)
2124
                - a dictionary made of the axes as keys and any of the above as values.
2125

2126
                This takes priority over the axes and range arguments. Defaults to 100.
2127
            axes (Union[str, Sequence[str]], optional): The names of the axes (columns)
2128
                on which to calculate the histogram. The order will be the order of the
2129
                dimensions in the resulting array. Defaults to None.
2130
            ranges (Sequence[Tuple[float, float]], optional): list of tuples containing
2131
                the start and end point of the binning range. Defaults to None.
2132
            normalize_to_acquisition_time (Union[bool, str]): Option to normalize the
2133
                result to the acquistion time. If a "slow" axis was scanned, providing
2134
                the name of the scanned axis will compute and apply the corresponding
2135
                normalization histogram. Defaults to False.
2136
            **kwds: Keyword arguments:
2137

2138
                - **hist_mode**: Histogram calculation method. "numpy" or "numba". See
2139
                  ``bin_dataframe`` for details. Defaults to
2140
                  config["binning"]["hist_mode"].
2141
                - **mode**: Defines how the results from each partition are combined.
2142
                  "fast", "lean" or "legacy". See ``bin_dataframe`` for details.
2143
                  Defaults to config["binning"]["mode"].
2144
                - **pbar**: Option to show the tqdm progress bar. Defaults to
2145
                  config["binning"]["pbar"].
2146
                - **n_cores**: Number of CPU cores to use for parallelization.
2147
                  Defaults to config["binning"]["num_cores"] or N_CPU-1.
2148
                - **threads_per_worker**: Limit the number of threads that
2149
                  multiprocessing can spawn per binning thread. Defaults to
2150
                  config["binning"]["threads_per_worker"].
2151
                - **threadpool_api**: The API to use for multiprocessing. "blas",
2152
                  "openmp" or None. See ``threadpool_limit`` for details. Defaults to
2153
                  config["binning"]["threadpool_API"].
2154
                - **df_partitions**: A sequence of dataframe partitions, or the
2155
                  number of the dataframe partitions to use. Defaults to all partitions.
2156
                - **filter**: A Sequence of Dictionaries with entries "col", "lower_bound",
2157
                  "upper_bound" to apply as filter to the dataframe before binning. The
2158
                  dataframe in the class remains unmodified by this.
2159

2160
                Additional kwds are passed to ``bin_dataframe``.
2161

2162
        Raises:
2163
            AssertError: Rises when no dataframe has been loaded.
2164

2165
        Returns:
2166
            xr.DataArray: The result of the n-dimensional binning represented in an
2167
            xarray object, combining the data with the axes.
2168
        """
2169
        assert self._dataframe is not None, "dataframe needs to be loaded first!"
1✔
2170

2171
        hist_mode = kwds.pop("hist_mode", self._config["binning"]["hist_mode"])
1✔
2172
        mode = kwds.pop("mode", self._config["binning"]["mode"])
1✔
2173
        pbar = kwds.pop("pbar", self._config["binning"]["pbar"])
1✔
2174
        num_cores = kwds.pop("num_cores", self._config["binning"]["num_cores"])
1✔
2175
        threads_per_worker = kwds.pop(
1✔
2176
            "threads_per_worker",
2177
            self._config["binning"]["threads_per_worker"],
2178
        )
2179
        threadpool_api = kwds.pop(
1✔
2180
            "threadpool_API",
2181
            self._config["binning"]["threadpool_API"],
2182
        )
2183
        df_partitions: Union[int, Sequence[int]] = kwds.pop("df_partitions", None)
1✔
2184
        if isinstance(df_partitions, int):
1✔
2185
            df_partitions = list(range(0, min(df_partitions, self._dataframe.npartitions)))
1✔
2186
        if df_partitions is not None:
1✔
2187
            dataframe = self._dataframe.partitions[df_partitions]
1✔
2188
        else:
2189
            dataframe = self._dataframe
1✔
2190

2191
        filter_params = kwds.pop("filter", None)
1✔
2192
        if filter_params is not None:
1✔
2193
            try:
1✔
2194
                for param in filter_params:
1✔
2195
                    if "col" not in param:
1✔
2196
                        raise ValueError(
1✔
2197
                            "'col' needs to be defined for each filter entry! ",
2198
                            f"Not present in {param}.",
2199
                        )
2200
                    assert set(param.keys()).issubset({"col", "lower_bound", "upper_bound"})
1✔
2201
                    dataframe = apply_filter(dataframe, **param)
1✔
2202
            except AssertionError as exc:
1✔
2203
                invalid_keys = set(param.keys()) - {"lower_bound", "upper_bound"}
1✔
2204
                raise ValueError(
1✔
2205
                    "Only 'col', 'lower_bound' and 'upper_bound' allowed as filter entries. ",
2206
                    f"Parameters {invalid_keys} not valid in {param}.",
2207
                ) from exc
2208

2209
        self._binned = bin_dataframe(
1✔
2210
            df=dataframe,
2211
            bins=bins,
2212
            axes=axes,
2213
            ranges=ranges,
2214
            hist_mode=hist_mode,
2215
            mode=mode,
2216
            pbar=pbar,
2217
            n_cores=num_cores,
2218
            threads_per_worker=threads_per_worker,
2219
            threadpool_api=threadpool_api,
2220
            **kwds,
2221
        )
2222

2223
        for dim in self._binned.dims:
1✔
2224
            try:
1✔
2225
                self._binned[dim].attrs["unit"] = self._config["dataframe"]["units"][dim]
1✔
2226
            except KeyError:
1✔
2227
                pass
1✔
2228

2229
        self._binned.attrs["units"] = "counts"
1✔
2230
        self._binned.attrs["long_name"] = "photoelectron counts"
1✔
2231
        self._binned.attrs["metadata"] = self._attributes.metadata
1✔
2232

2233
        if normalize_to_acquisition_time:
1✔
2234
            if isinstance(normalize_to_acquisition_time, str):
1✔
2235
                axis = normalize_to_acquisition_time
1✔
2236
                print(
1✔
2237
                    f"Calculate normalization histogram for axis '{axis}'...",
2238
                )
2239
                self._normalization_histogram = self.get_normalization_histogram(
1✔
2240
                    axis=axis,
2241
                    df_partitions=df_partitions,
2242
                )
2243
                # if the axes are named correctly, xarray figures out the normalization correctly
2244
                self._normalized = self._binned / self._normalization_histogram
1✔
2245
                self._attributes.add(
1✔
2246
                    self._normalization_histogram.values,
2247
                    name="normalization_histogram",
2248
                    duplicate_policy="overwrite",
2249
                )
2250
            else:
2251
                acquisition_time = self.loader.get_elapsed_time(
×
2252
                    fids=df_partitions,
2253
                )
2254
                if acquisition_time > 0:
×
2255
                    self._normalized = self._binned / acquisition_time
×
2256
                self._attributes.add(
×
2257
                    acquisition_time,
2258
                    name="normalization_histogram",
2259
                    duplicate_policy="overwrite",
2260
                )
2261

2262
            self._normalized.attrs["units"] = "counts/second"
1✔
2263
            self._normalized.attrs["long_name"] = "photoelectron counts per second"
1✔
2264
            self._normalized.attrs["metadata"] = self._attributes.metadata
1✔
2265

2266
            return self._normalized
1✔
2267

2268
        return self._binned
1✔
2269

2270
    def get_normalization_histogram(
1✔
2271
        self,
2272
        axis: str = "delay",
2273
        use_time_stamps: bool = False,
2274
        **kwds,
2275
    ) -> xr.DataArray:
2276
        """Generates a normalization histogram from the timed dataframe. Optionally,
2277
        use the TimeStamps column instead.
2278

2279
        Args:
2280
            axis (str, optional): The axis for which to compute histogram.
2281
                Defaults to "delay".
2282
            use_time_stamps (bool, optional): Use the TimeStamps column of the
2283
                dataframe, rather than the timed dataframe. Defaults to False.
2284
            **kwds: Keyword arguments:
2285

2286
                - **df_partitions**: A sequence of dataframe partitions, or the
2287
                  number of the dataframe partitions to use. Defaults to all partitions.
2288

2289
        Raises:
2290
            ValueError: Raised if no data are binned.
2291
            ValueError: Raised if 'axis' not in binned coordinates.
2292
            ValueError: Raised if config["dataframe"]["time_stamp_alias"] not found
2293
                in Dataframe.
2294

2295
        Returns:
2296
            xr.DataArray: The computed normalization histogram (in TimeStamp units
2297
            per bin).
2298
        """
2299

2300
        if self._binned is None:
1✔
2301
            raise ValueError("Need to bin data first!")
1✔
2302
        if axis not in self._binned.coords:
1✔
2303
            raise ValueError(f"Axis '{axis}' not found in binned data!")
1✔
2304

2305
        df_partitions: Union[int, Sequence[int]] = kwds.pop("df_partitions", None)
1✔
2306
        if isinstance(df_partitions, int):
1✔
2307
            df_partitions = list(range(0, min(df_partitions, self._dataframe.npartitions)))
1✔
2308
        if use_time_stamps or self._timed_dataframe is None:
1✔
2309
            if df_partitions is not None:
1✔
2310
                self._normalization_histogram = normalization_histogram_from_timestamps(
1✔
2311
                    self._dataframe.partitions[df_partitions],
2312
                    axis,
2313
                    self._binned.coords[axis].values,
2314
                    self._config["dataframe"]["time_stamp_alias"],
2315
                )
2316
            else:
2317
                self._normalization_histogram = normalization_histogram_from_timestamps(
×
2318
                    self._dataframe,
2319
                    axis,
2320
                    self._binned.coords[axis].values,
2321
                    self._config["dataframe"]["time_stamp_alias"],
2322
                )
2323
        else:
2324
            if df_partitions is not None:
1✔
2325
                self._normalization_histogram = normalization_histogram_from_timed_dataframe(
1✔
2326
                    self._timed_dataframe.partitions[df_partitions],
2327
                    axis,
2328
                    self._binned.coords[axis].values,
2329
                    self._config["dataframe"]["timed_dataframe_unit_time"],
2330
                )
2331
            else:
2332
                self._normalization_histogram = normalization_histogram_from_timed_dataframe(
×
2333
                    self._timed_dataframe,
2334
                    axis,
2335
                    self._binned.coords[axis].values,
2336
                    self._config["dataframe"]["timed_dataframe_unit_time"],
2337
                )
2338

2339
        return self._normalization_histogram
1✔
2340

2341
    def view_event_histogram(
1✔
2342
        self,
2343
        dfpid: int,
2344
        ncol: int = 2,
2345
        bins: Sequence[int] = None,
2346
        axes: Sequence[str] = None,
2347
        ranges: Sequence[Tuple[float, float]] = None,
2348
        backend: str = "bokeh",
2349
        legend: bool = True,
2350
        histkwds: dict = None,
2351
        legkwds: dict = None,
2352
        **kwds,
2353
    ):
2354
        """Plot individual histograms of specified dimensions (axes) from a substituent
2355
        dataframe partition.
2356

2357
        Args:
2358
            dfpid (int): Number of the data frame partition to look at.
2359
            ncol (int, optional): Number of columns in the plot grid. Defaults to 2.
2360
            bins (Sequence[int], optional): Number of bins to use for the speicified
2361
                axes. Defaults to config["histogram"]["bins"].
2362
            axes (Sequence[str], optional): Names of the axes to display.
2363
                Defaults to config["histogram"]["axes"].
2364
            ranges (Sequence[Tuple[float, float]], optional): Value ranges of all
2365
                specified axes. Defaults toconfig["histogram"]["ranges"].
2366
            backend (str, optional): Backend of the plotting library
2367
                ('matplotlib' or 'bokeh'). Defaults to "bokeh".
2368
            legend (bool, optional): Option to include a legend in the histogram plots.
2369
                Defaults to True.
2370
            histkwds (dict, optional): Keyword arguments for histograms
2371
                (see ``matplotlib.pyplot.hist()``). Defaults to {}.
2372
            legkwds (dict, optional): Keyword arguments for legend
2373
                (see ``matplotlib.pyplot.legend()``). Defaults to {}.
2374
            **kwds: Extra keyword arguments passed to
2375
                ``sed.diagnostics.grid_histogram()``.
2376

2377
        Raises:
2378
            TypeError: Raises when the input values are not of the correct type.
2379
        """
2380
        if bins is None:
1✔
2381
            bins = self._config["histogram"]["bins"]
1✔
2382
        if axes is None:
1✔
2383
            axes = self._config["histogram"]["axes"]
1✔
2384
        axes = list(axes)
1✔
2385
        for loc, axis in enumerate(axes):
1✔
2386
            if axis.startswith("@"):
1✔
2387
                axes[loc] = self._config["dataframe"].get(axis.strip("@"))
1✔
2388
        if ranges is None:
1✔
2389
            ranges = list(self._config["histogram"]["ranges"])
1✔
2390
            for loc, axis in enumerate(axes):
1✔
2391
                if axis == self._config["dataframe"]["tof_column"]:
1✔
2392
                    ranges[loc] = np.asarray(ranges[loc]) / 2 ** (
1✔
2393
                        self._config["dataframe"]["tof_binning"] - 1
2394
                    )
2395
                elif axis == self._config["dataframe"]["adc_column"]:
1✔
2396
                    ranges[loc] = np.asarray(ranges[loc]) / 2 ** (
×
2397
                        self._config["dataframe"]["adc_binning"] - 1
2398
                    )
2399

2400
        input_types = map(type, [axes, bins, ranges])
1✔
2401
        allowed_types = [list, tuple]
1✔
2402

2403
        df = self._dataframe
1✔
2404

2405
        if not set(input_types).issubset(allowed_types):
1✔
2406
            raise TypeError(
×
2407
                "Inputs of axes, bins, ranges need to be list or tuple!",
2408
            )
2409

2410
        # Read out the values for the specified groups
2411
        group_dict_dd = {}
1✔
2412
        dfpart = df.get_partition(dfpid)
1✔
2413
        cols = dfpart.columns
1✔
2414
        for ax in axes:
1✔
2415
            group_dict_dd[ax] = dfpart.values[:, cols.get_loc(ax)]
1✔
2416
        group_dict = ddf.compute(group_dict_dd)[0]
1✔
2417

2418
        # Plot multiple histograms in a grid
2419
        grid_histogram(
1✔
2420
            group_dict,
2421
            ncol=ncol,
2422
            rvs=axes,
2423
            rvbins=bins,
2424
            rvranges=ranges,
2425
            backend=backend,
2426
            legend=legend,
2427
            histkwds=histkwds,
2428
            legkwds=legkwds,
2429
            **kwds,
2430
        )
2431

2432
    def save(
1✔
2433
        self,
2434
        faddr: str,
2435
        **kwds,
2436
    ):
2437
        """Saves the binned data to the provided path and filename.
2438

2439
        Args:
2440
            faddr (str): Path and name of the file to write. Its extension determines
2441
                the file type to write. Valid file types are:
2442

2443
                - "*.tiff", "*.tif": Saves a TIFF stack.
2444
                - "*.h5", "*.hdf5": Saves an HDF5 file.
2445
                - "*.nxs", "*.nexus": Saves a NeXus file.
2446

2447
            **kwds: Keyword argumens, which are passed to the writer functions:
2448
                For TIFF writing:
2449

2450
                - **alias_dict**: Dictionary of dimension aliases to use.
2451

2452
                For HDF5 writing:
2453

2454
                - **mode**: hdf5 read/write mode. Defaults to "w".
2455

2456
                For NeXus:
2457

2458
                - **reader**: Name of the nexustools reader to use.
2459
                  Defaults to config["nexus"]["reader"]
2460
                - **definiton**: NeXus application definition to use for saving.
2461
                  Must be supported by the used ``reader``. Defaults to
2462
                  config["nexus"]["definition"]
2463
                - **input_files**: A list of input files to pass to the reader.
2464
                  Defaults to config["nexus"]["input_files"]
2465
                - **eln_data**: An electronic-lab-notebook file in '.yaml' format
2466
                  to add to the list of files to pass to the reader.
2467
        """
2468
        if self._binned is None:
1✔
2469
            raise NameError("Need to bin data first!")
1✔
2470

2471
        if self._normalized is not None:
1✔
2472
            data = self._normalized
×
2473
        else:
2474
            data = self._binned
1✔
2475

2476
        extension = pathlib.Path(faddr).suffix
1✔
2477

2478
        if extension in (".tif", ".tiff"):
1✔
2479
            to_tiff(
1✔
2480
                data=data,
2481
                faddr=faddr,
2482
                **kwds,
2483
            )
2484
        elif extension in (".h5", ".hdf5"):
1✔
2485
            to_h5(
1✔
2486
                data=data,
2487
                faddr=faddr,
2488
                **kwds,
2489
            )
2490
        elif extension in (".nxs", ".nexus"):
1✔
2491
            try:
1✔
2492
                reader = kwds.pop("reader", self._config["nexus"]["reader"])
1✔
2493
                definition = kwds.pop(
1✔
2494
                    "definition",
2495
                    self._config["nexus"]["definition"],
2496
                )
2497
                input_files = kwds.pop(
1✔
2498
                    "input_files",
2499
                    self._config["nexus"]["input_files"],
2500
                )
2501
            except KeyError as exc:
×
2502
                raise ValueError(
×
2503
                    "The nexus reader, definition and input files need to be provide!",
2504
                ) from exc
2505

2506
            if isinstance(input_files, str):
1✔
2507
                input_files = [input_files]
1✔
2508

2509
            if "eln_data" in kwds:
1✔
2510
                input_files.append(kwds.pop("eln_data"))
×
2511

2512
            to_nexus(
1✔
2513
                data=data,
2514
                faddr=faddr,
2515
                reader=reader,
2516
                definition=definition,
2517
                input_files=input_files,
2518
                **kwds,
2519
            )
2520

2521
        else:
2522
            raise NotImplementedError(
1✔
2523
                f"Unrecognized file format: {extension}.",
2524
            )
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