screamlab 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
screamlab/utils.py ADDED
@@ -0,0 +1,729 @@
1
+ """
2
+ Spectral Analysis/Fitting Module
3
+
4
+ This module provides tools for fitting spectral data and analyzing buildup behaviors
5
+ using the `lmfit` package. It includes several classes designed for spectral deconvolution
6
+ and dynamic nuclear polarization (DNP) buildup kinetic analysis.
7
+
8
+ Classes:
9
+ Spectral Fitting/Deconvolution Classes:
10
+ - Fitter: The base class for fitting spectral data.
11
+ - Prefitter: A specialized fitter that fits a preselected spectrum.
12
+ - GlobalFitter: A fitter that applies parameter constraints across multiple spectra.
13
+ - IndependentFitter: A simple extension of `Fitter` with no additional functionality.
14
+
15
+ DNP Buildup Kinetic Fitting Classes:
16
+ - BuildupFitter: The parent class for fitting DNP buildup kinetics.
17
+ - ExpFitter: A fitter for single-exponential buildup behavior.
18
+ - ExpFitterWithOffset: A variant of `ExpFitter` with an additional offset parameter.
19
+ - BiexpFitter: A fitter for biexponential buildup behavior.
20
+ - BiexpFitterWithOffset: A variant of `BiexpFitter`
21
+ with an additional offset parameter.
22
+ - StretchedExponentialFitter: A fitter for stretched exponential buildup behavior.
23
+ """
24
+
25
+ import copy
26
+ import numpy as np
27
+ import lmfit
28
+ from pyDOE3 import lhs
29
+ from screamlab import functions
30
+
31
+
32
+ class Fitter:
33
+ """
34
+ Base class for spectral fitting using `lmfit`.
35
+
36
+ This class handles parameter initialization and spectral fitting for a ds.
37
+
38
+ Attributes
39
+ ----------
40
+ dataset : :obj:`screamlab.ds.Dataset`
41
+ Containing spectra and peak information.
42
+
43
+ """
44
+
45
+ def __init__(self, dataset):
46
+ """
47
+ Initializes the Fitter with a ds.
48
+
49
+ Args
50
+ ----
51
+ dataset : An object containing spectral data and peak list.
52
+
53
+ """
54
+ self.dataset = dataset
55
+
56
+ def fit(self):
57
+ """
58
+ Performs spectral fitting using the `lmfit.minimize` function.
59
+
60
+ Returns
61
+ -------
62
+ lmfit.MinimizerResult
63
+ The result of the fitting process.
64
+
65
+ """
66
+ x_axis, y_axis = self._generate_axis_list()
67
+ params = self._generate_params_list()
68
+ params = self._set_param_expr(params)
69
+
70
+ return self._start_minimize(x_axis, y_axis, params)
71
+
72
+ def _start_minimize(self, x_axis, y_axis, params):
73
+ return lmfit.minimize(
74
+ self._spectral_fitting, params, args=(x_axis, y_axis)
75
+ )
76
+
77
+ def _set_param_expr(self, params):
78
+ """
79
+ Modifies parameter expressions if needed.
80
+
81
+ Default implementation returns parameters unchanged.
82
+
83
+ Args
84
+ ----
85
+ params : lmfit.Parameters
86
+ The parameters to be modified.
87
+
88
+ Returns
89
+ -------
90
+ lmfit.Parameters
91
+ The modified parameters.
92
+
93
+ """
94
+ return params
95
+
96
+ def _generate_axis_list(self):
97
+ """
98
+ Generates lists of x-axis and y-axis values for all spectra in the ds.
99
+
100
+ Returns
101
+ -------
102
+ tuple
103
+ Two lists containing x-axis and y-axis values for each spectrum.
104
+
105
+ """
106
+ x_axis, y_axis = [], []
107
+ for spectrum in self.dataset.spectra:
108
+ x_axis.append(spectrum.x_axis)
109
+ y_axis.append(spectrum.y_axis)
110
+ return x_axis, y_axis
111
+
112
+ def _generate_params_list(self):
113
+ """
114
+ Generates initial fitting parameters based on peak information in the ds.
115
+
116
+ Returns
117
+ -------
118
+ lmfit.Parameters
119
+ The initialized parameters for fitting.
120
+
121
+ """
122
+ params = lmfit.Parameters()
123
+ spectra = self._get_spectra_list()
124
+ lw_types = {
125
+ "voigt": ["sigma", "gamma"],
126
+ "gauss": ["sigma"],
127
+ "lorentz": ["gamma"],
128
+ }
129
+ for spectrum_nr, _ in enumerate(spectra):
130
+ for peak in self.dataset.peak_list:
131
+ params.add(**self._get_amplitude_dict(peak, spectrum_nr))
132
+ params.add(**self._get_center_dict(peak, spectrum_nr))
133
+
134
+ for lw_type in lw_types.get(peak.fitting_type, []):
135
+ params.add(
136
+ **self._get_lw_dict(peak, spectrum_nr, lw_type)
137
+ )
138
+ return params
139
+
140
+ def _get_spectra_list(self):
141
+ """
142
+ Retrieves the appropriate spectra for fitting.
143
+
144
+ Returns
145
+ -------
146
+ list
147
+ A list of spectra to be fitted.
148
+
149
+ """
150
+ return (
151
+ [self.dataset.spectra[self.dataset.props.spectrum_for_prefit]]
152
+ if isinstance(self, Prefitter)
153
+ else self.dataset.spectra
154
+ )
155
+
156
+ def _get_amplitude_dict(self, peak, nr):
157
+ """
158
+ Generates an amplitude parameter dictionary for a given peak.
159
+
160
+ Args:
161
+ peak: A peak object containing fitting information.
162
+ nr (int): The spectrum index.
163
+
164
+ Returns
165
+ -------
166
+ dict
167
+ A dictionary defining the amplitude parameter.
168
+
169
+ """
170
+ return {
171
+ "name": f"{peak.peak_label}_amp_{nr}",
172
+ "value": 200 if peak.peak_sign == "+" else -200,
173
+ "min": 0 if peak.peak_sign == "+" else -np.inf,
174
+ "max": np.inf if peak.peak_sign == "+" else 0,
175
+ }
176
+
177
+ def _get_center_dict(self, peak, nr):
178
+ """
179
+ Generates a center parameter dictionary for a given peak.
180
+
181
+ Args
182
+ ----
183
+ peak
184
+ A peak object containing fitting information.
185
+ nr (int)
186
+ The spectrum index.
187
+
188
+ Returns
189
+ -------
190
+ dict
191
+ A dictionary defining the center parameter.
192
+
193
+ """
194
+ return {
195
+ "name": f"{peak.peak_label}_cen_{nr}",
196
+ "value": peak.peak_center,
197
+ "min": peak.peak_center - 20,
198
+ "max": peak.peak_center + 20,
199
+ }
200
+
201
+ def _get_lw_dict(self, peak, nr, lw):
202
+ """
203
+ Generates a linewidth parameter dictionary for a given peak.
204
+
205
+ Args
206
+ ----
207
+ peak: A peak object containing fitting information.
208
+ nr (int): The spectrum index.
209
+ lw (str): The linewidth type (e.g., 'sigma', 'gamma').
210
+
211
+ Returns
212
+ -------
213
+ dict: A dictionary defining the linewidth parameter.
214
+
215
+ """
216
+ return {
217
+ "name": f"{peak.peak_label}_{lw}_{nr}",
218
+ "value": (
219
+ peak.line_broadening[lw]["min"]
220
+ + peak.line_broadening[lw]["max"]
221
+ )
222
+ / 2,
223
+ "min": peak.line_broadening[lw]["min"],
224
+ "max": peak.line_broadening[lw]["max"],
225
+ }
226
+
227
+ def _spectral_fitting(self, params, x_axis, y_axis):
228
+ """
229
+ Computes the residual between the fitted and experimental spectra.
230
+
231
+ Args
232
+ ----
233
+ params (lmfit.Parameters): The fitting parameters.
234
+ x_axis (list): List of x-axis values.
235
+ y_axis (list): List of y-axis values.
236
+
237
+ Returns
238
+ -------
239
+ np.ndarray: The residual between the fitted and experimental spectra.
240
+
241
+ """
242
+ residual = copy.deepcopy(y_axis)
243
+ params_dict_list = functions.generate_spectra_param_dict(params)
244
+ for key, val_list in params_dict_list.items():
245
+ for val in val_list:
246
+ simspec = [0 for _ in range(len(x_axis[key]))]
247
+ simspec = functions.calc_peak(x_axis[key], simspec, val)
248
+ residual[key] -= simspec
249
+ return np.concatenate(residual)
250
+
251
+
252
+ class Prefitter(Fitter):
253
+ """
254
+ A subclass of Fitter that performs a preliminary fit on a preselected spectrum.
255
+
256
+ By fitting the spectrum first, it estimates optimal parameters, particularly for
257
+ linewidths, and narrows down the parameter intervals. The pre-fit parameters define bounds
258
+ (±10%) for the linewidths. These refined intervals are then used in the global fit,
259
+ significantly reducing computational time by limiting the parameter range.
260
+ """
261
+
262
+ def _generate_axis_list(self):
263
+ """
264
+ Generate the x and y axes for prefit spectrum.
265
+
266
+ This function retrieves the x and y axes from the spectrum
267
+ specified in the ds properties for prefit.
268
+
269
+ :return: Tuple containing lists of x and y axes.
270
+ """
271
+ spectrum_for_prefit = self.dataset.props.spectrum_for_prefit
272
+ x_axis, y_axis = [], []
273
+ x_axis.append(self.dataset.spectra[spectrum_for_prefit].x_axis)
274
+ y_axis.append(self.dataset.spectra[spectrum_for_prefit].y_axis)
275
+ return x_axis, y_axis
276
+
277
+ def _start_minimize(self, x_axis, y_axis, params):
278
+ result = lmfit.minimize(
279
+ self._spectral_fitting, params, args=(x_axis, y_axis)
280
+ )
281
+ return result
282
+
283
+
284
+ class GlobalFitter(Fitter):
285
+ """
286
+ Global fit over all spectra at different polarization times.
287
+
288
+ For SCREAM-DNP data, it can be assumed that the line broadening did not vary over all
289
+ polarization times in cases where a homogeneous polarization buildup on protons exists.
290
+
291
+ Same goes for the center of each peak since the chemical shift is not depending on the
292
+ polarization time. For this, it is recommended to carefully reference all spectra during
293
+ post-processing. With this, the number of fitting parameters can drastically be reduced,
294
+ yielding a shorter calculation time. In this case, all spectra from a SCREAM-DNP buildup
295
+ series can be described by two lineshape parameters (sigma and gamma), one variable for
296
+ the peak center (µ), and n amplitude variables per resonance, where n stands for the
297
+ number of spectra within one series.
298
+ """
299
+
300
+ def _set_param_expr(self, params):
301
+ """
302
+ Set parameter expressions to enforce global constraints.
303
+
304
+ This function modifies parameters such that all parameters
305
+ except for amplitudes ("amp") share the same global parameter
306
+ value across multiple spectra by setting their expressions.
307
+
308
+ :param params: lmfit Parameters object containing the parameters to be modified.
309
+ :return: Modified lmfit Parameters object with parameter expressions set.
310
+ """
311
+ for keys in params.keys():
312
+ splitted_keys = keys.split("_")
313
+ if splitted_keys[-1] != "0" and splitted_keys[-2] != "amp":
314
+ splitted_keys[-1] = "0"
315
+ params[keys].expr = "_".join(splitted_keys)
316
+ return params
317
+
318
+
319
+ class IndependentFitter(Fitter):
320
+ """
321
+ Fit of each spectrum with individual parameter set.
322
+
323
+ In some cases it might be necessary to simulate each spectrum from one series with its own
324
+ parameter set.
325
+
326
+ This option is also provided. Each resonance in each spectrum will be fitted to two
327
+ lineshape parameters, an amplitude and a globally determined peak center. Note that this
328
+ yields higher run times. A prefit can be combined with this case to save time. However, it
329
+ must be ensured that all spectra can be fitted by conditions given in point two.
330
+ """
331
+
332
+
333
+ class BuildupFitter:
334
+ """
335
+ Base class for fitting buildup data using `lmfit`.
336
+
337
+ This class is responsible for performing a fitting procedure on a ds
338
+ of peaks with time-dependent intensities.
339
+
340
+ Attributes
341
+ ----------
342
+ dataset: :obj:`screamlab.ds.Dataset` containing peak
343
+ intensity and polarization time information.
344
+
345
+ """
346
+
347
+ def __init__(self, dataset):
348
+ """
349
+ Initialize the BuildupFitter with a ds.
350
+
351
+ :param dataset: The ds containing peak list information.
352
+ """
353
+ self.dataset = dataset
354
+
355
+ def perform_fit(self):
356
+ """
357
+ Perform the fitting procedure on the ds's peak list.
358
+
359
+ :return: List of best fit results for each peak.
360
+ """
361
+ result_list = []
362
+ for peak in self.dataset.peak_list:
363
+ default_param_dict = self._get_default_param_dict(peak)
364
+ lhs_init_params = self._get_lhs_init_params(default_param_dict)
365
+ best_result = None
366
+ best_chisqr = np.inf
367
+ for init_params in lhs_init_params:
368
+ params = self._set_params(default_param_dict, init_params)
369
+ try:
370
+ result = self._start_minimize(params, peak.buildup_vals)
371
+ best_result, best_chisqr = self._check_result_quality(
372
+ best_result, best_chisqr, result
373
+ )
374
+ except (ValueError, RuntimeError): # nosec B110
375
+ pass
376
+ result_list.append(best_result)
377
+ return result_list
378
+
379
+ def _get_lhs_init_params(self, default_param_dict, n_samples=1):
380
+ """
381
+ Generate Latin Hypercube Sampling (LHS) initial parameters.
382
+
383
+ :param default_param_dict: Dictionary of default parameter values and bounds.
384
+ :param n_samples: Number of LHS samples to generate.
385
+ :return: List of sampled parameters.
386
+ """
387
+ param_bounds = [
388
+ self._get_param_bounds(default_param_dict[key])
389
+ for key in default_param_dict
390
+ ]
391
+ if n_samples == 1:
392
+ n_samples = len(default_param_dict.keys()) * 100
393
+ lhs_samples = lhs(len(default_param_dict.keys()), samples=n_samples)
394
+ return self._set_sample_params(lhs_samples, param_bounds)
395
+
396
+ def _start_minimize(self, params, args):
397
+ """
398
+ Start the minimization process using lmfit.
399
+
400
+ :param params: Parameters for fitting.
401
+ :param args: Arguments containing time delays and intensities.
402
+ :return: Minimization result.
403
+ """
404
+ return lmfit.minimize(
405
+ self._fitting_function,
406
+ params,
407
+ args=(args.tpol, args.intensity),
408
+ )
409
+
410
+ def _check_result_quality(self, best_result, best_chisqr, result):
411
+ """
412
+ Check if the new result is better than the current best result.
413
+
414
+ :param best_result: The current best fitting result.
415
+ :param best_chisqr: The chi-squared value of the best result.
416
+ :param result: The new fitting result.
417
+ :return: The best result and its chi-squared value.
418
+ """
419
+ if result.chisqr < best_chisqr:
420
+ return result, result.chisqr
421
+ return best_result, best_chisqr
422
+
423
+ def _get_param_bounds(self, params):
424
+ """
425
+ Retrieve parameter bounds.
426
+
427
+ :param params: Dictionary containing parameter min and max values.
428
+ :return: Tuple containing (min, max) bounds.
429
+ """
430
+ return (params["min"], params["max"])
431
+
432
+ def _set_sample_params(self, lhs_samples, param_bounds):
433
+ """
434
+ Scale LHS samples according to parameter bounds.
435
+
436
+ :param lhs_samples: LHS-generated samples.
437
+ :param param_bounds: List of parameter bounds.
438
+ :return: List of sampled parameters.
439
+ """
440
+ sampled_params = []
441
+ for sample in lhs_samples:
442
+ scaled_sample = [
443
+ low + sample[i] * (high - low)
444
+ for i, (low, high) in enumerate(param_bounds)
445
+ ]
446
+ sampled_params.append(scaled_sample)
447
+ return sampled_params
448
+
449
+ def _set_params(self, default_param_dict, init_params):
450
+ """
451
+ Set up lmfit Parameters object using initial parameters.
452
+
453
+ :param default_param_dict: Default parameter dictionary.
454
+ :param init_params: Initial parameter values.
455
+ :return: lmfit Parameters object.
456
+ """
457
+ params = lmfit.Parameters()
458
+ for key_nr, key in enumerate(default_param_dict.keys()):
459
+ default_param_dict[key]["value"] = init_params[key_nr]
460
+ params.add(key, **default_param_dict[key])
461
+ return params
462
+
463
+ def _fitting_function(self, params, tdel, intensity):
464
+ """
465
+ Define the residual function for fitting.
466
+
467
+ :param params: Parameters for fitting.
468
+ :param tdel: Time delays.
469
+ :param intensity: Measured intensities.
470
+ :return: Residuals between observed and simulated intensities.
471
+ """
472
+ residual = copy.deepcopy(intensity)
473
+ param_list = self._generate_param_list(params)
474
+ intensity_sim = self._calc_intensity(tdel, param_list)
475
+ return [a - b for a, b in zip(residual, intensity_sim)]
476
+
477
+ def _generate_param_list(self, params):
478
+ """
479
+ Generate a list of parameter values from lmfit Parameters.
480
+
481
+ :param params: lmfit Parameters object.
482
+ :return: List of parameter values.
483
+ """
484
+ return [params[key].value for key in params]
485
+
486
+ def _get_intensity_dict(self, peak):
487
+ """
488
+ Generate intensity parameter dictionary.
489
+
490
+ :param peak: Peak object containing buildup values.
491
+ :return: Dictionary with default intensity parameter values.
492
+ """
493
+ return (
494
+ {
495
+ "value": 10,
496
+ "min": 0,
497
+ "max": max(peak.buildup_vals.intensity) * 3,
498
+ }
499
+ if peak.peak_sign == "+"
500
+ else {
501
+ "value": 10,
502
+ "max": 0,
503
+ "min": min(peak.buildup_vals.intensity) * 3,
504
+ }
505
+ )
506
+
507
+ def _get_time_dict(self, peak):
508
+ """
509
+ Generate time delay parameter dictionary.
510
+
511
+ :param peak: Peak object containing buildup values.
512
+ :return: Dictionary with default time parameter values.
513
+ """
514
+ return {"value": 5, "min": 0, "max": max(peak.buildup_vals.tpol) * 3}
515
+
516
+ def _get_beta_dict(self):
517
+ return {"value": 0, "min": 0, "max": 1}
518
+
519
+
520
+ class BiexpFitter(BuildupFitter):
521
+ """
522
+ Class for fitting biexponential models to buildup data.
523
+
524
+ The biexponential model fits buildup curves using two exponential terms
525
+ characterized by amplitudes (Af, As) and time constants (tf, ts).
526
+
527
+ The model function is defined as:
528
+ I(t) = Af * (1 - exp(-t_pol / tf)) + As * (1 - exp(-t_pol / ts))
529
+
530
+ where:
531
+ - Af, As : amplitudes of the exponential components
532
+ - tf, ts : time constants of the exponential components (tf, ts > 0)
533
+ - t_pol : polarization time (independent variable)
534
+ - I(t_pol) : peak intensity at polarization time t_pol
535
+ """
536
+
537
+ def _get_default_param_dict(self, peak):
538
+ """
539
+ Define default parameters for biexponential fitting.
540
+
541
+ :param peak: Peak object containing peak_sign and buildup values.
542
+ :return: Dictionary of default parameters with keys: Af, As, tf, ts.
543
+ """
544
+ return {
545
+ "Af": self._get_intensity_dict(peak),
546
+ "As": self._get_intensity_dict(peak),
547
+ "tf": self._get_time_dict(peak),
548
+ "ts": self._get_time_dict(peak),
549
+ }
550
+
551
+ def _calc_intensity(self, tdel, param):
552
+ """
553
+ Calculate biexponential intensity.
554
+
555
+ :param tdel: Time delays.
556
+ :param param: List of parameters.
557
+ :return: Calculated intensity values.
558
+ """
559
+ return functions.calc_biexponential(tdel, param)
560
+
561
+
562
+ class BiexpFitterWithOffset(BuildupFitter):
563
+ """
564
+ Class for fitting biexponential models with offset to buildup data.
565
+
566
+ This fits buildup curves using two exponential terms
567
+ characterized by amplitudes (Af, As), time constants (tf, ts) and offset (t_off).
568
+
569
+ The model function is defined as:
570
+ I(t) = Af * (1 - exp(-(t_pol-t_off) / tf)) + As * (1 - exp(-(t_pol-t_off) / ts))
571
+
572
+ where:
573
+ - Af, As : amplitudes of the exponential components
574
+ - tf, ts : time constants of the exponential components (tf, ts > 0)
575
+ - t_off : offset in polarization time
576
+ - t_pol : polarization time (independent variable)
577
+ - I(t_pol) : peak intensity at polarization time t_pol
578
+ """
579
+
580
+ def _get_default_param_dict(self, peak):
581
+ """
582
+ Define default parameters for biexponential fitting.
583
+
584
+ :param peak: Peak object containing peak_sign and buildup values.
585
+ :return: Dictionary of default parameters with keys: Af, As, tf, ts.
586
+ """
587
+ return {
588
+ "Af": self._get_intensity_dict(peak),
589
+ "As": self._get_intensity_dict(peak),
590
+ "tf": self._get_time_dict(peak),
591
+ "ts": self._get_time_dict(peak),
592
+ "t_off": {"value": 0, "min": -5, "max": 5},
593
+ }
594
+
595
+ def _calc_intensity(self, tdel, param):
596
+ """
597
+ Calculate biexponential intensity with x axis offset.
598
+
599
+ :param tdel: Time delays.
600
+ :param param: List of parameters.
601
+ :return: Calculated intensity values.
602
+ """
603
+ return functions.calc_biexponential_with_offset(tdel, param)
604
+
605
+
606
+ class ExpFitter(BuildupFitter):
607
+ """
608
+ Class for fitting exponential models to buildup data.
609
+
610
+ This fits buildup curves using an exponential term
611
+ characterized by amplitude (Af) and time constant (tf).
612
+
613
+ The model function is defined as:
614
+ I(t) = Af * (1 - exp(-t_pol / tf))
615
+
616
+ where:
617
+ - Af : amplitudes of the exponential components
618
+ - tf : time constants of the exponential components (tf > 0)
619
+ - t_pol : polarization time (independent variable)
620
+ - I(t_pol) : peak intensity at polarization time t_pol
621
+ """
622
+
623
+ def _get_default_param_dict(self, peak):
624
+ """
625
+ Define default parameters for exponential fitting.
626
+
627
+ :param peak: Peak object containing peak_sign and buildup values.
628
+ :return: Dictionary of default parameters with keys: Af, tf.
629
+ """
630
+ return {
631
+ "Af": self._get_intensity_dict(peak),
632
+ "tf": self._get_time_dict(peak),
633
+ }
634
+
635
+ def _calc_intensity(self, tdel, param):
636
+ """
637
+ Calculate exponential intensity.
638
+
639
+ :param tdel: Time delays.
640
+ :param param: List of parameters.
641
+ :return: Calculated intensity values.
642
+ """
643
+ return functions.calc_exponential(tdel, param)
644
+
645
+
646
+ class ExpFitterWithOffset(BuildupFitter):
647
+ """
648
+ Class for fitting exponential models with offset to buildup data.
649
+
650
+ This fits buildup curves using an exponential term
651
+ characterized by amplitude (Af), time constant (tf) and offset (t_off).
652
+
653
+ The model function is defined as:
654
+ I(t) = Af * (1 - exp(-(t_pol-t_off) / tf))
655
+
656
+ where:
657
+ - Af : amplitudes of the exponential components
658
+ - tf : time constants of the exponential components (tf > 0)
659
+ - t_off : offset in polarization time
660
+ - t_pol : polarization time (independent variable)
661
+ - I(t_pol) : peak intensity at polarization time t_pol
662
+ """
663
+
664
+ def _get_default_param_dict(self, peak):
665
+ """
666
+ Define default parameters for exponential with offset in x fitting.
667
+
668
+ :param peak: Peak object containing peak_sign and buildup values.
669
+ :return: Dictionary of default parameters with keys: Af, tf, t_off.
670
+ """
671
+ return {
672
+ "Af": self._get_intensity_dict(peak),
673
+ "tf": self._get_time_dict(peak),
674
+ "t_off": {"value": 0, "min": -5, "max": 5},
675
+ }
676
+
677
+ def _calc_intensity(self, tdel, param):
678
+ """
679
+ Calculate exponential intensity with x axis offset.
680
+
681
+ :param tdel: Time delays.
682
+ :param param: List of parameters.
683
+ :return: Calculated intensity values.
684
+ """
685
+ return functions.calc_exponential_with_offset(tdel, param)
686
+
687
+
688
+ class StrechedExponentialFitter(BuildupFitter):
689
+ """
690
+ Class for fitting streched exponential models to buildup data.
691
+
692
+ This fits buildup curves using an streched exponential term
693
+ characterized by amplitude (Af), time constant (tf), and stretching factor (beta)..
694
+
695
+ The model function is defined as:
696
+ I(t) = Af * (1 - exp(-(t_pol / tf)^beta))
697
+
698
+ where:
699
+ - Af : amplitudes of the exponential components
700
+ - tf : time constants of the exponential components (tf > 0)
701
+ - beta : stretching factor (beta > 0, controls deviation from a simple exponential)
702
+ - t_pol : polarization time (independent variable)
703
+ - I(t_pol): peak intensity at polarization time t_pol
704
+
705
+
706
+ """
707
+
708
+ def _get_default_param_dict(self, peak):
709
+ """
710
+ Define default parameters for strechted exponential fitting.
711
+
712
+ :param peak: Peak object containing peak_sign and buildup values.
713
+ :return: Dictionary of default parameters with keys: Af, tf, beta.
714
+ """
715
+ return {
716
+ "Af": self._get_intensity_dict(peak),
717
+ "tf": self._get_time_dict(peak),
718
+ "beta": self._get_beta_dict(),
719
+ }
720
+
721
+ def _calc_intensity(self, tdel, param):
722
+ """
723
+ Calculate streched exponential intensity.
724
+
725
+ :param tdel: Time delays.
726
+ :param param: List of parameters.
727
+ :return: Calculated intensity values.
728
+ """
729
+ return functions.calc_stretched_exponential(tdel, param)