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/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """Package for reproducible evaluation of SCREAM-DNP data."""
Binary file
Binary file
screamlab/dataset.py ADDED
@@ -0,0 +1,655 @@
1
+ """
2
+ Spectral and Peak information containing module.
3
+
4
+ This moduel provides classes for handling and processing of spectral/peak information as well as
5
+ results from spectral fitting.
6
+
7
+ Classes:
8
+ Dataset: Represents the hole dataset and provides all functions needed to start analysis.
9
+ Spectra: Represents spectral data from NMR (Nuclear Magnetic Resonance) experiments.
10
+ Peak: Represents a peak with its properties.
11
+ BuildupList: Represents a list of buildup values used for fitting delay times and intensities.
12
+
13
+ """
14
+
15
+ from datetime import datetime
16
+ import numpy as np
17
+ from screamlab import io, utils, settings, functions
18
+
19
+
20
+ class Dataset:
21
+ """Represents a dataset containing NMR spectra, peak fitting, and buildup fitting."""
22
+
23
+ def __init__(self, props=settings.Properties()):
24
+ """
25
+ Initialize the Dataset with default or specified properties.
26
+
27
+ Parameters
28
+ ----------
29
+ props : settings.Properties, optional
30
+ Experiment properties used to configure the ds.
31
+ Defaults to settings.Properties().
32
+
33
+ """
34
+ self.importer = None
35
+ self.props = props
36
+ self.spectra = []
37
+ self.fitter = None
38
+ self.peak_list = []
39
+ self.lmfit_result_handler = io.LmfitResultHandler()
40
+
41
+ def __str__(self):
42
+ """Returns a string representation of the ds."""
43
+ return (
44
+ f"[[Dataset]]\n"
45
+ f"Fitted {len(self.peak_list)} peaks per spectrum in {len(self.spectra)} spectra."
46
+ )
47
+
48
+ def start_analysis(self):
49
+ """
50
+ Starting the analysis process.
51
+
52
+ The analysis is carried out in three stages: first, the spectral data are imported
53
+ from the Topspin file format; second, spectral deconvolution is performed; and finally,
54
+ a buildup fit is applied.
55
+
56
+ """
57
+ print(
58
+ f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}: "
59
+ f"Start loading data from topspin: {self.props.path_to_experiment}"
60
+ )
61
+ self._read_in_data_from_topspin()
62
+ print(
63
+ f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}: Start fitting."
64
+ )
65
+ self._calculate_peak_intensities()
66
+ print(
67
+ f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}: Start buildup fit."
68
+ )
69
+ self._start_buildup_fit()
70
+ print(
71
+ f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}: "
72
+ f"Start generating result files. ({self.props.output_folder})"
73
+ )
74
+ self._print_all()
75
+
76
+ def _start_buildup_fit_from_spectra(self):
77
+ """Starts buildup fitting using data imported from spectra CSV files."""
78
+
79
+ def _start_buildup_from_intensitys(self):
80
+ """Placeholder for starting buildup fitting from intensity values."""
81
+
82
+ def add_peak(
83
+ self,
84
+ center_of_peak,
85
+ peak_label="",
86
+ fitting_type="voigt",
87
+ peak_sign="-",
88
+ line_broadening=None,
89
+ ):
90
+ """
91
+ Adds a peak to the ds.
92
+
93
+ Attributes
94
+ ----------
95
+ center_of_peak (float): Peak position in ppm (chemical shift).
96
+ peak_label (str, optional): Custom label. Defaults to "Peak_at_<ppm>_ppm".
97
+ fitting_type (str, optional): Peak shape: "gauss", "lorentz", or "voigt" (default).
98
+ peak_sign (str, optional): "+" for upward, "-" for downward peaks. Defaults to "+".
99
+ line_broadening (dict, optional): Dict with "sigma" and "gamma" keys for line width.
100
+ Defaults to {"sigma": {"min": 0, "max": 3}, "gamma": {"min": 0, "max": 3}}.
101
+
102
+ """
103
+ if line_broadening is None:
104
+ line_broadening = {}
105
+ self.peak_list.append(Peak())
106
+ peak = self.peak_list[-1]
107
+ peak.peak_center = center_of_peak
108
+ peak.peak_label = peak_label
109
+ peak.fitting_type = fitting_type
110
+ peak.peak_sign = peak_sign
111
+ peak.line_broadening = line_broadening
112
+
113
+ def _read_in_data_from_topspin(self):
114
+ """Reads and imports data from TopSpin."""
115
+ self._setup_correct_topspin_importer()
116
+ self.importer.import_topspin_data()
117
+
118
+ def _setup_correct_topspin_importer(self):
119
+ """Sets up the appropriate TopSpin importer based on experiment properties."""
120
+ if len(self.props.expno) == 1:
121
+ self.importer = io.Pseudo2DImporter(self)
122
+ else:
123
+ self.importer = io.ScreamImporter(self)
124
+
125
+ def _print_all(self):
126
+ """Prints all results using an exporter."""
127
+ exporter = io.Exporter(self)
128
+ exporter.print()
129
+
130
+ def _read_in_data_from_csv(self):
131
+ """Placeholder function for reading data from CSV files."""
132
+
133
+ def _calculate_peak_intensities(self):
134
+ """Calculates peak intensities based on fitting methods."""
135
+ if self.props.prefit:
136
+ print(
137
+ f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}: Start prefit."
138
+ )
139
+ self._set_prefitter()
140
+ result = self.fitter.fit()
141
+ self.lmfit_result_handler.prefit = result
142
+ self._update_line_broadening(result)
143
+ if "individual" == self.props.spectrum_fit_type:
144
+ print(
145
+ f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}: Start individual fit."
146
+ )
147
+ self._set_single_fitter()
148
+ result = self.fitter.fit()
149
+ self.lmfit_result_handler.global_fit = result
150
+ self._get_intensities(result)
151
+ if "global" == self.props.spectrum_fit_type:
152
+ print(
153
+ f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}: Start global fit."
154
+ )
155
+ self._set_global_fitter()
156
+ result = self.fitter.fit()
157
+ self.lmfit_result_handler.global_fit = result
158
+ self._get_intensities(result)
159
+
160
+ def _start_buildup_fit(self):
161
+ """Performs buildup fitting using the appropriate fitter classes."""
162
+ fitter_classes = {
163
+ "biexponential": utils.BiexpFitter,
164
+ "biexponential_with_offset": utils.BiexpFitterWithOffset,
165
+ "exponential": utils.ExpFitter,
166
+ "exponential_with_offset": utils.ExpFitterWithOffset,
167
+ "streched_exponential": utils.StrechedExponentialFitter,
168
+ }
169
+
170
+ for b_type in self.props.buildup_types:
171
+ fitter_class = fitter_classes.get(b_type)
172
+ if fitter_class:
173
+ fitter = fitter_class(self)
174
+ self.lmfit_result_handler.buildup_fit[b_type] = (
175
+ fitter.perform_fit()
176
+ )
177
+
178
+ def _set_prefitter(self):
179
+ """Sets up a pre-fitter."""
180
+ self.fitter = utils.Prefitter(self)
181
+
182
+ def _set_single_fitter(self):
183
+ """Sets up a single-spectrum fitter."""
184
+ self.fitter = utils.IndependentFitter(self)
185
+
186
+ def _set_global_fitter(self):
187
+ """Sets up a global fitter for all spectra."""
188
+ self.fitter = utils.GlobalFitter(self)
189
+
190
+ def _get_intensities(self, result):
191
+ """Extracts intensity values from the fitting results."""
192
+ if isinstance(
193
+ self.fitter, (utils.IndependentFitter, utils.GlobalFitter)
194
+ ):
195
+ for peak in self.peak_list:
196
+ peak.buildup_vals = (result, self.spectra)
197
+
198
+ def _update_line_broadening(self, result):
199
+ """Updates line broadening values based on fitting results."""
200
+ for peak in self.peak_list:
201
+ peak.line_broadening = {
202
+ lw: {
203
+ "min": result.params.get(
204
+ f"{peak.peak_label}_{lw}_0"
205
+ ).value
206
+ * 0.9,
207
+ "max": result.params.get(
208
+ f"{peak.peak_label}_{lw}_0"
209
+ ).value
210
+ * 1.1,
211
+ }
212
+ for lw in ["sigma", "gamma"]
213
+ if f"{peak.peak_label}_{lw}_0" in result.params
214
+ }
215
+
216
+
217
+ class Spectra:
218
+ """
219
+ Represents spectral data for NMR (Nuclear Magnetic Resonance) experiments.
220
+
221
+ Attributes
222
+ ----------
223
+ number_of_scans : list of int or None
224
+ The number of scans performed in the NMR experiment.
225
+ tpol : float or None
226
+ The polarization time used in the experiment.
227
+ x_axis : array-like or None
228
+ The x-axis values representing frequency domain data data.
229
+ y_axis : array-like or None
230
+ The y-axis values representing intensity or amplitude data.
231
+
232
+ """
233
+
234
+ def __init__(self):
235
+ """Initializes Spectra attributes."""
236
+ self.number_of_scans = None
237
+ self.tpol = None
238
+ self.x_axis = None
239
+ self.y_axis = None
240
+
241
+
242
+ class Peak:
243
+ """
244
+ Represents a peak with its properties.
245
+
246
+ Such as peak center, peak label, fitting group, fitting type,
247
+ peak sign, line broadening, and buildup values.
248
+
249
+ """
250
+
251
+ def __init__(self):
252
+ """Initializes a Peak object with default values set to None."""
253
+ self._peak_center = None
254
+ self._peak_label = None
255
+ self._fitting_type = None
256
+ self._peak_sign = None
257
+ self._line_broadening = None
258
+ self._line_broadening_init = None
259
+ self._buildup_vals = None
260
+
261
+ def __str__(self):
262
+ """
263
+ Returns a formatted string representation of the peak.
264
+
265
+ :return: A string describing the peak's attributes.
266
+ """
267
+ return (
268
+ f"Peak center: {self.peak_center}\n"
269
+ f"Peak label: {self.peak_label}\n"
270
+ f"Peak shape: {self.fitting_type}\n"
271
+ f"Peak sign: {self.peak_sign}\n"
272
+ f"During the prefit stage, variables are permitted to vary within "
273
+ f"the following ranges:\n"
274
+ f" {self._format_fitting_range('prefit')}"
275
+ f"During the main analysis stage, variables are permitted to vary "
276
+ f"within the following ranges:\n"
277
+ f" {self._format_fitting_range('')}"
278
+ )
279
+
280
+ def _format_fitting_range(self, fit_type):
281
+ a_max = "0 and inf" if self.peak_sign == "+" else "-inf and 0"
282
+ lb = ""
283
+ line_broadening = (
284
+ self._line_broadening_init
285
+ if fit_type == "prefit"
286
+ else self._line_broadening
287
+ )
288
+ for keys in line_broadening:
289
+ lb += (
290
+ f"\t{keys}:\t\t\tBetween {line_broadening[keys]['min']} ppm"
291
+ f" and {line_broadening[keys]['max']} ppm.\n"
292
+ )
293
+ return (
294
+ f"\tCenter (µ):\t\tBetween {self.peak_center-1} ppm and"
295
+ f" {self.peak_center+1} ppm.\n"
296
+ f"\tAmplitude (A):\tBetween {a_max}.\n"
297
+ f"{lb}"
298
+ )
299
+
300
+ @property
301
+ def buildup_vals(self) -> list:
302
+ """
303
+ Gets the buildup values.
304
+
305
+ :return: A list of buildup values.
306
+ """
307
+ return self._buildup_vals
308
+
309
+ @buildup_vals.setter
310
+ def buildup_vals(self, args):
311
+ """
312
+ Sets the buildup values.
313
+
314
+ :param args: Tuple containing result and spectra.
315
+ """
316
+ result, spectra = args
317
+ self._buildup_vals = BuildupList()
318
+ self._buildup_vals.set_vals(result, spectra, self.peak_label)
319
+
320
+ @property
321
+ def line_broadening(self) -> str:
322
+ """
323
+ Gets the line broadening parameters.
324
+
325
+ :return: A dictionary representing line broadening values.
326
+ """
327
+ return self._line_broadening
328
+
329
+ @line_broadening.setter
330
+ def line_broadening(self, value):
331
+ """
332
+ Sets the line broadening parameters after validation.
333
+
334
+ :param value: Dictionary containing line broadening parameters.
335
+ """
336
+ allowed_values = ["sigma", "gamma"]
337
+ inner_allowed_values = ["min", "max"]
338
+ self._check_if_value_is_dict(value)
339
+ self._check_for_invalid_keys(value, allowed_values)
340
+ self._check_for_invalid_dict(value)
341
+ self._check_for_invalid_inner_keys(value, inner_allowed_values)
342
+ params = self._set_init_params()
343
+ self._overwrite_init_params(
344
+ value, allowed_values, inner_allowed_values, params
345
+ )
346
+
347
+ self._line_broadening = params
348
+ if self._line_broadening_init is None:
349
+ self._line_broadening_init = params
350
+
351
+ @property
352
+ def peak_sign(self) -> str:
353
+ """
354
+ Gets the peak sign.
355
+
356
+ :return: The peak sign ('+' or '-').
357
+ """
358
+ return self._peak_sign
359
+
360
+ @peak_sign.setter
361
+ def peak_sign(self, value):
362
+ """
363
+ Sets the peak sign after validation.
364
+
365
+ :param value: A string representing the peak sign ('+' or '-').
366
+ """
367
+ allowed_values = {"+", "-"}
368
+ if not isinstance(value, str):
369
+ raise TypeError(
370
+ f"'peak_sign' must be of type 'str', but got {type(value)}."
371
+ )
372
+ if value not in allowed_values:
373
+ raise ValueError(
374
+ f"'peak_sign' must be one of {sorted(allowed_values)}."
375
+ )
376
+ self._peak_sign = value
377
+
378
+ @property
379
+ def fitting_type(self) -> str:
380
+ """
381
+ Gets the peak fitting type.
382
+
383
+ :return: The fitting type as a string.
384
+ """
385
+ return self._fitting_type
386
+
387
+ @fitting_type.setter
388
+ def fitting_type(self, value):
389
+ """
390
+ Sets the peak fitting type after validation.
391
+
392
+ :param value: A string representing the fitting type.
393
+ """
394
+ allowed_values = {"voigt", "lorentz", "gauss"}
395
+ if not isinstance(value, str):
396
+ raise TypeError(
397
+ f"'fitting_type' must be of type 'str', but got {type(value)}."
398
+ )
399
+ if value not in allowed_values:
400
+ raise ValueError(
401
+ f"'fitting_type' must be one of {sorted(allowed_values)}."
402
+ )
403
+ self._fitting_type = value
404
+
405
+ @property
406
+ def peak_center(self) -> (int, float):
407
+ """
408
+ Gets the peak center.
409
+
410
+ :return: The peak center as an integer or float.
411
+ """
412
+ return self._peak_center
413
+
414
+ @peak_center.setter
415
+ def peak_center(self, value):
416
+ """
417
+ Sets the peak center after validation.
418
+
419
+ :param value: An integer or float representing the peak center.
420
+ """
421
+ if not isinstance(value, (int, float)):
422
+ raise TypeError(
423
+ f"'peak_center' must be of type 'int' or 'float', but got {type(value)}."
424
+ )
425
+ self._peak_center = float(value)
426
+
427
+ @property
428
+ def peak_label(self) -> str:
429
+ """
430
+ Get or set the label for a peak.
431
+
432
+ Returns
433
+ -------
434
+ str: The current peak label.
435
+
436
+ """
437
+ return self._peak_label
438
+
439
+ @peak_label.setter
440
+ def peak_label(self, value):
441
+ """
442
+ Set the label for a peak.
443
+
444
+ If an empty string is provided, a default label in the format
445
+ 'Peak_at_<center>_ppm' is generated, where <center> is the integer
446
+ value of `self.peak_center` (with '-' replaced by 'm').
447
+
448
+ Args
449
+ ----
450
+ value (str): The label to assign to the peak.
451
+
452
+ Raises
453
+ ------
454
+ TypeError: If the provided value is not a string.
455
+
456
+ """
457
+ if not isinstance(value, str):
458
+ raise TypeError(
459
+ f"'peak_label' must be of type 'str', but got {type(value)}."
460
+ )
461
+ if value == "":
462
+ name = str(int(self.peak_center)).replace("-", "m")
463
+ value = f"Peak_at_{name}_ppm"
464
+ self._peak_label = value
465
+
466
+ def _return_default_dict(self):
467
+ """
468
+ Returns the default dictionary for line broadening values.
469
+
470
+ :return: A dictionary with default values for 'sigma' and 'gamma'.
471
+ """
472
+ return {
473
+ "sigma": {"min": 0, "max": 3},
474
+ "gamma": {"min": 0, "max": 3},
475
+ }
476
+
477
+ def _check_if_value_is_dict(self, value):
478
+ """
479
+ Checks if the given value is a dictionary.
480
+
481
+ :param value: The value to check.
482
+ """
483
+ if not isinstance(value, dict):
484
+ raise TypeError(
485
+ f"'line_broadening' must be a 'dict', but got {type(value)}."
486
+ )
487
+
488
+ def _check_for_invalid_keys(self, value, allowed_values):
489
+ invalid_keys = [
490
+ key for key in value.keys() if key not in allowed_values
491
+ ]
492
+ if invalid_keys:
493
+ raise ValueError(
494
+ f"Invalid keys found in the dictionary: {invalid_keys}. "
495
+ f"Allowed keys are: {allowed_values}."
496
+ )
497
+
498
+ def _check_for_invalid_dict(self, value):
499
+ if not all(isinstance(v, dict) for v in value.values()):
500
+ raise TypeError(
501
+ "Each value in the 'line_broadening' dictionary must be of type 'dict'."
502
+ )
503
+
504
+ def _check_for_invalid_inner_keys(self, value, inner_allowed_values):
505
+ for key, inner_dict in value.items():
506
+ invalid_inner_keys = [
507
+ inner_key
508
+ for inner_key in inner_dict.keys()
509
+ if inner_key not in inner_allowed_values
510
+ ]
511
+ if invalid_inner_keys:
512
+ raise ValueError(
513
+ f"Invalid inner keys for '{key}': {invalid_inner_keys}. "
514
+ f"Allowed inner keys are: {inner_allowed_values}."
515
+ )
516
+
517
+ def _set_init_params(self):
518
+ """
519
+ Sets initial parameters based on fitting type.
520
+
521
+ :return: A dictionary of initial parameters.
522
+ """
523
+ params = self._return_default_dict()
524
+ if self.fitting_type == "gauss":
525
+ params = {"sigma": params["sigma"]}
526
+ elif self.fitting_type == "lorentz":
527
+ params = {"gamma": params["gamma"]}
528
+ return params
529
+
530
+ def _overwrite_init_params(
531
+ self, value, allowed_values, inner_allowed_values, params
532
+ ):
533
+ """
534
+ Overwrites initial parameters with provided values.
535
+
536
+ :param value: Dictionary containing new parameter values.
537
+ :param allowed_values: List of allowed outer dictionary keys.
538
+ :param inner_allowed_values: List of allowed inner dictionary keys.
539
+ :param params: Dictionary of existing parameters to be updated.
540
+
541
+ """
542
+ for key in allowed_values:
543
+ if key in value:
544
+ for inner_key in inner_allowed_values:
545
+ inner_value = value[key].get(inner_key)
546
+ if inner_value is not None:
547
+ if not isinstance(inner_value, (int, float)):
548
+ raise TypeError(
549
+ f"'{inner_key}' value must be an 'int' or 'float', "
550
+ f"but got {type(inner_value)}."
551
+ )
552
+ params[key][inner_key] = float(inner_value)
553
+ return params
554
+
555
+
556
+ class BuildupList:
557
+ """
558
+ Represents a list of buildup values used for fitting delay times and intensities.
559
+
560
+ Attributes
561
+ ----------
562
+ tpol (list): List of delay times.
563
+ intensity (list): List of intensity values.
564
+
565
+ """
566
+
567
+ def __init__(self):
568
+ """Initializes an empty BuildupList with None values for attributes."""
569
+ self.tpol = None
570
+ self.intensity = None
571
+
572
+ def __str__(self):
573
+ """
574
+ Returns a formatted string representation of the buildup values.
575
+
576
+ Returns
577
+ -------
578
+ str: A formatted string listing delay times and intensities.
579
+
580
+ """
581
+ return (
582
+ "Parameters for buildup fitting:\nDelay times:\t"
583
+ + "\t\t\t".join(str(x) for x in self.tpol)
584
+ + "\nIntegral:\t"
585
+ + "\t".join(str(x) for x in self.intensity)
586
+ )
587
+
588
+ def set_vals(self, result, spectra, label):
589
+ """
590
+ Sets buildup values using the result parameters and spectra.
591
+
592
+ Attributes
593
+ ----------
594
+ result (object): Fitted parameter values used for calculating buildup.
595
+ spectra (list): Spectrum objects used with result to compute buildup.
596
+ label (str): Peak label used to filter relevant parameters in result.
597
+
598
+ """
599
+ self._set_tpol(spectra)
600
+ self._set_intensity(result, label, spectra)
601
+ self._sort_lists()
602
+
603
+ def _set_tpol(self, spectra):
604
+ self.tpol = [s.tpol for s in spectra]
605
+
606
+ def _set_intensity(self, result, label, spectra):
607
+ last_digid = None
608
+ self.intensity = []
609
+ val_list = []
610
+ for param in result.params:
611
+ if label in param:
612
+ if last_digid != param.split("_")[-1]:
613
+ if val_list:
614
+ self.intensity.append(
615
+ self._calc_integral(
616
+ val_list, spectra[int(last_digid)]
617
+ )
618
+ )
619
+ last_digid = param.split("_")[-1]
620
+ val_list = []
621
+ val_list.append(float(result.params[param].value))
622
+ if param.split("_")[-2] == "gamma":
623
+ val_list.append("gamma")
624
+ self.intensity.append(
625
+ self._calc_integral(val_list, spectra[int(last_digid)])
626
+ )
627
+
628
+ def _calc_integral(self, val_list, spectrum):
629
+ """
630
+ Computes the numerical integral of the simulated spectrum.
631
+
632
+ Args
633
+ ----
634
+ val_list (list): List of values used for peak calculation.
635
+ spectrum (object): Spectrum object containing x-axis data.
636
+
637
+ Returns
638
+ -------
639
+ float: The computed integral of the spectrum.
640
+
641
+ """
642
+ simspec = [0 for _ in range(len(spectrum.x_axis))]
643
+ simspec = functions.calc_peak(spectrum.x_axis, simspec, val_list)
644
+ return np.trapz(simspec)
645
+
646
+ def _sort_lists(self):
647
+ """
648
+ Sorting method.
649
+
650
+ Sorts the delay times and corresponding intensity values in
651
+ ascending order of delay times.
652
+ """
653
+ self.tpol, self.intensity = map(
654
+ list, zip(*sorted(zip(self.tpol, self.intensity)))
655
+ )