augmentrum 0.0.1__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.
Files changed (42) hide show
  1. augmentrum/__init__.py +23 -0
  2. augmentrum/augmentation/__init__.py +32 -0
  3. augmentrum/augmentation/amplitude_scaling.py +170 -0
  4. augmentrum/augmentation/apodization.py +275 -0
  5. augmentrum/augmentation/artificial_peaks.py +334 -0
  6. augmentrum/augmentation/baseline_augmentation.py +544 -0
  7. augmentrum/augmentation/eddy_current.py +278 -0
  8. augmentrum/augmentation/gaussian_noise.py +211 -0
  9. augmentrum/augmentation/line_broadening.py +179 -0
  10. augmentrum/augmentation/phase_frequency.py +259 -0
  11. augmentrum/augmentation/residual_water.py +264 -0
  12. augmentrum/augmentation/spatial_augmentations.py +859 -0
  13. augmentrum/augmentation/spurious_echoes.py +378 -0
  14. augmentrum/augmentation/zero_fill.py +163 -0
  15. augmentrum/core/__init__.py +10 -0
  16. augmentrum/core/augmentrum.py +1172 -0
  17. augmentrum/core/base_module.py +423 -0
  18. augmentrum/core/dataset_utils.py +417 -0
  19. augmentrum/core/nifti_mrs_plus.py +1079 -0
  20. augmentrum/core/pipeline.py +358 -0
  21. augmentrum/dataset/brainbeats.py +537 -0
  22. augmentrum/dataset/cows.py +302 -0
  23. augmentrum/dataset/fmrsinpain.py +208 -0
  24. augmentrum/processing/bicubic_interpolator.py +296 -0
  25. augmentrum/processing/nifti_raw_processor.py +389 -0
  26. augmentrum/processing/tricubic_interpolator.py +293 -0
  27. augmentrum/processing/utils.py +414 -0
  28. augmentrum/resources/__init__.py +14 -0
  29. augmentrum/resources/scans/__init__.py +0 -0
  30. augmentrum/sampling/coil_average_sampler.py +274 -0
  31. augmentrum/sampling/kspace_reconstructor.py +275 -0
  32. augmentrum/sampling/kspace_sampling.py +2562 -0
  33. augmentrum/sampling/subject_splitter.py +131 -0
  34. augmentrum/utils/__init__.py +10 -0
  35. augmentrum/utils/philips.py +90 -0
  36. augmentrum/utils/plotting.py +651 -0
  37. augmentrum/utils/tensor_ops.py +203 -0
  38. augmentrum-0.0.1.dist-info/METADATA +189 -0
  39. augmentrum-0.0.1.dist-info/RECORD +42 -0
  40. augmentrum-0.0.1.dist-info/WHEEL +5 -0
  41. augmentrum-0.0.1.dist-info/licenses/LICENSE +202 -0
  42. augmentrum-0.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,334 @@
