Skip to content

API documentation

reset_config()

Resets the config file.

Source code in QDMpy/__init__.py
79
80
81
82
83
84
def reset_config():
    """
    Resets the config file.
    """
    make_configfile(reset=True)
    LOG.info("Config file reset")

QDM

QDM

The QDM class is a container for all data related to a single QDM measurement.

The QDM class contains the light and laser images as well as an ODMR and Fit instances.

See Also

QDMpy._core.odmr.ODMR QDMpy._core.fit.Fit

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

    The QDM class contains the light and laser images as well as an ODMR and Fit instances.

    See Also:
        QDMpy._core.odmr.ODMR
        QDMpy._core.fit.Fit

    """

    LOG = logging.getLogger(__name__)

    # outliers
    def __init__(
            self,
            odmr_instance: ODMR,
            light: np.ndarray,
            laser: np.ndarray,
            working_directory: Union[str, os.PathLike],
            pixel_size: float = 4e-6,
            model_name: str = 'auto',
    ) -> None:
        """Initialize the QDM object.

        Args:
            odmr_instance: ODMR instance
            light: light image
            laser: laser image
            working_directory: working directory
            pixel_size: pixel size in m
            model_name: model name (Default value = 'auto')
                If 'auto' the model is chosen based on the mean ODMR data.
                See Also: QDMpy._core.models.guess_model_name

        """

        self.LOG.info("Initializing QDM object.")
        self.LOG.info(f'Working directory: "{working_directory}"')
        self.working_directory = Path(working_directory)

        self.LOG.debug("ODMR data format is [polarity, f_range, n_pixels, n_freqs]")
        self.LOG.debug(f"read parameter shape: data: {odmr_instance.data.shape}")
        self.LOG.debug(f"                      scan_dimensions: {odmr_instance.data_shape}")
        self.LOG.debug(f"                      frequencies: {odmr_instance.f_ghz.shape}")
        self.LOG.debug(f"                      n_freqs: {odmr_instance.n_freqs}")

        self.odmr = odmr_instance

        self._outliers = np.ones(self.odmr.data_shape, dtype=bool)

        self.light = light
        self.laser = laser

        self._B111 = None

        self._fit = Fit(data=self.odmr.data,
                        frequencies=self.odmr.f_ghz,
                        model_name=model_name)

        self.pixel_size = pixel_size  # 4 um

        self._check_bin_factor()

    @property
    def outliers(self) -> NDArray:
        """

        Args:

        Returns:
          :return: ndarray of boolean

        """
        return self._outliers

    @property
    def outliers_idx(self) -> NDArray:
        """

        Args:

        Returns:
          Indices are in reference to the binned ODMR data.

          :return: np.array

        """
        return np.where(self.outliers)[0]

    @property
    def outliers_xy(self) -> NDArray:
        """

        Args:

        Returns:
          In reference to the binned ODMR data.

          :return: np.array of shape (n_outlier, 2)

        """
        y, x = self.idx2rc(self.outliers_idx)
        return np.stack([x, y], axis=1)

    @property
    def outlier_pdf(self) -> pd.DataFrame:
        """

        Args:

        Returns:
          :return: pandas.DataFrame

        """
        outlier_pdf = pd.DataFrame(columns=["idx", "x", "y"])
        outlier_pdf["x"] = self.outliers_xy[:, 0]
        outlier_pdf["y"] = self.outliers_xy[:, 1]
        outlier_pdf["idx"] = self.outliers_idx
        return outlier_pdf

    def detect_outliers(self, dtype: str = "width", method: str = "LocalOutlierFactor",
                        **outlier_props: Any) -> np.ndarray:
        """Detect outliers in the ODMR data.

        The outliers are detected using 'method'. The method can be either 'LocalOutlierFactor' or 'IsolationForest'.

        The LocalOutlierFactor is a scikit-learn method.
        The IsolationForest is a scikit-learn method.

        The method arguments can be passed as a keyword argument.

        Args:
          dtype: the data type the method should be used on (Default value = "width")
          method: the outlier detection method (Default value = "LocalOutlierFactor")
          **outlier_props: keyword arguments for the outlier detection method

        Returns: outlier mask [bool] of shape ODMR.data_shape

        """
        outlier_props["n_jobs"] = -1
        d1 = self.get_param("chi2", reshape=False)
        d1 = np.sum(d1, axis=tuple(range(0, d1.ndim - 1)))

        if dtype in self.fit.model_params + self.fit.model_params_unique:
            d2 = self.get_param(dtype, reshape=False)
        else:
            raise ValueError(f"dtype {dtype} not recognized")

        d2 = np.sum(d2, axis=tuple(range(0, d2.ndim - 1)))
        data = np.stack([d1, d2], axis=0)

        outlier_props["contamination"] = outlier_props.pop("contamination", 0.05)

        if method == "LocalOutlierFactor":
            clf = LocalOutlierFactor(**outlier_props)
        elif method == "IsolationForest":
            outlier_props = {
                k: v
                for k, v in outlier_props.items()
                if k
                   in [
                       "n_estimators",
                       "max_samples",
                       "contamination",
                       "max_features",
                       "bootstrap",
                       "n_jobs",
                       "random_state",
                   ]
            }
            clf = IsolationForest(**outlier_props)
        else:
            raise ValueError(f"Method {method} not recognized.")

        shape = data.shape
        self.LOG.debug(f"Detecting outliers in <<{dtype}>> data of shape {shape}")
        outliers = (clf.fit_predict(data.T) + 1) / 2
        # collapse the first dimensions so that the product applies to all except the pixel dimension
        outliers = outliers.reshape(-1, shape[-1])
        outliers = ~np.product(outliers, axis=0).astype(bool)
        self.LOG.info(
            f"Outlier detection using {method} of <<{dtype}>> detected {outliers.sum()} outliers pixels.\n"
            f"                                      Indixes can be accessed using 'outlier_idx' and 'outlier_xy'"
        )
        self.LOG.debug(f"returning {outliers.shape}")
        self._outliers = outliers
        return self._outliers

    def apply_outlier_mask(self, outlier: Union[NDArray, None] = None) -> None:
        """

        Args:
          outlier:  (Default value = None)

        Returns:

        """
        if outlier is None:
            outlier = self.outliers

        self.LOG.debug(f"Applying outlier mask of shape {outlier.shape}")
        self.odmr.apply_outlier_mask(outlier)

    # binning
    @property
    def bin_factor(self) -> int:
        """mirrors the bin_factor of the ODMR instance"""
        return self.odmr.bin_factor

    def bin_data(self, bin_factor: int) -> None:
        """Bin the data.

        Args:
          bin_factor: return:

        Returns:

        """
        if bin_factor == self.bin_factor:
            return
        self.odmr.bin_data(bin_factor=bin_factor)
        self._fit.data = self.odmr.data
        if self._fit.fitted:
            self.LOG.info("Binning changed, fits need to be recalculated!")
            self._fit._reset_fit()

    def _check_bin_factor(self) -> None:
        """check if the bin factor is correct

        If there was some form pf "prebinning" in the ODMR data, the initial bin factor is not 1.
        Therefore, we add a pre_binning factor.
        """
        bin_factors = self.light.shape / self.odmr.data_shape

        if np.all(self.odmr._img_shape != self.light.shape):
            self.LOG.warning(
                f"Scan dimensions of ODMR ({self.odmr._img_shape}) and LED ({self.light.shape}) are not equal. Setting pre_binfactor to {bin_factors[0]}."
            )
            # set the true bin factor
            self.odmr._pre_bin_factor = bin_factors[0]
            self.odmr._img_shape = np.array(self.light.shape)

    # global fluorescence related functions
    def correct_glob_fluorescence(self, glob_fluo: float) -> None:
        """Corrects the global fluorescence.

        Args:
          glob_fluo: global fluorescence correction factor
        """
        self.LOG.debug(f"Correcting global fluorescence {glob_fluo}.")
        self.odmr.correct_glob_fluorescence(glob_fluo)
        self._fit.data = self.odmr.data

    @property
    def global_factor(self) -> float:
        """Global fluorescence factor used for correction"""
        return self.odmr.global_factor

    # MODEL related
    @property
    def model_names(self) -> str:
        """List of available models"""
        return self.fit.model['func_name']

    def set_model_name(self, model_name: Union[str, int]) -> None:
        """Set the diamond type.

        Args:
          model_name: type of diamond used (int or str) e.g. N15 of 2 as in 2 peaks
        """

        if isinstance(model_name, int):
            # get str of diamond type from number of peaks (e.g. 2 -> ESR15N)
            model_name = models.PEAK_TO_TYPE[model_name]

        if model_name not in models.IMPLEMENTED:
            raise NotImplementedError('diamond type has not been implemented, yet')

        self.LOG.debug(f'Setting model to "{model_name}"')

        if hasattr(self, "_fit") and self._fit is not None:
            self._fit.model_name = models.IMPLEMENTED[self._model_name]['func_name']

    @property
    def data_shape(self) -> NDArray:
        """ """
        return self.odmr.data_shape

    # fitting
    @property
    def fit(self) -> Fit:
        """ """
        return self._fit

    @property
    def fitted(self) -> bool:
        """ """
        return self._fit.fitted

    def set_constraints(
            self,
            param: str,
            vmin: Optional[Union[str, None]] = None,
            vmax: Optional[Union[str, None]] = None,
            bound_type: Optional[Union[str, None]] = None,
    ) -> None:
        """Set the constraints for the fit.

        Args:
          param:
          vmin:  (Default value = None)
          vmax:  (Default value = None)
          bound_type:  (Default value = None)

        Returns:


        """
        self._fit.set_constraints(param, vmin, vmax, bound_type)

    def reset_constraints(self) -> None:
        """Reset the constraints to the default values."""
        self._fit._set_initial_constraints()

    def fit_odmr(self, refit=False) -> None:
        """Fit the data using the current fit type."""
        if not QDMpy.PYGPUFIT_PRESENT:
            self.LOG.error("pygpufit not installed. Skipping fitting.")
            raise ImportError("pygpufit not installed.")
        self._fit.fit_odmr(refit=refit)

    def get_param(self, param: str, reshape: bool = True) -> NDArray:
        """Get the value of a parameter reshaped to the image dimesions.

        Args:
          param:
          reshape:  (Default value = True)

        Returns:

        """
        out = self._fit.get_param(param)

        if reshape:
            out = out.reshape(
                -1,
                self.odmr.n_pol,
                self.odmr.n_frange,
                *self.odmr.data_shape,
            )

        return np.squeeze(out)

    def _reshape_parameter(
            self,
            data: NDArray,
            n_pol: int,
            n_frange: int,
    ) -> NDArray:
        """Reshape data so that all data for a frange are in series (i.e. [low_freq(B+), low_freq(B-)]).
        Input data must have format: [polarity, frange, n_pixel, n_freqs]

        Args:
          data:
          n_pol:
          n_frange:

        Returns:

        """
        out = np.array(data)
        out = np.reshape(out, (n_frange, n_pol, -1, data.shape[-1]))
        out = np.swapaxes(out, 0, 1)  # swap polarity and frange
        return out

    ## from METHODS ##
    @classmethod
    def from_matlab(cls, matlab_files: Union[os.PathLike[Any], str], dialect: str = "QDM.io") -> Any:
        """Loads QDM data from a Matlab file.

        Args:
          matlab_files:
          dialect:  (Default value = "QDM.io")

        Returns:

        """

        match dialect:
            case "QDM.io":
                return cls.from_qdmio(matlab_files)

        raise NotImplementedError(f'Dialect "{dialect}" not implemented.')

    @classmethod
    def from_qdmio(cls, data_folder: Union[os.PathLike[Any], str], model_name: str = "auto") -> Any:
        """Loads QDM data from a Matlab file.

        Args:
          data_folder:
          model_name:  (Default value = None)

        Returns:

        """
        cls.LOG.info(f"Initializing QDMpy object from QDMio data in {data_folder}")
        files = os.listdir(data_folder)
        light_files = [f for f in files if "led" in f.lower()]
        laser_files = [f for f in files if "laser" in f.lower()]
        cls.LOG.info(f"Reading {len(light_files)} led, {len(laser_files)} laser files.")

        try:
            odmr_obj = ODMR.from_qdmio(data_folder=data_folder)
            light = get_image(data_folder, light_files)
            laser = get_image(data_folder, laser_files)
        except WrongFileNumber as e:
            raise CantImportError(f'Cannot import QDM data from "{data_folder}"') from e

        return cls(
            odmr_obj,
            light=light,
            laser=laser,
            model_name=model_name,
            working_directory=data_folder,
        )

    # EXPORT METHODS ###
    def export_qdmio(self, path_to_file: Union[os.PathLike, str, None] = None) -> None:
        """Export the data to a QDM.io file. This is a Matlab file named B111dataToPlot.mat. With the following variables:

        ['negDiff', 'posDiff', 'B111ferro', 'B111para', 'chi2Pos1', 'chi2Pos2', 'chi2Neg1', 'chi2Neg2', 'ledImg',
         'laser', 'pixelAlerts']

        Args:
          path_to_file:  (Default value = None)

        Returns:

        """

        path_to_file = Path(path_to_file) if path_to_file is not None else self.working_directory
        full_folder = path_to_file / f"{self.odmr.bin_factor}x{self.odmr.bin_factor}Binned"
        full_folder.mkdir(parents=True, exist_ok=True)
        data = self._save_data(dialect="QDMio")

        savemat(
            full_folder / "B111dataToPlot.mat",
            data,
        )

    def export_qdmpy(self, path_to_file: Union[os.PathLike, str]) -> None:
        """

        Args:
          path_to_file:

        Returns:

        """
        path_to_file = Path(path_to_file)
        savemat(path_to_file, self._save_data(dialect="QDMpy"))

    # CALCULATIONS ###
    @property
    def delta_resonance(self) -> NDArray:
        """Return the difference between low and high freq. resonance of the fit.

        Args:

        Returns:
          numpy.ndarray: negative difference
          numpy.ndarray: positive difference

        """
        d = np.expand_dims(np.array([-1, 1]), axis=[1, 2])
        resonance = self.get_param("resonance")
        return (resonance[:, 1] - resonance[:, 0]) / 2 / GAMMA * d

    @property
    def b111(self) -> Tuple[np.ndarray, np.ndarray]:
        """ """
        neg_difference, pos_difference = self.delta_resonance
        return (neg_difference + pos_difference) / 2, (neg_difference - pos_difference) / 2

    @property
    def b111_remanent(self) -> np.ndarray:
        """

        Args:

        Returns:
          :return: numpy.ndarray

        """
        return self.b111[0]

    @property
    def b111_induced(self) -> np.ndarray:
        """

        Args:

        Returns:
          :return: numpy.ndarray

        """
        return self.b111[1]

    # PLOTTING
    def rc2idx(self, rc: np.ndarray, ref: str = "data") -> NDArray:
        """Convert the xy coordinates to the index of the data.

        If the reference is 'data', the index is relative to the data.
        If the reference is 'img', the index is relative to the LED/laser image.
        Only data -> data and img -> img are supported.

        Args:
          rc: numpy.ndarray [[row], [column]] -> [[y], [x]]
          ref: str 'data' or 'img' (Default value = "data")
          rc:np.ndarray:

        Returns:
          numpy.ndarray [idx]

        """
        if ref == "data":
            shape = self.odmr.data_shape
        elif ref == "img":
            shape = self.odmr.img_shape
        else:
            raise ValueError(f"Reference {ref} not supported.")
        return rc2idx(rc, shape)  # type: ignore[arg-type]

    def idx2rc(
            self, idx: Union[int, np.ndarray], ref: str = "data"
    ) -> Tuple[np.ndarray[Any, Any], np.ndarray[Any, Any]]:
        """Convert an index to a rc coordinate of the reference.

        If the reference is 'data', the index is relative to the data.
        If the reference is 'img', the index is relative to the LED/laser image.
        Only data -> data and img -> img are implemented.

        Args:
          idx: int or numpy.ndarray [idx] or [idx, idx]
          ref: data' or 'img' (Default value = "data")

        Returns:
          numpy.ndarray ([row], [col]) -> [[y], [x]]

        """
        if ref == "data":
            rc = idx2rc(idx, self.data_shape)  # type: ignore[arg-type]
        elif ref == "img":
            rc = idx2rc(idx, self.light.shape)
        else:
            raise ValueError(f"Reference {ref} not supported.")
        return rc

    def _save_data(self, dialect: str = "QDMpy") -> dict:
        """Return the data structure that can be saved to a file.

        Args:
          dialect: str 'QDMpy' or 'QDMio'
          dialect:str:  (Default value = "QDMpy")

        Returns:
          dict

        """

        if dialect == "QDMpy":
            return {
                "remanent": self.b111[0],
                "induced": self.b111[1],
                "chi_squares": self.get_param("chi2"),
                "resonance": self.get_param("resonance"),
                "width": self.get_param("width"),
                "contrast": self.get_param("contrast"),
                "offset": self.get_param("offset"),
                "fit.constraints": self.fit.constraints,
                "diamond_type": self.model_name,
                "laser": self.laser,
                "light": self.light,
                "bin_factor": self.bin_factor,
            }

        elif dialect == "QDMio":
            neg_diff, pos_diff = self.delta_resonance
            b111_remanent, b111_induced = self.b111
            chi_squares = self.get_param("chi2")
            chi2_pos1, chi2_pos2 = chi_squares[0]
            chi2_neg1, chi2_neg2 = chi_squares[1]
            led_img = self.light
            laser_img = self.laser
            pixel_alerts = np.zeros(b111_remanent.shape)

            out = dict(
                negDiff=neg_diff,
                posDiff=pos_diff,
                B111ferro=b111_remanent,
                B111para=b111_induced,
                chi2Pos1=chi2_pos1,
                chi2Pos2=chi2_pos2,
                chi2Neg1=chi2_neg1,
                chi2Neg2=chi2_neg2,
                ledImg=led_img,
                laser=laser_img,
                pixelAlerts=pixel_alerts,
                bin_factor=self.bin_factor,
                QDMpy_version=QDMpy.__version__,
            )
            return out

        else:
            raise ValueError(f"Dialect {dialect} not supported.")

__init__(odmr_instance: ODMR, light: np.ndarray, laser: np.ndarray, working_directory: Union[str, os.PathLike], pixel_size: float = 4e-06, model_name: str = 'auto') -> None

Initialize the QDM object.

Parameters:

Name Type Description Default
odmr_instance ODMR

ODMR instance

required
light np.ndarray

light image

required
laser np.ndarray

laser image

required
working_directory Union[str, os.PathLike]

working directory

required
pixel_size float

pixel size in m

4e-06
model_name str

model name (Default value = 'auto') If 'auto' the model is chosen based on the mean ODMR data. See Also: QDMpy._core.models.guess_model_name

'auto'
Source code in QDMpy/_core/qdm.py
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
def __init__(
        self,
        odmr_instance: ODMR,
        light: np.ndarray,
        laser: np.ndarray,
        working_directory: Union[str, os.PathLike],
        pixel_size: float = 4e-6,
        model_name: str = 'auto',
) -> None:
    """Initialize the QDM object.

    Args:
        odmr_instance: ODMR instance
        light: light image
        laser: laser image
        working_directory: working directory
        pixel_size: pixel size in m
        model_name: model name (Default value = 'auto')
            If 'auto' the model is chosen based on the mean ODMR data.
            See Also: QDMpy._core.models.guess_model_name

    """

    self.LOG.info("Initializing QDM object.")
    self.LOG.info(f'Working directory: "{working_directory}"')
    self.working_directory = Path(working_directory)

    self.LOG.debug("ODMR data format is [polarity, f_range, n_pixels, n_freqs]")
    self.LOG.debug(f"read parameter shape: data: {odmr_instance.data.shape}")
    self.LOG.debug(f"                      scan_dimensions: {odmr_instance.data_shape}")
    self.LOG.debug(f"                      frequencies: {odmr_instance.f_ghz.shape}")
    self.LOG.debug(f"                      n_freqs: {odmr_instance.n_freqs}")

    self.odmr = odmr_instance

    self._outliers = np.ones(self.odmr.data_shape, dtype=bool)

    self.light = light
    self.laser = laser

    self._B111 = None

    self._fit = Fit(data=self.odmr.data,
                    frequencies=self.odmr.f_ghz,
                    model_name=model_name)

    self.pixel_size = pixel_size  # 4 um

    self._check_bin_factor()

apply_outlier_mask(outlier: Union[NDArray, None] = None) -> None

Parameters:

Name Type Description Default
outlier Union[NDArray, None]

(Default value = None)

None
Source code in QDMpy/_core/qdm.py
219
220
221
222
223
224
225
226
227
228
229
230
231
232
def apply_outlier_mask(self, outlier: Union[NDArray, None] = None) -> None:
    """

    Args:
      outlier:  (Default value = None)

    Returns:

    """
    if outlier is None:
        outlier = self.outliers

    self.LOG.debug(f"Applying outlier mask of shape {outlier.shape}")
    self.odmr.apply_outlier_mask(outlier)

b111_induced() -> np.ndarray property

Returns:

Type Description
np.ndarray

return: numpy.ndarray

Source code in QDMpy/_core/qdm.py
527
528
529
530
531
532
533
534
535
536
537
@property
def b111_induced(self) -> np.ndarray:
    """

    Args:

    Returns:
      :return: numpy.ndarray

    """
    return self.b111[1]

b111_remanent() -> np.ndarray property

Returns:

Type Description
np.ndarray

return: numpy.ndarray

Source code in QDMpy/_core/qdm.py
515
516
517
518
519
520
521
522
523
524
525
@property
def b111_remanent(self) -> np.ndarray:
    """

    Args:

    Returns:
      :return: numpy.ndarray

    """
    return self.b111[0]

bin_data(bin_factor: int) -> None

Bin the data.

Parameters:

Name Type Description Default
bin_factor int

return:

required
Source code in QDMpy/_core/qdm.py
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
def bin_data(self, bin_factor: int) -> None:
    """Bin the data.

    Args:
      bin_factor: return:

    Returns:

    """
    if bin_factor == self.bin_factor:
        return
    self.odmr.bin_data(bin_factor=bin_factor)
    self._fit.data = self.odmr.data
    if self._fit.fitted:
        self.LOG.info("Binning changed, fits need to be recalculated!")
        self._fit._reset_fit()

bin_factor() -> int property

mirrors the bin_factor of the ODMR instance

Source code in QDMpy/_core/qdm.py
235
236
237
238
@property
def bin_factor(self) -> int:
    """mirrors the bin_factor of the ODMR instance"""
    return self.odmr.bin_factor

correct_glob_fluorescence(glob_fluo: float) -> None

Corrects the global fluorescence.

Parameters:

Name Type Description Default
glob_fluo float

global fluorescence correction factor

required
Source code in QDMpy/_core/qdm.py
274
275
276
277
278
279
280
281
282
def correct_glob_fluorescence(self, glob_fluo: float) -> None:
    """Corrects the global fluorescence.

    Args:
      glob_fluo: global fluorescence correction factor
    """
    self.LOG.debug(f"Correcting global fluorescence {glob_fluo}.")
    self.odmr.correct_glob_fluorescence(glob_fluo)
    self._fit.data = self.odmr.data

delta_resonance() -> NDArray property

Return the difference between low and high freq. resonance of the fit.

Returns:

Type Description
NDArray

numpy.ndarray: negative difference

NDArray

numpy.ndarray: positive difference

Source code in QDMpy/_core/qdm.py
494
495
496
497
498
499
500
501
502
503
504
505
506
507
@property
def delta_resonance(self) -> NDArray:
    """Return the difference between low and high freq. resonance of the fit.

    Args:

    Returns:
      numpy.ndarray: negative difference
      numpy.ndarray: positive difference

    """
    d = np.expand_dims(np.array([-1, 1]), axis=[1, 2])
    resonance = self.get_param("resonance")
    return (resonance[:, 1] - resonance[:, 0]) / 2 / GAMMA * d

detect_outliers(dtype: str = 'width', method: str = 'LocalOutlierFactor', **outlier_props: Any) -> np.ndarray

Detect outliers in the ODMR data.

The outliers are detected using 'method'. The method can be either 'LocalOutlierFactor' or 'IsolationForest'.

The LocalOutlierFactor is a scikit-learn method. The IsolationForest is a scikit-learn method.

The method arguments can be passed as a keyword argument.

Parameters:

Name Type Description Default
dtype str

the data type the method should be used on (Default value = "width")

'width'
method str

the outlier detection method (Default value = "LocalOutlierFactor")

'LocalOutlierFactor'
**outlier_props Any

keyword arguments for the outlier detection method

{}
Source code in QDMpy/_core/qdm.py
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
def detect_outliers(self, dtype: str = "width", method: str = "LocalOutlierFactor",
                    **outlier_props: Any) -> np.ndarray:
    """Detect outliers in the ODMR data.

    The outliers are detected using 'method'. The method can be either 'LocalOutlierFactor' or 'IsolationForest'.

    The LocalOutlierFactor is a scikit-learn method.
    The IsolationForest is a scikit-learn method.

    The method arguments can be passed as a keyword argument.

    Args:
      dtype: the data type the method should be used on (Default value = "width")
      method: the outlier detection method (Default value = "LocalOutlierFactor")
      **outlier_props: keyword arguments for the outlier detection method

    Returns: outlier mask [bool] of shape ODMR.data_shape

    """
    outlier_props["n_jobs"] = -1
    d1 = self.get_param("chi2", reshape=False)
    d1 = np.sum(d1, axis=tuple(range(0, d1.ndim - 1)))

    if dtype in self.fit.model_params + self.fit.model_params_unique:
        d2 = self.get_param(dtype, reshape=False)
    else:
        raise ValueError(f"dtype {dtype} not recognized")

    d2 = np.sum(d2, axis=tuple(range(0, d2.ndim - 1)))
    data = np.stack([d1, d2], axis=0)

    outlier_props["contamination"] = outlier_props.pop("contamination", 0.05)

    if method == "LocalOutlierFactor":
        clf = LocalOutlierFactor(**outlier_props)
    elif method == "IsolationForest":
        outlier_props = {
            k: v
            for k, v in outlier_props.items()
            if k
               in [
                   "n_estimators",
                   "max_samples",
                   "contamination",
                   "max_features",
                   "bootstrap",
                   "n_jobs",
                   "random_state",
               ]
        }
        clf = IsolationForest(**outlier_props)
    else:
        raise ValueError(f"Method {method} not recognized.")

    shape = data.shape
    self.LOG.debug(f"Detecting outliers in <<{dtype}>> data of shape {shape}")
    outliers = (clf.fit_predict(data.T) + 1) / 2
    # collapse the first dimensions so that the product applies to all except the pixel dimension
    outliers = outliers.reshape(-1, shape[-1])
    outliers = ~np.product(outliers, axis=0).astype(bool)
    self.LOG.info(
        f"Outlier detection using {method} of <<{dtype}>> detected {outliers.sum()} outliers pixels.\n"
        f"                                      Indixes can be accessed using 'outlier_idx' and 'outlier_xy'"
    )
    self.LOG.debug(f"returning {outliers.shape}")
    self._outliers = outliers
    return self._outliers

export_qdmio(path_to_file: Union[os.PathLike, str, None] = None) -> None

Export the data to a QDM.io file. This is a Matlab file named B111dataToPlot.mat. With the following variables:

['negDiff', 'posDiff', 'B111ferro', 'B111para', 'chi2Pos1', 'chi2Pos2', 'chi2Neg1', 'chi2Neg2', 'ledImg', 'laser', 'pixelAlerts']

Parameters:

Name Type Description Default
path_to_file Union[os.PathLike, str, None]

(Default value = None)

None
Source code in QDMpy/_core/qdm.py
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
def export_qdmio(self, path_to_file: Union[os.PathLike, str, None] = None) -> None:
    """Export the data to a QDM.io file. This is a Matlab file named B111dataToPlot.mat. With the following variables:

    ['negDiff', 'posDiff', 'B111ferro', 'B111para', 'chi2Pos1', 'chi2Pos2', 'chi2Neg1', 'chi2Neg2', 'ledImg',
     'laser', 'pixelAlerts']

    Args:
      path_to_file:  (Default value = None)

    Returns:

    """

    path_to_file = Path(path_to_file) if path_to_file is not None else self.working_directory
    full_folder = path_to_file / f"{self.odmr.bin_factor}x{self.odmr.bin_factor}Binned"
    full_folder.mkdir(parents=True, exist_ok=True)
    data = self._save_data(dialect="QDMio")

    savemat(
        full_folder / "B111dataToPlot.mat",
        data,
    )

export_qdmpy(path_to_file: Union[os.PathLike, str]) -> None

Parameters:

Name Type Description Default
path_to_file Union[os.PathLike, str] required
Source code in QDMpy/_core/qdm.py
481
482
483
484
485
486
487
488
489
490
491
def export_qdmpy(self, path_to_file: Union[os.PathLike, str]) -> None:
    """

    Args:
      path_to_file:

    Returns:

    """
    path_to_file = Path(path_to_file)
    savemat(path_to_file, self._save_data(dialect="QDMpy"))

fit_odmr(refit = False) -> None

Fit the data using the current fit type.

Source code in QDMpy/_core/qdm.py
355
356
357
358
359
360
def fit_odmr(self, refit=False) -> None:
    """Fit the data using the current fit type."""
    if not QDMpy.PYGPUFIT_PRESENT:
        self.LOG.error("pygpufit not installed. Skipping fitting.")
        raise ImportError("pygpufit not installed.")
    self._fit.fit_odmr(refit=refit)

from_matlab(matlab_files: Union[os.PathLike[Any], str], dialect: str = 'QDM.io') -> Any classmethod

Loads QDM data from a Matlab file.

Parameters:

Name Type Description Default
matlab_files Union[os.PathLike[Any], str] required
dialect str

(Default value = "QDM.io")

'QDM.io'
Source code in QDMpy/_core/qdm.py
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
@classmethod
def from_matlab(cls, matlab_files: Union[os.PathLike[Any], str], dialect: str = "QDM.io") -> Any:
    """Loads QDM data from a Matlab file.

    Args:
      matlab_files:
      dialect:  (Default value = "QDM.io")

    Returns:

    """

    match dialect:
        case "QDM.io":
            return cls.from_qdmio(matlab_files)

    raise NotImplementedError(f'Dialect "{dialect}" not implemented.')

from_qdmio(data_folder: Union[os.PathLike[Any], str], model_name: str = 'auto') -> Any classmethod

Loads QDM data from a Matlab file.

Parameters:

Name Type Description Default
data_folder Union[os.PathLike[Any], str] required
model_name str

(Default value = None)

'auto'
Source code in QDMpy/_core/qdm.py
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
@classmethod
def from_qdmio(cls, data_folder: Union[os.PathLike[Any], str], model_name: str = "auto") -> Any:
    """Loads QDM data from a Matlab file.

    Args:
      data_folder:
      model_name:  (Default value = None)

    Returns:

    """
    cls.LOG.info(f"Initializing QDMpy object from QDMio data in {data_folder}")
    files = os.listdir(data_folder)
    light_files = [f for f in files if "led" in f.lower()]
    laser_files = [f for f in files if "laser" in f.lower()]
    cls.LOG.info(f"Reading {len(light_files)} led, {len(laser_files)} laser files.")

    try:
        odmr_obj = ODMR.from_qdmio(data_folder=data_folder)
        light = get_image(data_folder, light_files)
        laser = get_image(data_folder, laser_files)
    except WrongFileNumber as e:
        raise CantImportError(f'Cannot import QDM data from "{data_folder}"') from e

    return cls(
        odmr_obj,
        light=light,
        laser=laser,
        model_name=model_name,
        working_directory=data_folder,
    )

get_param(param: str, reshape: bool = True) -> NDArray

Get the value of a parameter reshaped to the image dimesions.

Parameters:

Name Type Description Default
param str required
reshape bool

(Default value = True)

True
Source code in QDMpy/_core/qdm.py
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
def get_param(self, param: str, reshape: bool = True) -> NDArray:
    """Get the value of a parameter reshaped to the image dimesions.

    Args:
      param:
      reshape:  (Default value = True)

    Returns:

    """
    out = self._fit.get_param(param)

    if reshape:
        out = out.reshape(
            -1,
            self.odmr.n_pol,
            self.odmr.n_frange,
            *self.odmr.data_shape,
        )

    return np.squeeze(out)

global_factor() -> float property

Global fluorescence factor used for correction

Source code in QDMpy/_core/qdm.py
284
285
286
287
@property
def global_factor(self) -> float:
    """Global fluorescence factor used for correction"""
    return self.odmr.global_factor

idx2rc(idx: Union[int, np.ndarray], ref: str = 'data') -> Tuple[np.ndarray[Any, Any], np.ndarray[Any, Any]]

Convert an index to a rc coordinate of the reference.

If the reference is 'data', the index is relative to the data. If the reference is 'img', the index is relative to the LED/laser image. Only data -> data and img -> img are implemented.

Parameters:

Name Type Description Default
idx Union[int, np.ndarray]

int or numpy.ndarray [idx] or [idx, idx]

required
ref str

data' or 'img' (Default value = "data")

'data'

Returns:

Type Description
Tuple[np.ndarray[Any, Any], np.ndarray[Any, Any]]

numpy.ndarray ([row], [col]) -> [[y], [x]]

Source code in QDMpy/_core/qdm.py
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
def idx2rc(
        self, idx: Union[int, np.ndarray], ref: str = "data"
) -> Tuple[np.ndarray[Any, Any], np.ndarray[Any, Any]]:
    """Convert an index to a rc coordinate of the reference.

    If the reference is 'data', the index is relative to the data.
    If the reference is 'img', the index is relative to the LED/laser image.
    Only data -> data and img -> img are implemented.

    Args:
      idx: int or numpy.ndarray [idx] or [idx, idx]
      ref: data' or 'img' (Default value = "data")

    Returns:
      numpy.ndarray ([row], [col]) -> [[y], [x]]

    """
    if ref == "data":
        rc = idx2rc(idx, self.data_shape)  # type: ignore[arg-type]
    elif ref == "img":
        rc = idx2rc(idx, self.light.shape)
    else:
        raise ValueError(f"Reference {ref} not supported.")
    return rc

model_names() -> str property

List of available models

Source code in QDMpy/_core/qdm.py
290
291
292
293
@property
def model_names(self) -> str:
    """List of available models"""
    return self.fit.model['func_name']

outlier_pdf() -> pd.DataFrame property

Returns:

Type Description
pd.DataFrame

return: pandas.DataFrame

Source code in QDMpy/_core/qdm.py
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
@property
def outlier_pdf(self) -> pd.DataFrame:
    """

    Args:

    Returns:
      :return: pandas.DataFrame

    """
    outlier_pdf = pd.DataFrame(columns=["idx", "x", "y"])
    outlier_pdf["x"] = self.outliers_xy[:, 0]
    outlier_pdf["y"] = self.outliers_xy[:, 1]
    outlier_pdf["idx"] = self.outliers_idx
    return outlier_pdf

outliers() -> NDArray property

Returns:

Type Description
NDArray

return: ndarray of boolean

Source code in QDMpy/_core/qdm.py
 94
 95
 96
 97
 98
 99
100
101
102
103
104
@property
def outliers(self) -> NDArray:
    """

    Args:

    Returns:
      :return: ndarray of boolean

    """
    return self._outliers

outliers_idx() -> NDArray property

Returns:

Type Description
NDArray

Indices are in reference to the binned ODMR data.

NDArray

return: np.array

Source code in QDMpy/_core/qdm.py
106
107
108
109
110
111
112
113
114
115
116
117
118
@property
def outliers_idx(self) -> NDArray:
    """

    Args:

    Returns:
      Indices are in reference to the binned ODMR data.

      :return: np.array

    """
    return np.where(self.outliers)[0]

outliers_xy() -> NDArray property

Returns:

Type Description
NDArray

In reference to the binned ODMR data.

NDArray

return: np.array of shape (n_outlier, 2)

Source code in QDMpy/_core/qdm.py
120
121
122
123
124
125
126
127
128
129
130
131
132
133
@property
def outliers_xy(self) -> NDArray:
    """

    Args:

    Returns:
      In reference to the binned ODMR data.

      :return: np.array of shape (n_outlier, 2)

    """
    y, x = self.idx2rc(self.outliers_idx)
    return np.stack([x, y], axis=1)

rc2idx(rc: np.ndarray, ref: str = 'data') -> NDArray

Convert the xy coordinates to the index of the data.

If the reference is 'data', the index is relative to the data. If the reference is 'img', the index is relative to the LED/laser image. Only data -> data and img -> img are supported.

Parameters:

Name Type Description Default
rc np.ndarray

numpy.ndarray [[row], [column]] -> [[y], [x]]

required
ref str

str 'data' or 'img' (Default value = "data")

'data'
rc np.ndarray

np.ndarray:

required

Returns:

Type Description
NDArray

numpy.ndarray [idx]

Source code in QDMpy/_core/qdm.py
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
def rc2idx(self, rc: np.ndarray, ref: str = "data") -> NDArray:
    """Convert the xy coordinates to the index of the data.

    If the reference is 'data', the index is relative to the data.
    If the reference is 'img', the index is relative to the LED/laser image.
    Only data -> data and img -> img are supported.

    Args:
      rc: numpy.ndarray [[row], [column]] -> [[y], [x]]
      ref: str 'data' or 'img' (Default value = "data")
      rc:np.ndarray:

    Returns:
      numpy.ndarray [idx]

    """
    if ref == "data":
        shape = self.odmr.data_shape
    elif ref == "img":
        shape = self.odmr.img_shape
    else:
        raise ValueError(f"Reference {ref} not supported.")
    return rc2idx(rc, shape)  # type: ignore[arg-type]

reset_constraints() -> None

Reset the constraints to the default values.

Source code in QDMpy/_core/qdm.py
351
352
353
def reset_constraints(self) -> None:
    """Reset the constraints to the default values."""
    self._fit._set_initial_constraints()

set_constraints(param: str, vmin: Optional[Union[str, None]] = None, vmax: Optional[Union[str, None]] = None, bound_type: Optional[Union[str, None]] = None) -> None

Set the constraints for the fit.

Parameters:

Name Type Description Default
param str required
vmin Optional[Union[str, None]]

(Default value = None)

None
vmax Optional[Union[str, None]]

(Default value = None)

None
bound_type Optional[Union[str, None]]

(Default value = None)

None
Source code in QDMpy/_core/qdm.py
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
def set_constraints(
        self,
        param: str,
        vmin: Optional[Union[str, None]] = None,
        vmax: Optional[Union[str, None]] = None,
        bound_type: Optional[Union[str, None]] = None,
) -> None:
    """Set the constraints for the fit.

    Args:
      param:
      vmin:  (Default value = None)
      vmax:  (Default value = None)
      bound_type:  (Default value = None)

    Returns:


    """
    self._fit.set_constraints(param, vmin, vmax, bound_type)

set_model_name(model_name: Union[str, int]) -> None

Set the diamond type.

Parameters:

Name Type Description Default
model_name Union[str, int]

type of diamond used (int or str) e.g. N15 of 2 as in 2 peaks

required
Source code in QDMpy/_core/qdm.py
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
def set_model_name(self, model_name: Union[str, int]) -> None:
    """Set the diamond type.

    Args:
      model_name: type of diamond used (int or str) e.g. N15 of 2 as in 2 peaks
    """

    if isinstance(model_name, int):
        # get str of diamond type from number of peaks (e.g. 2 -> ESR15N)
        model_name = models.PEAK_TO_TYPE[model_name]

    if model_name not in models.IMPLEMENTED:
        raise NotImplementedError('diamond type has not been implemented, yet')

    self.LOG.debug(f'Setting model to "{model_name}"')

    if hasattr(self, "_fit") and self._fit is not None:
        self._fit.model_name = models.IMPLEMENTED[self._model_name]['func_name']

ODMR

ODMR

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

    LOG = logging.getLogger(__name__)

    def __init__(self, data: NDArray, scan_dimensions: NDArray, frequencies: NDArray, **kwargs: Any) -> None:
        self.LOG.info("ODMR data object initialized")
        self.LOG.debug("ODMR data format is [polarity, f_range, n_pixels, n_freqs]")
        self.LOG.debug(f"read parameter shape: data: {data.shape}")
        self.LOG.debug(f"                      scan_dimensions: {scan_dimensions}")
        self.LOG.debug(f"                      frequencies: {frequencies.shape}")
        self.LOG.debug(f"                      n(freqs): {data.shape[-1]}")

        self._raw_data = data

        self.n_pol = data.shape[0]
        self.n_frange = data.shape[1]
        self._frequencies = frequencies
        self._frequencies_cropped = None

        self.outlier_mask = None
        self._img_shape = np.array(scan_dimensions)

        self._data_edited = np.ones(data.shape)
        self._norm_method = QDMpy.SETTINGS["odmr"]["norm_method"]

        self._edit_stack = [
            self.reset_data,
            self._normalize_data,
            self._apply_outlier_mask,
            None,
            None,
            None,
        ]

        self._apply_edit_stack()
        self._imported_files = kwargs.pop("imported_files", [])
        self._bin_factor = 1
        self._pre_bin_factor = 1  # in case pre binned data is loaded

        self._gf_factor = 0.0

        self.is_binned = False
        self.is_gf_corrected = False  # global fluorescence correction
        self.is_normalized = False
        self.is_cropped = False
        self.is_fcropped = False

    def __repr__(self) -> str:
        return (
            f"ODMR(data={self.data.shape}, "
            f"scan_dimensions={self.data_shape}, n_pol={self.n_pol}, "
            f"n_frange={self.n_frange}, n_pixel={self.n_pixel}, n_freqs={self.n_freqs}"
        )

    def __getitem__(self, item: Union[Sequence[Union[str]], str]) -> NDArray:
        """
        Return the data of a given polarization, frequency range, pixel or frequency.

        Args:
            item: desired return value   (Default value = None)
                  currently available:
                      '+' - positive polarization
                      '-' - negative polarization
                      '<' - lower frequency range
                      '>' - higher frequency range
                      'r' - reshape to 2D image (data_shape)

        Returns: data of the desired return value

        Examples:
        >>> odmr['+'] -> pos. polarization
        >>> odmr['+', '<'] -> pos. polarization + low frequency range

        """
        # todo implement slicing
        # if isinstance(item, slice):
        #     return self.data[item]

        if isinstance(item, int) or all(isinstance(i, int) for i in item):
            raise NotImplementedError("Indexing with only integers is not implemented yet.")

        idx, linear_idx = None, None

        self.LOG.debug(f"get item: {item}")

        items = ",".join(item)

        reshape = bool(re.findall("|".join(["r", "R"]), items))
        # get the data
        d = self.data
        if linear_idx is not None:
            d = d[:, :, linear_idx]
        elif reshape:
            self.LOG.debug("ODMR: reshaping data")
            d = d.reshape(self.n_pol, self.n_frange, self.data_shape[0], self.data_shape[1], self.n_freqs)

        # catch case where only indices are provided
        if len(item) == 0:
            return d

        # return the data
        if re.findall("|".join(["data", "d"]), items):
            return d

        # polarities
        if re.findall("|".join(["pos", re.escape("+")]), items):
            self.LOG.debug("ODMR: selected positive field polarity")
            pidx = [0]
        elif re.findall("|".join(["neg", "-"]), items):
            self.LOG.debug("ODMR: selected negative field polarity")
            pidx = [1]
        else:
            pidx = [0, 1]

        d = d[pidx]
        # franges
        if re.findall("|".join(["low", "l", "<"]), items):
            self.LOG.debug("ODMR: selected low frequency range")
            fidx = [0]
        elif re.findall("|".join(["high", "h", ">"]), items):
            self.LOG.debug("ODMR: selected high frequency range")
            fidx = [1]
        else:
            fidx = [0, 1]

        d = d[:, fidx]
        return np.squeeze(d)

    # index related
    def get_binned_pixel_indices(self, x: int, y: int) -> Tuple[Sequence[int], Sequence[int]]:
        """

        Args:
          x:
          y:

        Returns:
          :return: numpy.ndarray

        """
        idx = list(
            itertools.product(
                np.arange(y * self.bin_factor, (y + 1) * self.bin_factor),
                np.arange(x * self.bin_factor, (x + 1) * self.bin_factor),
            )
        )
        xid = [i[0] for i in idx]
        yid = [i[1] for i in idx]
        return xid, yid

    def rc2idx(self, rc: ArrayLike) -> NDArray:
        """

        Args:
          rc:

        Returns:

        """
        return QDMpy.utils.rc2idx(rc, self.data_shape)  # type: ignore[arg-type]

    def idx2rc(self, idx: ArrayLike) -> Tuple[NDArray, NDArray]:
        """

        Args:
          idx:

        Returns:

        """
        return QDMpy.utils.idx2rc(idx, self.data_shape)  # type: ignore[arg-type]

    def get_most_divergent_from_mean(self) -> Tuple[int, int]:
        """Get the most divergent pixel from the mean in data coordinates."""
        delta = self.delta_mean.copy()
        delta[delta > 0.001] = np.nan
        return np.unravel_index(np.argmax(delta, axis=None), self.delta_mean.shape)  # type: ignore[return-value]

    # from methods

    @classmethod
    def _qdmio_stack_data(cls, mat_dict: dict) -> NDArray:
        """Stack the data in the ODMR object.

        Args:
          mat_dict:

        Returns:

        """
        n_img_stacks = len([k for k in mat_dict.keys() if "imgStack" in k])
        img_stack1, img_stack2 = [], []

        if n_img_stacks == 2:
            # IF ONLY 2 IMG-STACKS, THEN WE ARE IN LOW freq. MODE (50 freq.)
            # imgStack1: [n_freqs, n_pixels] -> transpose to [n_pixels, n_freqs]
            cls.LOG.debug("Two ImgStacks found: Stacking data from imgStack1 and imgStack2.")
            img_stack1 = mat_dict["imgStack1"].T
            img_stack2 = mat_dict["imgStack2"].T
        elif n_img_stacks == 4:
            # 4 IMGSTACKS, THEN WE ARE IN HIGH freq. MODE (101 freqs)
            cls.LOG.debug("Four ImgStacks found: Stacking data from imgStack1, imgStack2 and imgStack3, imgStack4.")
            img_stack1 = np.concatenate([mat_dict["imgStack1"].T, mat_dict["imgStack2"].T])
            img_stack2 = np.concatenate([mat_dict["imgStack3"].T, mat_dict["imgStack4"].T])
        return np.stack((img_stack1, img_stack2), axis=0)

    @classmethod
    def from_qdmio(cls, data_folder: Union[str, os.PathLike]) -> "ODMR":
        """Loads QDM data from a Matlab file.

        Args:
          data_folder:

        Returns:

        """

        files = os.listdir(data_folder)
        run_files = [f for f in files if f.endswith(".mat") and "run_" in f and not f.startswith("#")]

        if not run_files:
            raise WrongFileNumber("No run files found in folder.")

        cls.LOG.info(f"Reading {len(run_files)} run_* files.")

        try:
            raw_data = [loadmat(os.path.join(data_folder, mfile)) for mfile in run_files]
        except NotImplementedError:
            raw_data = [mat73.loadmat(os.path.join(data_folder, mfile)) for mfile in run_files]

        cls.LOG.info(f">> done reading run_* files.")

        data = None

        for mfile in raw_data:
            d = cls._qdmio_stack_data(mfile)
            data = d if data is None else np.stack((data, d), axis=0)

        if data.ndim == 3:  # type: ignore[union-attr]
            data = data[np.newaxis, :, :, :]  # type: ignore[index]
        scan_dimensions = np.array(
            [np.squeeze(raw_data[0]["imgNumRows"]), np.squeeze(raw_data[0]["imgNumCols"])], dtype=int
        )

        scan_dimensions = np.array(
            [
                np.squeeze(raw_data[0]["imgNumRows"]),
                np.squeeze(raw_data[0]["imgNumCols"]),
            ],
            dtype=int,
        )

        n_freqs = int(np.squeeze(raw_data[0]["numFreqs"]))
        frequencies = np.squeeze(raw_data[0]["freqList"]).astype(np.float32)
        if n_freqs != len(frequencies):
            frequencies = np.array([frequencies[:n_freqs], frequencies[n_freqs:]])
        return cls(data=data, scan_dimensions=scan_dimensions, frequencies=frequencies)  # type: ignore[arg-type]

    @classmethod
    def get_norm_factors(cls, data: ArrayLike, method: str = "max") -> np.ndarray:
        """Return the normalization factors for the data.

        Args:
          data: data
          method: return: (Default value = "max")

        Returns:

        Raises: NotImplementedError: if method is not implemented
        """

        match method:
            case "max":
                mx = np.max(data, axis=-1)
                cls.LOG.debug(
                    f"Determining normalization factor from maximum value of each pixel spectrum. "
                    f"Shape of mx: {mx.shape}"
                )
                factors = np.expand_dims(mx, axis=-1)
            case _:
                raise NotImplementedError(f'Method "{method}" not implemented.')

        return factors

    # properties
    @property
    def data_shape(self) -> NDArray:
        """ """
        return (self.img_shape / self.bin_factor).astype(np.uint32)

    @property
    def img_shape(self) -> NDArray:
        """ """
        return self._img_shape

    @property
    def n_pixel(self) -> int:
        """

        Args:

        Returns:
          :return: int

        """
        return int(self.data_shape[0] * self.data_shape[1])

    @property
    def n_freqs(self) -> int:
        """

        Args:

        Returns:
          :return: int

        """
        return self.frequencies.shape[1]

    @property
    def frequencies(self) -> NDArray:
        """

        Args:

        Returns:
          :return: numpy.ndarray

        """
        if self._frequencies_cropped is None:
            return self._frequencies
        else:
            return self._frequencies_cropped

    @property
    def f_hz(self) -> NDArray:
        """Returns the frequencies of the ODMR in Hz."""
        return self.frequencies

    @property
    def f_ghz(self) -> NDArray:
        """Returns the frequencies of the ODMR in GHz."""
        return self.frequencies / 1e9

    @property
    def global_factor(self) -> float:
        """ """
        return self._gf_factor

    @property
    def data(self) -> NDArray:
        """ """
        if self._data_edited is None:
            return np.ascontiguousarray(self._raw_data)
        else:
            return np.ascontiguousarray(self._data_edited)

    @property
    def delta_mean(self) -> NDArray:
        """ """
        return np.sum(np.square(self.data - self.mean_odmr[:, :, np.newaxis, :]), axis=-1)

    @property
    def mean_odmr(self) -> NDArray:
        """Calculate the mean of the data."""
        return self.data.mean(axis=-2)

    @property
    def raw_contrast(self) -> NDArray:
        """Calculate the minimum of MW sweep for each pixel."""
        return np.min(self.data, -2)

    @property
    def mean_contrast(self) -> NDArray:
        """Calculate the mean of the minimum of MW sweep for each pixel."""
        return np.mean(self.raw_contrast)

    @property
    def _mean_baseline(self) -> Tuple[NDArray, NDArray, NDArray]:
        """Calculate the mean baseline of the data."""
        baseline_left_mean = np.mean(self.mean_odmr[:, :, :5], axis=-1)
        baseline_right_mean = np.mean(self.mean_odmr[:, :, -5:], axis=-1)
        baseline_mean = np.mean(np.stack([baseline_left_mean, baseline_right_mean], -1), axis=-1)
        return baseline_left_mean, baseline_right_mean, baseline_mean

    @property
    def bin_factor(self) -> int:
        """ """
        return self._bin_factor * self._pre_bin_factor

    # edit methods

    def _apply_edit_stack(self, **kwargs: Any) -> None:
        """Apply the edit stack.

        Args:
          **kwargs:

        Returns:

        """
        self.LOG.debug("Applying edit stack")
        for edit_func in self._edit_stack:
            if edit_func is not None:
                edit_func(**kwargs)  # type: ignore[operator]

    def reset_data(self, **kwargs: Any) -> None:
        """Reset the data.

        Args:
          **kwargs:

        Returns:

        """
        self.LOG.debug("Resetting data to raw data.")
        self._data_edited = deepcopy(self._raw_data)
        self._norm_factors = None
        self.is_normalized = False
        self.is_binned = False
        self.is_gf_corrected = False
        self.is_cropped = False
        self.is_fcropped = False

    def normalize_data(self, method: Union[str, None] = None, **kwargs: Any) -> None:
        """Normalize the data.

        Args:
          method:  (Default value = None)
          **kwargs:

        Returns:

        """
        if method is None:
            method = self._norm_method
        self._edit_stack[1] = self._normalize_data
        self._apply_edit_stack(method=method)

    def _normalize_data(self, method: str = "max", **kwargs: Any) -> None:
        """Normalize the data.

        Args:
          method:  (Default value = "max")
          **kwargs:

        Returns:

        """
        self._norm_factors = self.get_norm_factors(self.data, method=method)  # type: ignore[assignment]
        self.LOG.debug(f"Normalizing data with method: {method}")
        self._norm_method = method
        self.is_normalized = True
        self._data_edited /= self._norm_factors  # type: ignore[arg-type]

    def apply_outlier_mask(self, outlier_mask: Union[NDArray, None] = None, **kwargs: Any) -> None:  # todo get to work
        """Apply the outlier mask.

        Args:
          outlier_mask: np.ndarray:  (Default value = None)
          **kwargs:

        Returns:

        """
        if outlier_mask is None:
            outlier_mask = self.outlier_mask

        self.outlier_mask = outlier_mask
        self._apply_edit_stack()

    def _apply_outlier_mask(self, **kwargs: Any) -> None:
        """Apply the outlier mask.

        Args:
          **kwargs:

        Returns:

        """
        if self.outlier_mask is None:
            self.LOG.debug("No outlier mask applied.")
            return
        self.LOG.debug("Applying outlier mask")
        self._data_edited[:, :, self.outlier_mask.reshape(-1), :] = np.nan

    def bin_data(self, bin_factor: int, **kwargs: Any) -> None:
        """Bin the data.

        Args:
          bin_factor:
          **kwargs:

        Returns:

        """
        self._edit_stack[3] = self._bin_data
        self._apply_edit_stack(bin_factor=bin_factor)

    def _bin_data(self, bin_factor: Optional[Union[float, None]] = None, **kwargs: Any) -> None:
        """Bin the data from the raw data.

        Args:
          bin_factor:  (Default value = None)
          **kwargs:

        Returns:

        """
        if bin_factor is not None and self._pre_bin_factor:
            bin_factor /= self._pre_bin_factor

        if bin_factor is None:
            bin_factor = self._bin_factor

        # reshape into image size
        reshape_data = self.data.reshape(
            self.n_pol,
            self.n_frange,
            *(self._img_shape / self._pre_bin_factor).astype(int),
            self.n_freqs,
        )  # reshapes the data to the scan dimensions
        _odmr_binned = block_reduce(
            reshape_data,
            block_size=(1, 1, int(bin_factor), int(bin_factor), 1),
            func=np.nanmean,
            cval=np.median(reshape_data),
        )  # bins the data
        self._data_edited = _odmr_binned.reshape(
            self.n_pol, self.n_frange, -1, self.n_freqs
        )  # reshapes the data back to the ODMR data format

        self._bin_factor = int(bin_factor)  # sets the bin factor
        self.is_binned = True  # sets the binned flag

        self.LOG.info(
            f"Binned data from {reshape_data.shape[0]}x{reshape_data.shape[1]}x{reshape_data.shape[2]}x{reshape_data.shape[3]}x{reshape_data.shape[4]} "
            f"--> {_odmr_binned.shape[0]}x{_odmr_binned.shape[1]}x{_odmr_binned.shape[2]}x{_odmr_binned.shape[3]}x{_odmr_binned.shape[4]}"
        )

    def remove_overexposed(self, **kwargs: Any) -> None:
        """Remove overexposed pixels from the data.

        Args:
          **kwargs:

        Returns:

        """
        if self._data_edited is None:
            return self.LOG.warning("No data to remove overexposed pixels from.")

        self._overexposed = np.sum(self._data_edited, axis=-1) == self._data_edited.shape[-1]

        if np.sum(self._overexposed) > 0:
            self.LOG.warning(f"ODMR: {np.sum(self._overexposed)} pixels are overexposed")
            self._data_edited = ma.masked_where(self._data_edited == 1, self._data_edited)

    ### CORRECTION METHODS ###
    def calc_gf_correction(self, gf: float) -> NDArray:
        """Calculate the global fluorescence correction.

        Args:
          gf: The global fluorescence factor

        Returns: The global fluorescence correction
        """
        baseline_left_mean, baseline_right_mean, baseline_mean = self._mean_baseline
        return gf * (self.mean_odmr - baseline_mean[:, :, np.newaxis])

    def correct_glob_fluorescence(self, gf_factor: float, **kwargs: Any) -> None:
        """Correct the data for the gradient factor.

        Args:
          gf_factor:
          **kwargs:

        Returns:

        """
        self._edit_stack[3] = self._correct_glob_fluorescence
        self._gf_factor = gf_factor
        self._apply_edit_stack(glob_fluorescence=gf_factor, **kwargs)

    def _correct_glob_fluorescence(self, gf_factor: Union[float, None] = None, **kwargs: Any) -> None:
        """Correct the data for the global fluorescence.

        Args:
          gf_factor: global fluorescence factor (Default value = None)
          **kwargs: pass though for additional _apply_edit_stack kwargs
        """
        if gf_factor is None:
            gf_factor = self._gf_factor

        self.LOG.debug(f"Correcting for global fluorescence with value {gf_factor}")
        correction = self.calc_gf_correction(gf=gf_factor)

        self._data_edited -= correction[:, :, np.newaxis, :]
        self.is_gf_corrected = True  # sets the gf corrected flag
        self._gf_factor = gf_factor  # sets the gf factor

    # noinspection PyTypeChecker
    def check_glob_fluorescence(self, gf_factor: Union[float, None] = None, idx: Union[int, None] = None) -> None:
        """

        Args:
          gf_factor:  (Default value = None)
          idx:  (Default value = None)

        Returns:

        """
        if idx is None:
            idx = self.get_most_divergent_from_mean()[-1]

        if gf_factor is None:
            gf_factor = self._gf_factor

        new_correct = self.calc_gf_correction(gf=gf_factor)

        f, ax = plt.subplots(2, 2, sharex=False, sharey=True, figsize=(15, 10))
        for p in np.arange(self.n_pol):
            for f in np.arange(self.n_frange):
                d = self.data[p, f, idx].copy()

                old_correct = self.calc_gf_correction(gf=self._gf_factor)
                if self._gf_factor != 0:
                    ax[p, f].plot(self.f_ghz[f], d, "k:", label=f"current: GF={self._gf_factor}")

                (l,) = ax[p, f].plot(
                    self.f_ghz[f],
                    d + old_correct[p, f],
                    ".--",
                    mfc="w",
                    label="original",
                )
                ax[p, f].plot(
                    self.f_ghz[f],
                    d + old_correct[p, f] - new_correct[p, f],
                    ".-",
                    label="corrected",
                    color=l.get_color(),
                )
                ax[p, f].plot(self.f_ghz[f], 1 + new_correct[p, f], "r--", label="correction")
                ax[p, f].set_title(f"{['+', '-'][p]},{['<', '>'][f]}")
                ax[p, f].legend()
                # , ylim=(0, 1.5))
                ax[p, f].set(ylabel="ODMR contrast", xlabel="Frequency [GHz]")

__getitem__(item: Union[Sequence[Union[str]], str]) -> NDArray

Return the data of a given polarization, frequency range, pixel or frequency.

Parameters:

Name Type Description Default
item Union[Sequence[Union[str]], str]

desired return value (Default value = None) currently available: '+' - positive polarization '-' - negative polarization '<' - lower frequency range '>' - higher frequency range 'r' - reshape to 2D image (data_shape)

required

Examples:

odmr['+'] -> pos. polarization odmr['+', '<'] -> pos. polarization + low frequency range

Source code in QDMpy/_core/odmr.py
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
def __getitem__(self, item: Union[Sequence[Union[str]], str]) -> NDArray:
    """
    Return the data of a given polarization, frequency range, pixel or frequency.

    Args:
        item: desired return value   (Default value = None)
              currently available:
                  '+' - positive polarization
                  '-' - negative polarization
                  '<' - lower frequency range
                  '>' - higher frequency range
                  'r' - reshape to 2D image (data_shape)

    Returns: data of the desired return value

    Examples:
    >>> odmr['+'] -> pos. polarization
    >>> odmr['+', '<'] -> pos. polarization + low frequency range

    """
    # todo implement slicing
    # if isinstance(item, slice):
    #     return self.data[item]

    if isinstance(item, int) or all(isinstance(i, int) for i in item):
        raise NotImplementedError("Indexing with only integers is not implemented yet.")

    idx, linear_idx = None, None

    self.LOG.debug(f"get item: {item}")

    items = ",".join(item)

    reshape = bool(re.findall("|".join(["r", "R"]), items))
    # get the data
    d = self.data
    if linear_idx is not None:
        d = d[:, :, linear_idx]
    elif reshape:
        self.LOG.debug("ODMR: reshaping data")
        d = d.reshape(self.n_pol, self.n_frange, self.data_shape[0], self.data_shape[1], self.n_freqs)

    # catch case where only indices are provided
    if len(item) == 0:
        return d

    # return the data
    if re.findall("|".join(["data", "d"]), items):
        return d

    # polarities
    if re.findall("|".join(["pos", re.escape("+")]), items):
        self.LOG.debug("ODMR: selected positive field polarity")
        pidx = [0]
    elif re.findall("|".join(["neg", "-"]), items):
        self.LOG.debug("ODMR: selected negative field polarity")
        pidx = [1]
    else:
        pidx = [0, 1]

    d = d[pidx]
    # franges
    if re.findall("|".join(["low", "l", "<"]), items):
        self.LOG.debug("ODMR: selected low frequency range")
        fidx = [0]
    elif re.findall("|".join(["high", "h", ">"]), items):
        self.LOG.debug("ODMR: selected high frequency range")
        fidx = [1]
    else:
        fidx = [0, 1]

    d = d[:, fidx]
    return np.squeeze(d)

apply_outlier_mask(outlier_mask: Union[NDArray, None] = None, **kwargs: Any) -> None

Apply the outlier mask.

Parameters:

Name Type Description Default
outlier_mask Union[NDArray, None]

np.ndarray: (Default value = None)

None
**kwargs Any {}
Source code in QDMpy/_core/odmr.py
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
def apply_outlier_mask(self, outlier_mask: Union[NDArray, None] = None, **kwargs: Any) -> None:  # todo get to work
    """Apply the outlier mask.

    Args:
      outlier_mask: np.ndarray:  (Default value = None)
      **kwargs:

    Returns:

    """
    if outlier_mask is None:
        outlier_mask = self.outlier_mask

    self.outlier_mask = outlier_mask
    self._apply_edit_stack()

bin_data(bin_factor: int, **kwargs: Any) -> None

Bin the data.

Parameters:

Name Type Description Default
bin_factor int required
**kwargs Any {}
Source code in QDMpy/_core/odmr.py
508
509
510
511
512
513
514
515
516
517
518
519
def bin_data(self, bin_factor: int, **kwargs: Any) -> None:
    """Bin the data.

    Args:
      bin_factor:
      **kwargs:

    Returns:

    """
    self._edit_stack[3] = self._bin_data
    self._apply_edit_stack(bin_factor=bin_factor)

calc_gf_correction(gf: float) -> NDArray

Calculate the global fluorescence correction.

Parameters:

Name Type Description Default
gf float

The global fluorescence factor

required
Source code in QDMpy/_core/odmr.py
581
582
583
584
585
586
587
588
589
590
def calc_gf_correction(self, gf: float) -> NDArray:
    """Calculate the global fluorescence correction.

    Args:
      gf: The global fluorescence factor

    Returns: The global fluorescence correction
    """
    baseline_left_mean, baseline_right_mean, baseline_mean = self._mean_baseline
    return gf * (self.mean_odmr - baseline_mean[:, :, np.newaxis])

check_glob_fluorescence(gf_factor: Union[float, None] = None, idx: Union[int, None] = None) -> None

Parameters:

Name Type Description Default
gf_factor Union[float, None]

(Default value = None)

None
idx Union[int, None]

(Default value = None)

None
Source code in QDMpy/_core/odmr.py
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
def check_glob_fluorescence(self, gf_factor: Union[float, None] = None, idx: Union[int, None] = None) -> None:
    """

    Args:
      gf_factor:  (Default value = None)
      idx:  (Default value = None)

    Returns:

    """
    if idx is None:
        idx = self.get_most_divergent_from_mean()[-1]

    if gf_factor is None:
        gf_factor = self._gf_factor

    new_correct = self.calc_gf_correction(gf=gf_factor)

    f, ax = plt.subplots(2, 2, sharex=False, sharey=True, figsize=(15, 10))
    for p in np.arange(self.n_pol):
        for f in np.arange(self.n_frange):
            d = self.data[p, f, idx].copy()

            old_correct = self.calc_gf_correction(gf=self._gf_factor)
            if self._gf_factor != 0:
                ax[p, f].plot(self.f_ghz[f], d, "k:", label=f"current: GF={self._gf_factor}")

            (l,) = ax[p, f].plot(
                self.f_ghz[f],
                d + old_correct[p, f],
                ".--",
                mfc="w",
                label="original",
            )
            ax[p, f].plot(
                self.f_ghz[f],
                d + old_correct[p, f] - new_correct[p, f],
                ".-",
                label="corrected",
                color=l.get_color(),
            )
            ax[p, f].plot(self.f_ghz[f], 1 + new_correct[p, f], "r--", label="correction")
            ax[p, f].set_title(f"{['+', '-'][p]},{['<', '>'][f]}")
            ax[p, f].legend()
            # , ylim=(0, 1.5))
            ax[p, f].set(ylabel="ODMR contrast", xlabel="Frequency [GHz]")

correct_glob_fluorescence(gf_factor: float, **kwargs: Any) -> None

Correct the data for the gradient factor.

Parameters:

Name Type Description Default
gf_factor float required
**kwargs Any {}
Source code in QDMpy/_core/odmr.py
592
593
594
595
596
597
598
599
600
601
602
603
604
def correct_glob_fluorescence(self, gf_factor: float, **kwargs: Any) -> None:
    """Correct the data for the gradient factor.

    Args:
      gf_factor:
      **kwargs:

    Returns:

    """
    self._edit_stack[3] = self._correct_glob_fluorescence
    self._gf_factor = gf_factor
    self._apply_edit_stack(glob_fluorescence=gf_factor, **kwargs)

f_ghz() -> NDArray property

Returns the frequencies of the ODMR in GHz.

Source code in QDMpy/_core/odmr.py
361
362
363
364
@property
def f_ghz(self) -> NDArray:
    """Returns the frequencies of the ODMR in GHz."""
    return self.frequencies / 1e9

f_hz() -> NDArray property

Returns the frequencies of the ODMR in Hz.

Source code in QDMpy/_core/odmr.py
356
357
358
359
@property
def f_hz(self) -> NDArray:
    """Returns the frequencies of the ODMR in Hz."""
    return self.frequencies

frequencies() -> NDArray property

Returns:

Type Description
NDArray

return: numpy.ndarray

Source code in QDMpy/_core/odmr.py
341
342
343
344
345
346
347
348
349
350
351
352
353
354
@property
def frequencies(self) -> NDArray:
    """

    Args:

    Returns:
      :return: numpy.ndarray

    """
    if self._frequencies_cropped is None:
        return self._frequencies
    else:
        return self._frequencies_cropped

from_qdmio(data_folder: Union[str, os.PathLike]) -> ODMR classmethod

Loads QDM data from a Matlab file.

Parameters:

Name Type Description Default
data_folder Union[str, os.PathLike] required
Source code in QDMpy/_core/odmr.py
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
@classmethod
def from_qdmio(cls, data_folder: Union[str, os.PathLike]) -> "ODMR":
    """Loads QDM data from a Matlab file.

    Args:
      data_folder:

    Returns:

    """

    files = os.listdir(data_folder)
    run_files = [f for f in files if f.endswith(".mat") and "run_" in f and not f.startswith("#")]

    if not run_files:
        raise WrongFileNumber("No run files found in folder.")

    cls.LOG.info(f"Reading {len(run_files)} run_* files.")

    try:
        raw_data = [loadmat(os.path.join(data_folder, mfile)) for mfile in run_files]
    except NotImplementedError:
        raw_data = [mat73.loadmat(os.path.join(data_folder, mfile)) for mfile in run_files]

    cls.LOG.info(f">> done reading run_* files.")

    data = None

    for mfile in raw_data:
        d = cls._qdmio_stack_data(mfile)
        data = d if data is None else np.stack((data, d), axis=0)

    if data.ndim == 3:  # type: ignore[union-attr]
        data = data[np.newaxis, :, :, :]  # type: ignore[index]
    scan_dimensions = np.array(
        [np.squeeze(raw_data[0]["imgNumRows"]), np.squeeze(raw_data[0]["imgNumCols"])], dtype=int
    )

    scan_dimensions = np.array(
        [
            np.squeeze(raw_data[0]["imgNumRows"]),
            np.squeeze(raw_data[0]["imgNumCols"]),
        ],
        dtype=int,
    )

    n_freqs = int(np.squeeze(raw_data[0]["numFreqs"]))
    frequencies = np.squeeze(raw_data[0]["freqList"]).astype(np.float32)
    if n_freqs != len(frequencies):
        frequencies = np.array([frequencies[:n_freqs], frequencies[n_freqs:]])
    return cls(data=data, scan_dimensions=scan_dimensions, frequencies=frequencies)  # type: ignore[arg-type]

get_binned_pixel_indices(x: int, y: int) -> Tuple[Sequence[int], Sequence[int]]

Parameters:

Name Type Description Default
x int required
y int required

Returns:

Type Description
Tuple[Sequence[int], Sequence[int]]

return: numpy.ndarray

Source code in QDMpy/_core/odmr.py
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
def get_binned_pixel_indices(self, x: int, y: int) -> Tuple[Sequence[int], Sequence[int]]:
    """

    Args:
      x:
      y:

    Returns:
      :return: numpy.ndarray

    """
    idx = list(
        itertools.product(
            np.arange(y * self.bin_factor, (y + 1) * self.bin_factor),
            np.arange(x * self.bin_factor, (x + 1) * self.bin_factor),
        )
    )
    xid = [i[0] for i in idx]
    yid = [i[1] for i in idx]
    return xid, yid

get_most_divergent_from_mean() -> Tuple[int, int]

Get the most divergent pixel from the mean in data coordinates.

Source code in QDMpy/_core/odmr.py
194
195
196
197
198
def get_most_divergent_from_mean(self) -> Tuple[int, int]:
    """Get the most divergent pixel from the mean in data coordinates."""
    delta = self.delta_mean.copy()
    delta[delta > 0.001] = np.nan
    return np.unravel_index(np.argmax(delta, axis=None), self.delta_mean.shape)  # type: ignore[return-value]

get_norm_factors(data: ArrayLike, method: str = 'max') -> np.ndarray classmethod

Return the normalization factors for the data.

Parameters:

Name Type Description Default
data ArrayLike

data

required
method str

return: (Default value = "max")

'max'
Source code in QDMpy/_core/odmr.py
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
@classmethod
def get_norm_factors(cls, data: ArrayLike, method: str = "max") -> np.ndarray:
    """Return the normalization factors for the data.

    Args:
      data: data
      method: return: (Default value = "max")

    Returns:

    Raises: NotImplementedError: if method is not implemented
    """

    match method:
        case "max":
            mx = np.max(data, axis=-1)
            cls.LOG.debug(
                f"Determining normalization factor from maximum value of each pixel spectrum. "
                f"Shape of mx: {mx.shape}"
            )
            factors = np.expand_dims(mx, axis=-1)
        case _:
            raise NotImplementedError(f'Method "{method}" not implemented.')

    return factors

idx2rc(idx: ArrayLike) -> Tuple[NDArray, NDArray]

Parameters:

Name Type Description Default
idx ArrayLike required
Source code in QDMpy/_core/odmr.py
183
184
185
186
187
188
189
190
191
192
def idx2rc(self, idx: ArrayLike) -> Tuple[NDArray, NDArray]:
    """

    Args:
      idx:

    Returns:

    """
    return QDMpy.utils.idx2rc(idx, self.data_shape)  # type: ignore[arg-type]

mean_contrast() -> NDArray property

Calculate the mean of the minimum of MW sweep for each pixel.

Source code in QDMpy/_core/odmr.py
394
395
396
397
@property
def mean_contrast(self) -> NDArray:
    """Calculate the mean of the minimum of MW sweep for each pixel."""
    return np.mean(self.raw_contrast)

mean_odmr() -> NDArray property

Calculate the mean of the data.

Source code in QDMpy/_core/odmr.py
384
385
386
387
@property
def mean_odmr(self) -> NDArray:
    """Calculate the mean of the data."""
    return self.data.mean(axis=-2)

n_freqs() -> int property

Returns:

Type Description
int

return: int

Source code in QDMpy/_core/odmr.py
329
330
331
332
333
334
335
336
337
338
339
@property
def n_freqs(self) -> int:
    """

    Args:

    Returns:
      :return: int

    """
    return self.frequencies.shape[1]

n_pixel() -> int property

Returns:

Type Description
int

return: int

Source code in QDMpy/_core/odmr.py
317
318
319
320
321
322
323
324
325
326
327
@property
def n_pixel(self) -> int:
    """

    Args:

    Returns:
      :return: int

    """
    return int(self.data_shape[0] * self.data_shape[1])

normalize_data(method: Union[str, None] = None, **kwargs: Any) -> None

Normalize the data.

Parameters:

Name Type Description Default
method Union[str, None]

(Default value = None)

None
**kwargs Any {}
Source code in QDMpy/_core/odmr.py
446
447
448
449
450
451
452
453
454
455
456
457
458
459
def normalize_data(self, method: Union[str, None] = None, **kwargs: Any) -> None:
    """Normalize the data.

    Args:
      method:  (Default value = None)
      **kwargs:

    Returns:

    """
    if method is None:
        method = self._norm_method
    self._edit_stack[1] = self._normalize_data
    self._apply_edit_stack(method=method)

raw_contrast() -> NDArray property

Calculate the minimum of MW sweep for each pixel.

Source code in QDMpy/_core/odmr.py
389
390
391
392
@property
def raw_contrast(self) -> NDArray:
    """Calculate the minimum of MW sweep for each pixel."""
    return np.min(self.data, -2)

rc2idx(rc: ArrayLike) -> NDArray

Parameters:

Name Type Description Default
rc ArrayLike required
Source code in QDMpy/_core/odmr.py
172
173
174
175
176
177
178
179
180
181
def rc2idx(self, rc: ArrayLike) -> NDArray:
    """

    Args:
      rc:

    Returns:

    """
    return QDMpy.utils.rc2idx(rc, self.data_shape)  # type: ignore[arg-type]

remove_overexposed(**kwargs: Any) -> None

Remove overexposed pixels from the data.

Parameters:

Name Type Description Default
**kwargs Any {}
Source code in QDMpy/_core/odmr.py
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
def remove_overexposed(self, **kwargs: Any) -> None:
    """Remove overexposed pixels from the data.

    Args:
      **kwargs:

    Returns:

    """
    if self._data_edited is None:
        return self.LOG.warning("No data to remove overexposed pixels from.")

    self._overexposed = np.sum(self._data_edited, axis=-1) == self._data_edited.shape[-1]

    if np.sum(self._overexposed) > 0:
        self.LOG.warning(f"ODMR: {np.sum(self._overexposed)} pixels are overexposed")
        self._data_edited = ma.masked_where(self._data_edited == 1, self._data_edited)

reset_data(**kwargs: Any) -> None

Reset the data.

Parameters:

Name Type Description Default
**kwargs Any {}
Source code in QDMpy/_core/odmr.py
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
def reset_data(self, **kwargs: Any) -> None:
    """Reset the data.

    Args:
      **kwargs:

    Returns:

    """
    self.LOG.debug("Resetting data to raw data.")
    self._data_edited = deepcopy(self._raw_data)
    self._norm_factors = None
    self.is_normalized = False
    self.is_binned = False
    self.is_gf_corrected = False
    self.is_cropped = False
    self.is_fcropped = False

Fit

Fit

Source code in QDMpy/_core/fit.py
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
class Fit:
    LOG = logging.getLogger(__name__)

    def __init__(
        self,
        data: NDArray,
        frequencies: NDArray,
        model_name: str = "auto",
        constraints: Optional[Dict[str, Any]] = None,
    ):
        """
        Fit the data to a model.
        Args:
            data: 3D array of the data to fit.
            frequencies: 1D array of the frequencies.
            model_name: Name of the model to fit. (Default value = 'auto')
                if 'auto' the model is guessed from the data.
                See Also: `models.guess_model_name`
            constraints: Constraints for the fit. (Default value = None)
                If None, the default constraints from the config.ini file in QDMpy.CONFIG_PATH are used.
        """

        self._data = data
        self.f_ghz = frequencies
        self.LOG.debug(f"Initializing Fit instance with data: {self.data.shape} at {frequencies.shape} frequencies.")

        if model_name == "auto":
            model_name = self.guess_model_name()
        self.model_name = model_name.upper()
        self._initial_parameter = None

        # fit results
        self._reset_fit()
        self._constraints = (
            self._set_initial_constraints()
        )  # structure is: type: [float(min), float(vmax), str(constraint_type), str(unit)]

        self.estimator_id = ESTIMATOR_ID[QDMpy.SETTINGS["fit"]["estimator"]]  # 0 for LSE, 1 for MLE

    def __repr__(self) -> str:
        return f"Fit(data: {self.data.shape},f: {self.f_ghz.shape}, model:{self.model_name})"

    @property
    def data(self) -> NDArray:
        return self._data

    @data.setter
    def data(self, data: NDArray) -> None:
        if np.all(self._data == data):
            return
        self._data = data
        self._initial_parameter = None
        self._reset_fit()

    ### MODEL RELATED METHODS ###
    @property
    def guess_model_name(self, n_spectra=100, *args, **kwargs) -> str:
        """Guess the model name from the data."""
        data = np.median(self.data, axis=2)
        n_peaks, doubt, peaks = guess_model(data)

        if doubt:
            self.LOG.warning(
                "Doubt on the diamond type. Check using `guess_diamond_type('debug')` " "and set manually if incorrect."
            )

        model = [mdict for m, mdict in models.IMPLEMENTED.items() if mdict["n_peaks"] == n_peaks][0]
        self.LOG.info(f"Guessed diamond type: {n_peaks} peaks -> {model}")
        return model["func_name"]

    @property
    def model_func(self) -> Callable:
        return self._model["func"]

    @property
    def model_params(self) -> dict:
        """
        Return the model parameters.
        """
        return self._model["params"]

    @property
    def model(self) -> dict:
        """
        Return the model dictionary.
        """
        return self._model

    @property
    def model_name(self) -> Callable:
        return self._model["func_name"]

    @model_name.setter
    def model_name(self, model_name: str) -> None:
        if model_name.upper() not in models.IMPLEMENTED:
            raise ValueError(f"Unknown model: {model_name} choose from {list(models.IMPLEMENTED.keys())}")

        self._model = models.IMPLEMENTED[model_name]
        self._constraints = self._set_initial_constraints()

        self.LOG.debug(f"Setting model to {model_name}, resetting all fit results and initial parameters.")
        self._reset_fit()
        self._initial_parameter = self.get_initial_parameter()

    @property
    def model_id(self) -> int:
        return self._model["model_id"]

    @property
    def model_params_unique(self) -> List[str]:
        """
        Return a list of unique fitting parameters.
        :return: list
        """
        lst = []
        for v in self.model_params:
            if self.model_params.count(v) > 1:
                for n in range(10):
                    if f"{v}_{n}" not in lst:
                        lst.append(f"{v}_{n}")
                        break
            else:
                lst.append(v)
        return lst

    @property
    def n_parameter(self) -> int:
        return len(self.model_params)

    ### INITIAL PARAMETER RELATED METHODS ###
    @property
    def initial_parameter(self) -> NDArray:
        """
        Return the initial parameter.
        """
        if self._initial_parameter is None:
            self._initial_parameter = self.get_initial_parameter()
        return self._initial_parameter

    ### COSTRAINTS RELATED METHODS ###
    def _set_initial_constraints(self) -> Dict[str, List[Any]]:
        """
        Get the default constraints dictionary for the fit.
        """
        constraints = QDMpy.SETTINGS["fit"]["constraints"]

        defaults = {}
        for value in self.model_params_unique:
            v = value.split("_")[0]
            defaults[value] = [constraints[f"{v}_min"], constraints[f"{v}_max"], constraints[f"{v}_type"], UNITS[v]]
        return defaults

    def set_constraints(
        self,
        param: str,
        vmin: Union[float, None] = None,
        vmax: Union[float, None] = None,
        constraint_type: Union[str, None] = None,
        reset_fit: bool = True,
    ):
        """
        Set the constraints for the fit.

        :param param: str
            The parameter to set the constraints for.
        :param vmin: float, optional
            The minimum value to set the constraints to. The default is None.
        :param vmax: float, optional
            The maximum value to set the constraints to. The default is None.
        :param constraint_type: str optional
            The bound type to set the constraints to. The default is None.
        :param reset_fit: bool, optional
            Whether to reset the fit results. The default is True.
        """
        if isinstance(constraint_type, int):
            constraint_type = CONSTRAINT_TYPES[constraint_type]

        if constraint_type is not None and constraint_type not in CONSTRAINT_TYPES:
            raise ValueError(f"Unknown constraint type: {constraint_type} choose from {CONSTRAINT_TYPES}")

        if param == "contrast" and self.model_params_unique != self.model_params:
            for contrast in [v for v in self.model_params_unique if "contrast" in v]:
                self.set_constraints(contrast, vmin=vmin, vmax=vmax, constraint_type=constraint_type)
        else:
            self.LOG.debug(f"Setting constraints for {param}: ({vmin}, {vmax}) with {constraint_type}")
            self._constraints[param] = [
                vmin,
                vmax,
                constraint_type,
                UNITS[param.split("_")[0]],
            ]

        if reset_fit:
            self._reset_fit()

    def set_free_constraints(self):
        """
        Set the constraints to be free.
        """
        for param in set(self.model_params_unique):
            self.set_constraints(param, constraint_type="FREE")

    @property
    def constraints(self) -> Dict[str, List[Union[float, str]]]:
        return self._constraints

    # todo not used
    def constraints_changed(self, constraints: List[float], constraint_types: List[str]) -> bool:
        """
        Check if the constraints have changed.
        """
        return list(self._constraints.keys()) != constraints or self._constraint_types != constraint_types

    def get_constraints_array(self, n_pixel: int) -> NDArray:
        """
        Return the constraints as an array (pixel, 2*fitting_parameters).
        :return: np.array
        """
        constraints_list: List[float] = []
        for k in self.model_params_unique:
            constraints_list.extend((self._constraints[k][0], self._constraints[k][1]))
        constraints = np.tile(constraints_list, (n_pixel, 1))
        return constraints

    def get_constraint_types(self) -> NDArray:
        """
        Return the constraint types.
        :return: np.array
        """
        fit_bounds = [CONSTRAINT_TYPES.index(self._constraints[k][2]) for k in self.model_params_unique]
        return np.array(fit_bounds).astype(np.int32)

    # parameters
    @property
    def parameter(self) -> NDArray:
        return self._fit_results

    def get_param(self, param: str) -> Union[NDArray, None]:
        """
        Get the value of a parameter reshaped to the image dimesions.
        """
        if not self.fitted:
            raise NotImplementedError("No fit has been performed yet. Run fit_odmr().")
        if param in ("chi2", "chi_squares", "chi_squared"):
            return self._chi_squares
        idx = self._param_idx(param)
        if param == "mean_contrast":
            return np.mean(self._fit_results[:, :, :, idx], axis=-1)  # type: ignore[index]
        return self._fit_results[:, :, :, idx]  # type: ignore[index]

    def _param_idx(self, parameter: str) -> List[int]:
        """
        Get the index of the fitted parameter.
        :param parameter:
        :return:
        """
        if parameter == "resonance":
            parameter = "center"
        if parameter == "mean_contrast":
            parameter = "contrast"
        idx = [i for i, v in enumerate(self.model_params) if v == parameter]
        if not idx:
            idx = [i for i, v in enumerate(self.model_params_unique) if v == parameter]
        if not idx:
            raise ValueError(f"Unknown parameter: {parameter}")
        return idx

    # initial guess
    def _guess_center(self) -> NDArray:
        """
        Guess the center of the ODMR spectra.
        """
        center = guess_center(self.data, self.f_ghz)
        self.LOG.debug(f"Guessing center frequency [GHz] of ODMR spectra {center.shape}.")
        return center

    def _guess_contrast(self) -> NDArray:
        """
        Guess the contrast of the ODMR spectra.
        """
        contrast = guess_contrast(self.data)
        self.LOG.debug(f"Guessing contrast of ODMR spectra {contrast.shape}.")
        # np.ones((self.n_pol, self.n_frange, self.n_pixel)) * 0.03
        return contrast

    def _guess_width(self) -> NDArray:
        """
        Guess the width of the ODMR spectra.
        """
        width = guess_width(self.data, self.f_ghz, self._model["n_peaks"])
        self.LOG.debug(f"Guessing width of ODMR spectra {width.shape}.")
        return width

    def _guess_offset(self) -> NDArray:
        """
        Guess the offset from 0 of the ODMR spectra. Usually this is 1
        """
        n_pol, nfrange, n_pixel, _ = self.data.shape
        offset = np.zeros((n_pol, nfrange, n_pixel))
        self.LOG.debug(f"Guessing offset {offset.shape}")
        return offset

    def get_initial_parameter(self) -> NDArray:
        """
        Constructs an initial guess for the fit.
        """
        fit_parameter = []

        for p in self.model_params:
            param = getattr(self, f"_guess_{p}")()
            fit_parameter.append(param)

        fit_parameter = np.stack(fit_parameter, axis=fit_parameter[-1].ndim)
        return np.ascontiguousarray(fit_parameter, dtype=np.float32)

    ### fitting related methods ###
    def _reset_fit(self) -> None:
        """Reset the fit results."""
        self._fitted = False
        self._fit_results = None
        self._states = None
        self._chi_squares = None
        self._number_iterations = None
        self._execution_time = None

    @property
    def fitted(self) -> bool:
        return self._fitted

    def fit_odmr(self, refit=False) -> None:
        if self._fitted and not refit:
            self.LOG.debug("Already fitted")
            return
        if self.fitted and refit:
            self._reset_fit()
            self.LOG.debug("Refitting the ODMR data")

        for irange in np.arange(0, self.data.shape[1]):
            self.LOG.info(
                f"Fitting frange {irange} from {self.f_ghz[irange].min():5.3f}-{self.f_ghz[irange].max():5.3f} GHz"
            )

            results = self.fit_frange(
                self.data[:, irange],
                self.f_ghz[irange],
                self.initial_parameter[:, irange],
            )
            results = self.reshape_results(results)

            if self._fit_results is None:
                self._fit_results = results[0]
                self._states = results[1]
                self._chi_squares = results[2]
                self._number_iterations = results[3]
                self._execution_time = results[4]
            else:
                self._fit_results = np.stack((self._fit_results, results[0]))
                self._states = np.stack((self._states, results[1]))
                self._chi_squares = np.stack((self._chi_squares, results[2]))
                self._number_iterations = np.stack((self._number_iterations, results[3]))
                self._execution_time = np.stack((self._execution_time, results[4]))

            self.LOG.info(f"fit finished in {results[4]:.2f} seconds")
        self._fit_results = np.swapaxes(self._fit_results, 0, 1)  # type: ignore[call-overload]
        self._fitted = True

    def fit_frange(self, data: NDArray, freq: NDArray, initial_parameters: NDArray) -> List[NDArray]:
        """
        Wrapper for the fit_constrained function.

        Args:
            data: data for one frequency range, to be fitted. array of size (n_pol, n_pixel, n_freqs) of the ODMR data
            freq: array of size (n_freqs) of the frequencies
            initial_parameters: initial guess for the fit, an array of size (n_pol * n_pixel, 2 * n_param) of
                the initial parameters

        Returns:
            fit_results: results consist of: parameters, states, chi_squares, number_iterations, execution_time
                results: array of size (n_pol*n_pixel, n_param) of the fitted parameters
                states: array of size (n_pol*n_pixel) of the fit states (i.e. did the fit work)
                chi_squares: array of size (n_pol*n_pixel) of the chi squares
                number_iterations: array of size (n_pol*n_pixel) of the number of iterations
                execution_time: execution time
        """
        # reshape the data into a single array with (n_pix*n_pol, n_freqs)
        n_pol, n_pix, n_freqs = data.shape
        data = data.reshape((-1, n_freqs))
        initial_parameters = initial_parameters.reshape((-1, self.n_parameter))
        n_pixel = data.shape[0]
        constraints = self.get_constraints_array(n_pixel)
        constraint_types = self.get_constraint_types()

        results = gf.fit_constrained(
            data=np.ascontiguousarray(data, dtype=np.float32),
            user_info=np.ascontiguousarray(freq, dtype=np.float32),
            constraints=np.ascontiguousarray(constraints, dtype=np.float32),
            constraint_types=constraint_types,
            initial_parameters=np.ascontiguousarray(initial_parameters, dtype=np.float32),
            weights=None,
            model_id=self.model_id,
            max_number_iterations=QDMpy.SETTINGS["fit"]["max_number_iterations"],
            tolerance=QDMpy.SETTINGS["fit"]["tolerance"],
        )

        return list(results)

    def reshape_results(self, results: List[NDArray]) -> NDArray:
        """Reshape the results from the fit_constrained function into the correct shape.

        Args:
            results: results consist of: parameters, states, chi_squares, number_iterations, execution_time

        Returns:
            results: results consist of: parameters, states, chi_squares, number_iterations, execution_time
                results: array of size (n_pol, n_pixel, n_param) of the fitted parameters
                states: array of size (n_pol, n_pixel) of the fit states (i.e. did the fit work)
                chi_squares: array of size (n_pol, n_pixel) of the chi squares
                number_iterations: array of size (n_pol, n_pixel) of the number of iterations
                execution_time: execution time
        """
        for i in range(len(results)):
            if isinstance(results[i], float):
                continue
            results[i] = self.reshape_result(results[i])
        return results

    def reshape_result(self, result: NDArray) -> NDArray:
        """
        Reshape the results to the original shape of (n_pol, npix, -1)

        Args:
            result: array of size (n_pol * n_pixel, -1) of the fitted parameters

        Returns:
            result: array of size (n_pol, n_pixel, -1) of the fitted parameters
        """
        n_pol, npix, _ = self.data[0].shape
        result = result.reshape((n_pol, npix, -1))
        return np.squeeze(result)

__init__(data: NDArray, frequencies: NDArray, model_name: str = 'auto', constraints: Optional[Dict[str, Any]] = None)

Fit the data to a model.

Parameters:

Name Type Description Default
data NDArray

3D array of the data to fit.

required
frequencies NDArray

1D array of the frequencies.

required
model_name str

Name of the model to fit. (Default value = 'auto') if 'auto' the model is guessed from the data. See Also: models.guess_model_name

'auto'
constraints Optional[Dict[str, Any]]

Constraints for the fit. (Default value = None) If None, the default constraints from the config.ini file in QDMpy.CONFIG_PATH are used.

None
Source code in QDMpy/_core/fit.py
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
def __init__(
    self,
    data: NDArray,
    frequencies: NDArray,
    model_name: str = "auto",
    constraints: Optional[Dict[str, Any]] = None,
):
    """
    Fit the data to a model.
    Args:
        data: 3D array of the data to fit.
        frequencies: 1D array of the frequencies.
        model_name: Name of the model to fit. (Default value = 'auto')
            if 'auto' the model is guessed from the data.
            See Also: `models.guess_model_name`
        constraints: Constraints for the fit. (Default value = None)
            If None, the default constraints from the config.ini file in QDMpy.CONFIG_PATH are used.
    """

    self._data = data
    self.f_ghz = frequencies
    self.LOG.debug(f"Initializing Fit instance with data: {self.data.shape} at {frequencies.shape} frequencies.")

    if model_name == "auto":
        model_name = self.guess_model_name()
    self.model_name = model_name.upper()
    self._initial_parameter = None

    # fit results
    self._reset_fit()
    self._constraints = (
        self._set_initial_constraints()
    )  # structure is: type: [float(min), float(vmax), str(constraint_type), str(unit)]

    self.estimator_id = ESTIMATOR_ID[QDMpy.SETTINGS["fit"]["estimator"]]  # 0 for LSE, 1 for MLE

constraints_changed(constraints: List[float], constraint_types: List[str]) -> bool

Check if the constraints have changed.

Source code in QDMpy/_core/fit.py
243
244
245
246
247
def constraints_changed(self, constraints: List[float], constraint_types: List[str]) -> bool:
    """
    Check if the constraints have changed.
    """
    return list(self._constraints.keys()) != constraints or self._constraint_types != constraint_types

fit_frange(data: NDArray, freq: NDArray, initial_parameters: NDArray) -> List[NDArray]

Wrapper for the fit_constrained function.

Parameters:

Name Type Description Default
data NDArray

data for one frequency range, to be fitted. array of size (n_pol, n_pixel, n_freqs) of the ODMR data

required
freq NDArray

array of size (n_freqs) of the frequencies

required
initial_parameters NDArray

initial guess for the fit, an array of size (n_pol * n_pixel, 2 * n_param) of the initial parameters

required

Returns:

Name Type Description
fit_results List[NDArray]

results consist of: parameters, states, chi_squares, number_iterations, execution_time results: array of size (n_poln_pixel, n_param) of the fitted parameters states: array of size (n_poln_pixel) of the fit states (i.e. did the fit work) chi_squares: array of size (n_poln_pixel) of the chi squares number_iterations: array of size (n_poln_pixel) of the number of iterations execution_time: execution time

Source code in QDMpy/_core/fit.py
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
def fit_frange(self, data: NDArray, freq: NDArray, initial_parameters: NDArray) -> List[NDArray]:
    """
    Wrapper for the fit_constrained function.

    Args:
        data: data for one frequency range, to be fitted. array of size (n_pol, n_pixel, n_freqs) of the ODMR data
        freq: array of size (n_freqs) of the frequencies
        initial_parameters: initial guess for the fit, an array of size (n_pol * n_pixel, 2 * n_param) of
            the initial parameters

    Returns:
        fit_results: results consist of: parameters, states, chi_squares, number_iterations, execution_time
            results: array of size (n_pol*n_pixel, n_param) of the fitted parameters
            states: array of size (n_pol*n_pixel) of the fit states (i.e. did the fit work)
            chi_squares: array of size (n_pol*n_pixel) of the chi squares
            number_iterations: array of size (n_pol*n_pixel) of the number of iterations
            execution_time: execution time
    """
    # reshape the data into a single array with (n_pix*n_pol, n_freqs)
    n_pol, n_pix, n_freqs = data.shape
    data = data.reshape((-1, n_freqs))
    initial_parameters = initial_parameters.reshape((-1, self.n_parameter))
    n_pixel = data.shape[0]
    constraints = self.get_constraints_array(n_pixel)
    constraint_types = self.get_constraint_types()

    results = gf.fit_constrained(
        data=np.ascontiguousarray(data, dtype=np.float32),
        user_info=np.ascontiguousarray(freq, dtype=np.float32),
        constraints=np.ascontiguousarray(constraints, dtype=np.float32),
        constraint_types=constraint_types,
        initial_parameters=np.ascontiguousarray(initial_parameters, dtype=np.float32),
        weights=None,
        model_id=self.model_id,
        max_number_iterations=QDMpy.SETTINGS["fit"]["max_number_iterations"],
        tolerance=QDMpy.SETTINGS["fit"]["tolerance"],
    )

    return list(results)

get_constraint_types() -> NDArray

Return the constraint types. :return: np.array

Source code in QDMpy/_core/fit.py
260
261
262
263
264
265
266
def get_constraint_types(self) -> NDArray:
    """
    Return the constraint types.
    :return: np.array
    """
    fit_bounds = [CONSTRAINT_TYPES.index(self._constraints[k][2]) for k in self.model_params_unique]
    return np.array(fit_bounds).astype(np.int32)

get_constraints_array(n_pixel: int) -> NDArray

Return the constraints as an array (pixel, 2*fitting_parameters). :return: np.array

Source code in QDMpy/_core/fit.py
249
250
251
252
253
254
255
256
257
258
def get_constraints_array(self, n_pixel: int) -> NDArray:
    """
    Return the constraints as an array (pixel, 2*fitting_parameters).
    :return: np.array
    """
    constraints_list: List[float] = []
    for k in self.model_params_unique:
        constraints_list.extend((self._constraints[k][0], self._constraints[k][1]))
    constraints = np.tile(constraints_list, (n_pixel, 1))
    return constraints

get_initial_parameter() -> NDArray

Constructs an initial guess for the fit.

Source code in QDMpy/_core/fit.py
338
339
340
341
342
343
344
345
346
347
348
349
def get_initial_parameter(self) -> NDArray:
    """
    Constructs an initial guess for the fit.
    """
    fit_parameter = []

    for p in self.model_params:
        param = getattr(self, f"_guess_{p}")()
        fit_parameter.append(param)

    fit_parameter = np.stack(fit_parameter, axis=fit_parameter[-1].ndim)
    return np.ascontiguousarray(fit_parameter, dtype=np.float32)

get_param(param: str) -> Union[NDArray, None]

Get the value of a parameter reshaped to the image dimesions.

Source code in QDMpy/_core/fit.py
273
274
275
276
277
278
279
280
281
282
283
284
def get_param(self, param: str) -> Union[NDArray, None]:
    """
    Get the value of a parameter reshaped to the image dimesions.
    """
    if not self.fitted:
        raise NotImplementedError("No fit has been performed yet. Run fit_odmr().")
    if param in ("chi2", "chi_squares", "chi_squared"):
        return self._chi_squares
    idx = self._param_idx(param)
    if param == "mean_contrast":
        return np.mean(self._fit_results[:, :, :, idx], axis=-1)  # type: ignore[index]
    return self._fit_results[:, :, :, idx]  # type: ignore[index]

guess_model_name(n_spectra = 100, *args, **kwargs) -> str property

Guess the model name from the data.

Source code in QDMpy/_core/fit.py
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
@property
def guess_model_name(self, n_spectra=100, *args, **kwargs) -> str:
    """Guess the model name from the data."""
    data = np.median(self.data, axis=2)
    n_peaks, doubt, peaks = guess_model(data)

    if doubt:
        self.LOG.warning(
            "Doubt on the diamond type. Check using `guess_diamond_type('debug')` " "and set manually if incorrect."
        )

    model = [mdict for m, mdict in models.IMPLEMENTED.items() if mdict["n_peaks"] == n_peaks][0]
    self.LOG.info(f"Guessed diamond type: {n_peaks} peaks -> {model}")
    return model["func_name"]

initial_parameter() -> NDArray property

Return the initial parameter.

Source code in QDMpy/_core/fit.py
166
167
168
169
170
171
172
173
@property
def initial_parameter(self) -> NDArray:
    """
    Return the initial parameter.
    """
    if self._initial_parameter is None:
        self._initial_parameter = self.get_initial_parameter()
    return self._initial_parameter

model() -> dict property

Return the model dictionary.

Source code in QDMpy/_core/fit.py
117
118
119
120
121
122
@property
def model(self) -> dict:
    """
    Return the model dictionary.
    """
    return self._model

model_params() -> dict property

Return the model parameters.

Source code in QDMpy/_core/fit.py
110
111
112
113
114
115
@property
def model_params(self) -> dict:
    """
    Return the model parameters.
    """
    return self._model["params"]

model_params_unique() -> List[str] property

Return a list of unique fitting parameters. :return: list

Source code in QDMpy/_core/fit.py
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
@property
def model_params_unique(self) -> List[str]:
    """
    Return a list of unique fitting parameters.
    :return: list
    """
    lst = []
    for v in self.model_params:
        if self.model_params.count(v) > 1:
            for n in range(10):
                if f"{v}_{n}" not in lst:
                    lst.append(f"{v}_{n}")
                    break
        else:
            lst.append(v)
    return lst

reshape_result(result: NDArray) -> NDArray

Reshape the results to the original shape of (n_pol, npix, -1)

Parameters:

Name Type Description Default
result NDArray

array of size (n_pol * n_pixel, -1) of the fitted parameters

required

Returns:

Name Type Description
result NDArray

array of size (n_pol, n_pixel, -1) of the fitted parameters

Source code in QDMpy/_core/fit.py
462
463
464
465
466
467
468
469
470
471
472
473
474
def reshape_result(self, result: NDArray) -> NDArray:
    """
    Reshape the results to the original shape of (n_pol, npix, -1)

    Args:
        result: array of size (n_pol * n_pixel, -1) of the fitted parameters

    Returns:
        result: array of size (n_pol, n_pixel, -1) of the fitted parameters
    """
    n_pol, npix, _ = self.data[0].shape
    result = result.reshape((n_pol, npix, -1))
    return np.squeeze(result)

reshape_results(results: List[NDArray]) -> NDArray

Reshape the results from the fit_constrained function into the correct shape.

Parameters:

Name Type Description Default
results List[NDArray]

results consist of: parameters, states, chi_squares, number_iterations, execution_time

required

Returns:

Name Type Description
results NDArray

results consist of: parameters, states, chi_squares, number_iterations, execution_time results: array of size (n_pol, n_pixel, n_param) of the fitted parameters states: array of size (n_pol, n_pixel) of the fit states (i.e. did the fit work) chi_squares: array of size (n_pol, n_pixel) of the chi squares number_iterations: array of size (n_pol, n_pixel) of the number of iterations execution_time: execution time

Source code in QDMpy/_core/fit.py
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
def reshape_results(self, results: List[NDArray]) -> NDArray:
    """Reshape the results from the fit_constrained function into the correct shape.

    Args:
        results: results consist of: parameters, states, chi_squares, number_iterations, execution_time

    Returns:
        results: results consist of: parameters, states, chi_squares, number_iterations, execution_time
            results: array of size (n_pol, n_pixel, n_param) of the fitted parameters
            states: array of size (n_pol, n_pixel) of the fit states (i.e. did the fit work)
            chi_squares: array of size (n_pol, n_pixel) of the chi squares
            number_iterations: array of size (n_pol, n_pixel) of the number of iterations
            execution_time: execution time
    """
    for i in range(len(results)):
        if isinstance(results[i], float):
            continue
        results[i] = self.reshape_result(results[i])
    return results

set_constraints(param: str, vmin: Union[float, None] = None, vmax: Union[float, None] = None, constraint_type: Union[str, None] = None, reset_fit: bool = True)

Set the constraints for the fit.

:param param: str The parameter to set the constraints for. :param vmin: float, optional The minimum value to set the constraints to. The default is None. :param vmax: float, optional The maximum value to set the constraints to. The default is None. :param constraint_type: str optional The bound type to set the constraints to. The default is None. :param reset_fit: bool, optional Whether to reset the fit results. The default is True.

Source code in QDMpy/_core/fit.py
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
def set_constraints(
    self,
    param: str,
    vmin: Union[float, None] = None,
    vmax: Union[float, None] = None,
    constraint_type: Union[str, None] = None,
    reset_fit: bool = True,
):
    """
    Set the constraints for the fit.

    :param param: str
        The parameter to set the constraints for.
    :param vmin: float, optional
        The minimum value to set the constraints to. The default is None.
    :param vmax: float, optional
        The maximum value to set the constraints to. The default is None.
    :param constraint_type: str optional
        The bound type to set the constraints to. The default is None.
    :param reset_fit: bool, optional
        Whether to reset the fit results. The default is True.
    """
    if isinstance(constraint_type, int):
        constraint_type = CONSTRAINT_TYPES[constraint_type]

    if constraint_type is not None and constraint_type not in CONSTRAINT_TYPES:
        raise ValueError(f"Unknown constraint type: {constraint_type} choose from {CONSTRAINT_TYPES}")

    if param == "contrast" and self.model_params_unique != self.model_params:
        for contrast in [v for v in self.model_params_unique if "contrast" in v]:
            self.set_constraints(contrast, vmin=vmin, vmax=vmax, constraint_type=constraint_type)
    else:
        self.LOG.debug(f"Setting constraints for {param}: ({vmin}, {vmax}) with {constraint_type}")
        self._constraints[param] = [
            vmin,
            vmax,
            constraint_type,
            UNITS[param.split("_")[0]],
        ]

    if reset_fit:
        self._reset_fit()

set_free_constraints()

Set the constraints to be free.

Source code in QDMpy/_core/fit.py
231
232
233
234
235
236
def set_free_constraints(self):
    """
    Set the constraints to be free.
    """
    for param in set(self.model_params_unique):
        self.set_constraints(param, constraint_type="FREE")

guess_center(data: NDArray, freq: NDArray) -> NDArray

Guess the center frequency of ODMR data.

:param data: np.array data to guess the center frequency from :param freq: np.array frequency range of the data

:return: np.array center frequency of the data

Source code in QDMpy/_core/fit.py
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
def guess_center(data: NDArray, freq: NDArray) -> NDArray:
    """
    Guess the center frequency of ODMR data.

    :param data: np.array
        data to guess the center frequency from
    :param freq: np.array
        frequency range of the data

    :return: np.array
        center frequency of the data
    """
    # center frequency
    center_lf = guess_center_freq_single(data[:, 0], freq[0])
    center_rf = guess_center_freq_single(data[:, 1], freq[1])
    center = np.stack([center_lf, center_rf], axis=0)
    center = np.swapaxes(center, 0, 1)

    return center

guess_center_freq_single(data: NDArray, freq: NDArray) -> NDArray

Guess the center frequency of a single frequency range.

:param data: np.array data to guess the center frequency from :param freq: np.array frequency range of the data :return: np.array center frequency of the data

Source code in QDMpy/_core/fit.py
584
585
586
587
588
589
590
591
592
593
594
595
596
597
def guess_center_freq_single(data: NDArray, freq: NDArray) -> NDArray:
    """
    Guess the center frequency of a single frequency range.

    :param data: np.array
        data to guess the center frequency from
    :param freq: np.array
        frequency range of the data
    :return: np.array
        center frequency of the data
    """
    data = normalized_cumsum(data)
    idx = np.argmin(np.abs(data - 0.5), axis=-1)
    return freq[idx]

guess_contrast(data: NDArray) -> NDArray

Guess the contrast of a ODMR data.

:param data: np.array data to guess the contrast from :return: np.array contrast of the data

Source code in QDMpy/_core/fit.py
477
478
479
480
481
482
483
484
485
486
487
488
489
def guess_contrast(data: NDArray) -> NDArray:
    """
    Guess the contrast of a ODMR data.

    :param data: np.array
        data to guess the contrast from
    :return: np.array
        contrast of the data
    """
    mx = np.nanmax(data, axis=-1)
    mn = np.nanmin(data, axis=-1)
    amp = np.abs((mx - mn) / mx)
    return amp

guess_width(data: NDArray, f_GHz: NDArray, n_peaks: Optional[int]) -> NDArray

Guess the width of a ODMR resonance peaks.

:param data: np.array data to guess the width from :param f_GHz: np.array frequency range of the data

:return: np.array width of the data

Source code in QDMpy/_core/fit.py
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
def guess_width(data: NDArray, f_GHz: NDArray, n_peaks: Optional[int]) -> NDArray:
    """
    Guess the width of a ODMR resonance peaks.

    :param data: np.array
        data to guess the width from
    :param f_GHz: np.array
        frequency range of the data

    :return: np.array
        width of the data
    """
    # center frequency
    width_lf = guess_width_single(data[:, 0], f_GHz[0], n_peaks=n_peaks)
    width_rf = guess_width_single(data[:, 1], f_GHz[1], n_peaks=n_peaks)
    width = np.stack([width_lf, width_rf], axis=1)

    return width

guess_width_single(data: NDArray, freq: NDArray, n_peaks: Optional[int]) -> NDArray

Guess the width of a single frequency range.

:param data: np.array data to guess the width from :param freq: np.array frequency range of the data

:return: np.array width of the data

Raises ValueError if the number of peaks is not 1, 2 or 3.

Source code in QDMpy/_core/fit.py
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
def guess_width_single(data: NDArray, freq: NDArray, n_peaks: Optional[int]) -> NDArray:
    """
    Guess the width of a single frequency range.

    :param data: np.array
        data to guess the width from
    :param freq: np.array
        frequency range of the data

    :return: np.array
        width of the data

    Raises ValueError if the number of peaks is not 1, 2 or 3.
    """
    data = normalized_cumsum(data)
    correct = 0
    if n_peaks == 1:
        vmin, vmax = 0.3, 0.7
    elif n_peaks == 2:
        vmin, vmax = 0.4, 0.6
        correct = -0.001
    elif n_peaks == 3:
        vmin, vmax = 0.35, 0.65
    else:
        raise ValueError("n_peaks must be 1, 2 or 3")

    lidx = np.argmin(np.abs(data - vmin), axis=-1)
    ridx = np.argmin(np.abs(data - vmax), axis=-1)
    return (freq[lidx] - freq[ridx]) + correct

make_parameter_array(c0: float, n_params: int, p: NDArray, params: Dict[int, List[float]]) -> np.ndarray

Make a parameter array for a given center frequency.

:param c0: float center frequency :param n_params: int number of parameters :param p: np.array parameter array :param params: dict parameter dictionary :return: np.array parameter array

Source code in QDMpy/_core/fit.py
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
def make_parameter_array(c0: float, n_params: int, p: NDArray, params: Dict[int, List[float]]) -> np.ndarray:
    """Make a parameter array for a given center frequency.

    :param c0: float
        center frequency
    :param n_params: int
        number of parameters
    :param p: np.array
        parameter array
    :param params: dict
        parameter dictionary
    :return: np.array
        parameter array
    """
    p00 = p.copy()
    p00[:, 0] *= c0 - 0.0001
    p00[:, 1:] *= params[n_params]
    return p00

normalized_cumsum(data: NDArray) -> NDArray

Calculate the normalized cumulative sum of the data.

Parameters

NDArray

Data to calculate the normalized cumulative sum of.

Returns

NDArray Normalized cumulative sum of the data.

Source code in QDMpy/_core/fit.py
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
def normalized_cumsum(data: NDArray) -> NDArray:
    """Calculate the normalized cumulative sum of the data.

    Parameters
    ----------
    data : NDArray
        Data to calculate the normalized cumulative sum of.


    Returns
    -------
    NDArray
        Normalized cumulative sum of the data.
    """
    data = np.cumsum(data - 1, axis=-1)
    data -= np.expand_dims(np.min(data, axis=-1), axis=2)
    data /= np.expand_dims(np.max(data, axis=-1), axis=2)
    return data