1
+ ####################################################################################################
2
+ # artificial_peaks.py #
3
+ ####################################################################################################
4
+ # #
5
+ # Authors: K. C. Igwe (kci2104@columbia.edu) #
6
+ # J. P. Merkofer (j.p.merkofer@tue.nl) #
7
+ # #
8
+ # Created: 2026-02-07 #
9
+ # #
10
+ # Purpose: Adds artificial contaminant peaks at specified frequencies in MRS data. #
11
+ # #
12
+ ####################################################################################################
13
+
14
+ import numpy as np
15
+ from typing import Optional, List, Dict
16
+ from augmentrum.core.base_module import BaseModule
17
+ from augmentrum.core.nifti_mrs_plus import Backend
18
+ from augmentrum.core.nifti_mrs_plus import Backend, NIfTI_MRS_Plus
19
+ from augmentrum.utils.tensor_ops import fft, ifft, fftshift, ifftshift, to_numpy, match_backend
20
+
21
+
22
+ class ArtificialPeaks(BaseModule):
23
+ """
24
+ Add artificial contaminant peaks to MRS data.
25
+
26
+ Adds Lorentzian, Gaussian, or Voigt peaks at specified ppm positions
27
+ in the frequency domain to simulate contamination or additional metabolites.
28
+
29
+ Parameters
30
+ ----------
31
+ peaks : list of dicts
32
+ Each dict contains:
33
+ - ppm: Peak position in ppm
34
+ - amp: Amplitude as fraction of main spectrum peak (0.0-1.0)
35
+ - phase_deg: Complex phase in degrees
36
+ - lb_hz: Lorentzian FWHM in Hz (0 for none)
37
+ - gb_hz: Gaussian FWHM in Hz (0 for none)
38
+ If both lb_hz and gb_hz > 0, creates Voigt peak
39
+ ref_ppm : float
40
+ Reference ppm for axis calculation (default: 4.7)
41
+ amp_mode : str
42
+ How to calculate reference amplitude: 'real' or 'abs' (default: 'real')
43
+
44
+ Examples
45
+ --------
46
+ >>> # Add single Lorentzian peak at 3.0 ppm
47
+ >>> peaks = ArtificialPeaks(peaks=[
48
+ ... {'ppm': 3.0, 'amp': 0.1, 'phase_deg': 0.0, 'lb_hz': 5.0, 'gb_hz': 0.0}
49
+ ... ])
50
+ >>> result_data, _ = peaks(nifti_plus, None)
51
+ """
52
+
53
+ SUPPORTED_BACKENDS = [] # Supports all backends
54
+
55
+ def __init__(self, peaks: List[Dict] = None, ref_ppm: float = 4.7, amp_mode: str = 'real'):
56
+ """Initialize artificial peaks module."""
57
+ if peaks is None:
58
+ peaks = [{'ppm': 3.0, 'amp': 0.05, 'phase_deg': 0.0, 'lb_hz': 5.0, 'gb_hz': 0.0}]
59
+
60
+ super().__init__(peaks=peaks, ref_ppm=ref_ppm, amp_mode=amp_mode)
61
+
62
+ self.peaks = peaks
63
+ self.ref_ppm = ref_ppm
64
+ self.amp_mode = amp_mode
65
+
66
+ def process_nifti_list(self, data_list: List, water_list: Optional[List] = None, **kwargs):
67
+ """
68
+ Add artificial peaks to list of NIFTI_MRS objects.
69
+
70
+ Args:
71
+ data_list: List of NIFTI_MRS objects
72
+ water_list: Optional list of water reference NIFTI_MRS objects
73
+ **kwargs: Additional arguments
74
+
75
+ Returns:
76
+ Tuple of (processed_data_list, processed_water_list)
77
+ """
78
+ processed_data = []
79
+
80
+ for nifti in data_list:
81
+ # Get FID data
82
+ fid = nifti[:]
83
+
84
+ # Get parameters
85
+ sw_hz = 1.0 / nifti.dwelltime
86
+ sf_mhz = nifti.spectrometer_frequency[0]
87
+
88
+ # Add peaks
89
+ fid_with_peaks = self._add_peaks(fid, sw_hz, sf_mhz)
90
+
91
+ # Update NIFTI_MRS data
92
+ nifti[:] = fid_with_peaks
93
+ processed_data.append(nifti)
94
+
95
+ return processed_data, water_list
96
+
97
+ def process_tensor(self, data_array, water_array=None, backend=None, **kwargs):
98
+ """
99
+ Add artificial peaks to tensor/array data (**any backend**).
100
+
101
+ FFT/IFFT are done **natively** via ``tensor_ops`` (preserves gradients).
102
+ Peak shapes and amplitude references are computed in NumPy then
103
+ promoted to the target backend with ``match_backend``.
104
+
105
+ Args:
106
+ data_array: Input tensor of shape ``(batch, ..., n_points)``
107
+ water_array: Optional water reference (unchanged)
108
+ backend: Backend enum (unused)
109
+ **kwargs: Must contain ``'sw_hz'`` and ``'sf_mhz'``
110
+
111
+ Returns:
112
+ Tuple of (processed_data, water_array)
113
+ """
114
+ sw_hz = kwargs.get('sw_hz')
115
+ sf_mhz = kwargs.get('sf_mhz')
116
+ if sw_hz is None or sf_mhz is None:
117
+ raise ValueError("ArtificialPeaks.process_tensor requires 'sw_hz' and 'sf_mhz' in kwargs")
118
+
119
+ N = data_array.shape[-1]
120
+ n_batch = int(np.prod(data_array.shape[:-1]))
121
+
122
+ # 1. Spectrum (backend-native FFT — preserves gradients)
123
+ spec = fftshift(ifft(data_array)) # shape = original_shape
124
+
125
+ # 2. ppm axis (numpy — coordinate, no gradients needed)
126
+ dt = 1.0 / float(sw_hz)
127
+ freq_hz = np.fft.fftshift(np.fft.fftfreq(N, d=dt))
128
+ ppm = self.ref_ppm - freq_hz / float(sf_mhz)
129
+
130
+ # 3. Amplitude reference and peak shapes (numpy, per-FID scalar pull)
131
+ spec_np = to_numpy(spec).reshape(n_batch, N)
132
+ contam_np = np.zeros((n_batch, N), dtype=np.complex128)
133
+
134
+ for i in range(n_batch):
135
+ if self.amp_mode == 'abs':
136
+ peak_ref = float(np.max(np.abs(spec_np[i]))) or 1.0
137
+ else:
138
+ peak_ref = float(np.max(np.abs(spec_np[i].real))) or 1.0
139
+
140
+ contam = np.zeros(N, dtype=np.complex128)
141
+ for p in self.peaks:
142
+ ppm0 = float(p['ppm'])
143
+ amp_frac = float(p.get('amp', p.get('amplitude', 0.05)))
144
+ phase_deg = float(p.get('phase_deg', 0.0))
145
+ lb_hz = float(p.get('lb_hz', 0.0))
146
+ gb_hz = float(p.get('gb_hz', 0.0))
147
+
148
+ if lb_hz > 0 and gb_hz > 0:
149
+ shape = self._voigt(ppm, ppm0, lb_hz, gb_hz, sf_mhz)
150
+ elif lb_hz > 0:
151
+ shape = self._lorentzian(ppm, ppm0, lb_hz, sf_mhz)
152
+ elif gb_hz > 0:
153
+ shape = self._gaussian(ppm, ppm0, gb_hz, sf_mhz)
154
+ else:
155
+ continue
156
+
157
+ contam += (amp_frac * peak_ref) * shape * np.exp(1j * np.deg2rad(phase_deg))
158
+
159
+ contam_np[i] = contam
160
+
161
+ contam_np = contam_np.reshape(data_array.shape)
162
+
163
+ # 4. Add contamination in spectral domain (backend-native)
164
+ spec_aug = spec + match_backend(contam_np, spec)
165
+
166
+ # 5. Back to FID (backend-native IFFT)
167
+ return fft(ifftshift(spec_aug)), water_array
168
+
169
+ def _add_peaks(self, fid: np.ndarray, sw_hz: float, sf_mhz: float) -> np.ndarray:
170
+ """
171
+ Add artificial peaks to FID data.
172
+
173
+ Args:
174
+ fid: Input FID data (shape: x, y, z, points, [coils], [averages], ...)
175
+ sw_hz: Spectral width in Hz
176
+ sf_mhz: Spectrometer frequency in MHz
177
+
178
+ Returns:
179
+ FID with peaks added
180
+ """
181
+ # In MRS data, dimension 3 (index 3) is typically the FID points
182
+ # Shape is typically: (x, y, z, points, coils, averages, ...)
183
+ original_shape = fid.shape
184
+
185
+ # Find the FID points dimension (should be dim 3, index 3)
186
+ # For standard MRS: shape is (1, 1, 1, points, coils, averages)
187
+ if len(original_shape) >= 4:
188
+ # FID points are at index 3 (4th dimension)
189
+ n_points = original_shape[3]
190
+
191
+ # Move FID dimension to the end for easier processing
192
+ # From: (x, y, z, points, coils, avg) -> (x, y, z, coils, avg, points)
193
+ fid_moved = np.moveaxis(fid, 3, -1)
194
+ moved_shape = fid_moved.shape
195
+
196
+ # Reshape to 2D: (all_other_dims, points)
197
+ fid_2d = fid_moved.reshape(-1, n_points)
198
+ result = np.zeros_like(fid_2d)
199
+
200
+ # Process each FID
201
+ for i in range(fid_2d.shape[0]):
202
+ fid_1d = fid_2d[i]
203
+ fid_with_peaks = self._add_peaks_1d(fid_1d, sw_hz, sf_mhz)
204
+ result[i] = fid_with_peaks
205
+
206
+ # Reshape back and move FID dimension back to position 3
207
+ result_moved = result.reshape(moved_shape)
208
+ result_final = np.moveaxis(result_moved, -1, 3)
209
+
210
+ return result_final
211
+ else:
212
+ # Simple 1D case or legacy format - last dimension is FID points
213
+ N = original_shape[-1]
214
+ fid_2d = fid.reshape(-1, N)
215
+ result = np.zeros_like(fid_2d)
216
+
217
+ for i in range(fid_2d.shape[0]):
218
+ fid_1d = fid_2d[i]
219
+ fid_with_peaks = self._add_peaks_1d(fid_1d, sw_hz, sf_mhz)
220
+ result[i] = fid_with_peaks
221
+
222
+ return result.reshape(original_shape)
223
+
224
+ def _add_peaks_1d(self, fid: np.ndarray, sw_hz: float, sf_mhz: float) -> np.ndarray:
225
+ """
226
+ Add peaks to 1D FID.
227
+
228
+ Args:
229
+ fid: 1D FID data
230
+ sw_hz: Spectral width in Hz
231
+ sf_mhz: Spectrometer frequency in MHz
232
+
233
+ Returns:
234
+ FID with peaks
235
+ """
236
+ # IFFT to frequency domain (correct MRS convention)
237
+ spec = np.fft.fftshift(np.fft.ifft(fid))
238
+
239
+ # Create ppm axis
240
+ n = fid.size
241
+ dt = 1.0 / float(sw_hz)
242
+ freq_hz = np.fft.fftshift(np.fft.fftfreq(n, d=dt))
243
+ # Legacy formula: ppm_offset = freq_hz_offset / larmor_freq_MHz
244
+ ppm = self.ref_ppm - freq_hz / float(sf_mhz)
245
+
246
+ # Calculate reference amplitude
247
+ if self.amp_mode == 'abs':
248
+ peak_ref = np.max(np.abs(spec)) or 1.0
249
+ else:
250
+ peak_ref = np.max(np.abs(spec.real)) or 1.0
251
+
252
+ # Generate contamination
253
+ contam = np.zeros_like(spec, dtype=np.complex128)
254
+
255
+ for p in self.peaks:
256
+ ppm0 = float(p['ppm'])
257
+ # Support both 'amp' and 'amplitude' keys
258
+ amp_frac = float(p.get('amp', p.get('amplitude', 0.05)))
259
+ phase_deg = float(p.get('phase_deg', 0.0))
260
+ lb_hz = float(p.get('lb_hz', 0.0))
261
+ gb_hz = float(p.get('gb_hz', 0.0))
262
+
263
+ # Generate peak shape
264
+ if lb_hz > 0 and gb_hz > 0:
265
+ shape = self._voigt(ppm, ppm0, lb_hz, gb_hz, sf_mhz)
266
+ elif lb_hz > 0:
267
+ shape = self._lorentzian(ppm, ppm0, lb_hz, sf_mhz)
268
+ elif gb_hz > 0:
269
+ shape = self._gaussian(ppm, ppm0, gb_hz, sf_mhz)
270
+ else:
271
+ continue # No width, skip
272
+
273
+ # Add to contamination
274
+ phase = np.exp(1j * np.deg2rad(phase_deg))
275
+ peak_contam = (amp_frac * peak_ref) * shape * phase
276
+ contam += peak_contam
277
+
278
+ # Add contamination to spectrum
279
+ spec_aug = spec + contam
280
+
281
+ # Back to time domain (FFT to invert IFFT)
282
+ return np.fft.fft(np.fft.ifftshift(spec_aug))
283
+
284
+ @staticmethod
285
+ def _lorentzian(ppm: np.ndarray, ppm0: float, fwhm_hz: float, sf_mhz: float) -> np.ndarray:
286
+ """Generate Lorentzian peak shape."""
287
+ if fwhm_hz <= 0:
288
+ return np.zeros_like(ppm)
289
+
290
+ # Convert Hz to ppm: divide by sf_mhz (which is in MHz)
291
+ # This gives: Hz / MHz = ppm (since 1 MHz = 1e6 Hz, the ratio is in ppm)
292
+ fwhm_ppm = fwhm_hz / float(sf_mhz)
293
+ x = ppm - ppm0
294
+ hw = 0.5 * fwhm_ppm
295
+ L = (hw / (x**2 + hw**2)) / np.pi
296
+ return L / (L.max() if L.max() > 0 else 1.0)
297
+
298
+ @staticmethod
299
+ def _gaussian(ppm: np.ndarray, ppm0: float, fwhm_hz: float, sf_mhz: float) -> np.ndarray:
300
+ """Generate Gaussian peak shape."""
301
+ if fwhm_hz <= 0:
302
+ return np.zeros_like(ppm)
303
+
304
+ # Convert Hz to ppm: divide by sf_mhz (which is in MHz)
305
+ fwhm_ppm = fwhm_hz / float(sf_mhz)
306
+ x = ppm - ppm0
307
+ G = np.exp(-4.0 * np.log(2.0) * (x**2) / (fwhm_ppm**2))
308
+ return G
309
+
310
+ @staticmethod
311
+ def _voigt(ppm: np.ndarray, ppm0: float, lb_hz: float, gb_hz: float, sf_mhz: float) -> np.ndarray:
312
+ """Generate Voigt peak shape (product of Lorentzian and Gaussian)."""
313
+ if lb_hz <= 0 and gb_hz <= 0:
314
+ return np.zeros_like(ppm)
315
+
316
+ # Don't call the normalized functions - build them here
317
+ if lb_hz > 0:
318
+ fwhm_ppm_l = lb_hz / float(sf_mhz)
319
+ x = ppm - ppm0
320
+ hw = 0.5 * fwhm_ppm_l
321
+ L = (hw / (x**2 + hw**2)) / np.pi
322
+ else:
323
+ L = 1.0
324
+
325
+ if gb_hz > 0:
326
+ fwhm_ppm_g = gb_hz / float(sf_mhz)
327
+ x = ppm - ppm0
328
+ G = np.exp(-4.0 * np.log(2.0) * (x**2) / (fwhm_ppm_g**2))
329
+ else:
330
+ G = 1.0
331
+
332
+ V = L * G
333
+ vmax = V.max() if isinstance(V, np.ndarray) else V
334
+ return V / (vmax if vmax > 0 else 1.0)