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
augmentrum/__init__.py ADDED
@@ -0,0 +1,23 @@
1
+ """
2
+ Augmentrum - A Data Augmentation Package for MR Spectroscopy
3
+
4
+ A modular Python framework designed to help researchers with limited in-vivo MRS data
5
+ create diverse, physically consistent datasets through flexible augmentation.
6
+ """
7
+
8
+ __version__ = "0.0.1"
9
+ __author__ = "John T. LaMaster, Julian P. Merkofer, Kay C. Igwe"
10
+ __email__ = "j.p.merkofer@tue.nl"
11
+
12
+ # Main API
13
+ from augmentrum.core import Augmentrum, NIfTI_MRS_Plus, Backend, BaseModule, AugmentationPipeline
14
+
15
+ __all__ = [
16
+ "__version__",
17
+ "Augmentrum",
18
+ "NIfTI_MRS_Plus",
19
+ "Backend",
20
+ "BaseModule",
21
+ "AugmentationPipeline",
22
+ ]
23
+
@@ -0,0 +1,32 @@
1
+ """
2
+ Augmentation modules for MRS data.
3
+ """
4
+
5
+ from augmentrum.augmentation.gaussian_noise import GaussianNoise
6
+ from augmentrum.augmentation.line_broadening import LineBroadening
7
+ from augmentrum.augmentation.baseline_augmentation import BaselineAugmentation
8
+ from augmentrum.augmentation.residual_water import ResidualWater
9
+ from augmentrum.augmentation.spurious_echoes import SpuriousEchoes
10
+ from augmentrum.augmentation.artificial_peaks import ArtificialPeaks
11
+ from augmentrum.augmentation.eddy_current import EddyCurrent
12
+ from augmentrum.augmentation.apodization import Apodization
13
+ from augmentrum.augmentation.phase_frequency import PhaseShift, FrequencyShift
14
+ from augmentrum.augmentation.amplitude_scaling import AmplitudeScaling
15
+ from augmentrum.augmentation.spatial_augmentations import SpatialAugmentations
16
+ from augmentrum.augmentation.zero_fill import ZeroFill
17
+
18
+ __all__ = [
19
+ 'GaussianNoise',
20
+ 'LineBroadening',
21
+ 'BaselineAugmentation',
22
+ 'ResidualWater',
23
+ 'SpuriousEchoes',
24
+ 'ArtificialPeaks',
25
+ 'EddyCurrent',
26
+ 'Apodization',
27
+ 'PhaseShift',
28
+ 'FrequencyShift',
29
+ 'AmplitudeScaling',
30
+ 'SpatialAugmentations',
31
+ 'ZeroFill',
32
+ ]
@@ -0,0 +1,170 @@
1
+ ####################################################################################################
2
+ # amplitude_scaling.py #
3
+ ####################################################################################################
4
+ # #
5
+ # Authors: K. C. Igwe (kcigwe@bu.edu) #
6
+ # J. P. Merkofer (j.p.merkofer@tue.nl) #
7
+ # #
8
+ # Created: 2026-02-08 #
9
+ # #
10
+ # Purpose: Amplitude scaling for MRS data augmentation. #
11
+ # Note: True metabolite-specific scaling requires basis functions (simulated data only). #
12
+ # #
13
+ ####################################################################################################
14
+
15
+
16
+ #*************#
17
+ # imports #
18
+ #*************#
19
+ from typing import List, Optional, Union, Tuple
20
+ import numpy as np
21
+ from fsl_mrs.core import NIFTI_MRS
22
+
23
+ from augmentrum.core.base_module import BaseModule
24
+ from augmentrum.core.nifti_mrs_plus import Backend
25
+ from augmentrum.utils.tensor_ops import match_backend
26
+
27
+
28
+ ####################################################################################################
29
+ # AmplitudeScaling Class #
30
+ ####################################################################################################
31
+ class AmplitudeScaling(BaseModule):
32
+ """
33
+ Apply amplitude scaling to MRS data.
34
+
35
+ **Note:** This performs global amplitude scaling. True metabolite-specific scaling
36
+ requires access to individual basis functions and is only applicable to simulated data.
37
+
38
+ For realistic metabolite-specific scaling:
39
+ - Fit in-vivo data to a basis set
40
+ - Scale individual metabolite components
41
+ - Reconstruct the FID
42
+
43
+ This module provides global scaling for demonstration/preprocessing purposes.
44
+
45
+ Parameters:
46
+ scale_factor: Scaling factor to apply. Can be:
47
+ - Single float: Fixed scaling
48
+ - Tuple (min, max): Random uniform sampling from range
49
+ Default: 1.0 (no scaling)
50
+ distribution: Distribution for random sampling ('uniform', 'normal')
51
+ Only used if scale_factor is a tuple
52
+ seed: Random seed for reproducibility
53
+
54
+ Examples:
55
+ >>> # Fixed scaling
56
+ >>> scaler = AmplitudeScaling(scale_factor=0.8)
57
+ >>>
58
+ >>> # Random scaling in range
59
+ >>> scaler = AmplitudeScaling(scale_factor=(0.7, 1.3))
60
+ >>>
61
+ >>> # Normal distribution around 1.0
62
+ >>> scaler = AmplitudeScaling(scale_factor=(0.9, 1.1), distribution='normal')
63
+ """
64
+
65
+ SUPPORTED_BACKENDS = [Backend.NIFTI_LIST, Backend.NUMPY, Backend.PYTORCH,
66
+ Backend.TENSORFLOW, Backend.JAX, Backend.KERAS]
67
+
68
+ def __init__(
69
+ self,
70
+ scale_factor: Union[float, Tuple[float, float]] = 1.0,
71
+ distribution: str = 'uniform',
72
+ seed: Optional[int] = None,
73
+ **kwargs
74
+ ):
75
+ """Initialize AmplitudeScaling module."""
76
+ super().__init__(**kwargs)
77
+
78
+ self.scale_factor = scale_factor
79
+ self.distribution = distribution
80
+ self.seed = seed
81
+ self.rng = np.random.default_rng(seed)
82
+
83
+ # Validate distribution
84
+ if distribution not in ['uniform', 'normal']:
85
+ raise ValueError(f"distribution must be 'uniform' or 'normal', got {distribution}")
86
+
87
+ def _get_scale_factor(self) -> float:
88
+ """Get scale factor (sample if range is provided)."""
89
+ if isinstance(self.scale_factor, (tuple, list)):
90
+ min_scale, max_scale = self.scale_factor
91
+
92
+ if self.distribution == 'uniform':
93
+ return self.rng.uniform(min_scale, max_scale)
94
+ elif self.distribution == 'normal':
95
+ # Use range as mean ± std
96
+ mean = (min_scale + max_scale) / 2.0
97
+ std = (max_scale - min_scale) / 4.0 # ~95% within range
98
+ return self.rng.normal(mean, std)
99
+ else:
100
+ return float(self.scale_factor)
101
+
102
+ def process_nifti_list(
103
+ self,
104
+ data_list: List[NIFTI_MRS],
105
+ water_list: Optional[List[NIFTI_MRS]] = None,
106
+ **kwargs
107
+ ) -> Tuple[List[NIFTI_MRS], Optional[List[NIFTI_MRS]]]:
108
+ """
109
+ Process list of NIFTI_MRS objects.
110
+
111
+ Args:
112
+ data_list: List of NIFTI_MRS objects
113
+ water_list: Optional list of water reference NIFTI_MRS objects
114
+
115
+ Returns:
116
+ Tuple of (scaled_data_list, water_list_unchanged)
117
+ """
118
+ scaled_data = []
119
+
120
+ for nifti in data_list:
121
+ # Get scale factor (sample if random)
122
+ scale = self._get_scale_factor()
123
+
124
+ # Get data
125
+ fid = nifti[:].copy()
126
+
127
+ # Apply scaling
128
+ fid_scaled = fid * scale
129
+
130
+ # Create new NIFTI_MRS with scaled data
131
+ nifti_scaled = nifti.copy()
132
+ nifti_scaled[:] = fid_scaled
133
+
134
+ scaled_data.append(nifti_scaled)
135
+
136
+ # Water references unchanged
137
+ return scaled_data, water_list
138
+
139
+ def process_tensor(
140
+ self,
141
+ data,
142
+ water=None,
143
+ **kwargs
144
+ ) -> Tuple:
145
+ """
146
+ Process tensor data.
147
+
148
+ Args:
149
+ data: Input tensor (batch, ...)
150
+ water: Optional water reference tensor
151
+
152
+ Returns:
153
+ Tuple of (scaled_data, water_unchanged)
154
+ """
155
+ # Get scale factor for each sample in batch
156
+ batch_size = data.shape[0]
157
+ scales = np.array([self._get_scale_factor() for _ in range(batch_size)])
158
+
159
+ # Reshape scales for broadcasting
160
+ scale_shape = [batch_size] + [1] * (data.ndim - 1)
161
+ scales = scales.reshape(scale_shape)
162
+
163
+ # Apply scaling
164
+ data_scaled = data * match_backend(scales, data)
165
+
166
+ return data_scaled, water
167
+
168
+ def __repr__(self):
169
+ return (f"AmplitudeScaling(scale_factor={self.scale_factor}, "
170
+ f"distribution='{self.distribution}')")
@@ -0,0 +1,275 @@
1
+ ####################################################################################################
2
+ # apodization.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: Implements FID truncation and exponential weighting (apodization) for MRS data. #
11
+ # #
12
+ ####################################################################################################
13
+
14
+ import numpy as np
15
+ from typing import Optional, List
16
+ from augmentrum.core.base_module import BaseModule
17
+ from augmentrum.core.nifti_mrs_plus import Backend, NIfTI_MRS_Plus
18
+ from fsl_mrs.core.nifti_mrs import gen_nifti_mrs
19
+ from augmentrum.utils.tensor_ops import match_backend
20
+
21
+
22
+ class Apodization(BaseModule):
23
+ """
24
+ Apply apodization (truncation or exponential weighting) to MRS data.
25
+
26
+ Supports two modes:
27
+ - 'truncate': Cut FID after specified number of points
28
+ - 'exponential': Apply exponential decay (Lorentzian broadening)
29
+
30
+ Parameters
31
+ ----------
32
+ mode : str
33
+ Apodization mode: 'truncate' or 'exponential' (default: 'exponential')
34
+ n_pts : int, optional
35
+ Number of points to keep (for truncation mode)
36
+ frac_pts : float, optional
37
+ Fraction of points to keep (for truncation mode), range [0, 1]
38
+ lb_hz : float, optional
39
+ Lorentzian broadening in Hz (for exponential mode)
40
+ auto_lb : bool
41
+ Auto-calculate lb_hz to preserve signal (default: False)
42
+ target_damp : float
43
+ Target damping factor for auto_lb (default: 0.01)
44
+ target_pts : int, optional
45
+ Target point where signal becomes mostly noise (for auto_lb)
46
+
47
+ Examples
48
+ --------
49
+ >>> # Truncation mode
50
+ >>> apod = Apodization(mode='truncate', n_pts=1024)
51
+ >>>
52
+ >>> # Exponential mode with manual lb
53
+ >>> apod = Apodization(mode='exponential', lb_hz=5.0)
54
+ >>>
55
+ >>> # Exponential mode with auto lb
56
+ >>> apod = Apodization(mode='exponential', auto_lb=True, target_pts=1024)
57
+ """
58
+
59
+ SUPPORTED_BACKENDS = [] # Supports all backends
60
+
61
+ def __init__(self, mode: str = 'exponential',
62
+ n_pts: Optional[int] = None,
63
+ frac_pts: Optional[float] = None,
64
+ lb_hz: Optional[float] = None,
65
+ auto_lb: bool = False,
66
+ target_damp: float = 0.01,
67
+ target_pts: Optional[int] = None):
68
+ """Initialize apodization module."""
69
+ super().__init__(mode=mode, n_pts=n_pts, frac_pts=frac_pts,
70
+ lb_hz=lb_hz, auto_lb=auto_lb,
71
+ target_damp=target_damp, target_pts=target_pts)
72
+
73
+ self.mode = mode.lower()
74
+ self.n_pts = n_pts
75
+ self.frac_pts = frac_pts
76
+ self.lb_hz = lb_hz
77
+ self.auto_lb = auto_lb
78
+ self.target_damp = target_damp
79
+ self.target_pts = target_pts
80
+
81
+ if self.mode not in ['truncate', 'exponential']:
82
+ raise ValueError(f"mode must be 'truncate' or 'exponential', got '{mode}'")
83
+
84
+ def process_nifti_list(self, data_list: List, water_list: Optional[List] = None, **kwargs):
85
+ """
86
+ Apply apodization to list of NIFTI_MRS objects.
87
+
88
+ Args:
89
+ data_list: List of NIFTI_MRS objects
90
+ water_list: Optional list of water reference NIFTI_MRS objects
91
+ **kwargs: Additional arguments
92
+
93
+ Returns:
94
+ Tuple of (processed_data_list, processed_water_list)
95
+ """
96
+ processed_data = []
97
+
98
+ for nifti in data_list:
99
+ # Get FID data
100
+ fid = nifti[:]
101
+
102
+ # Get spectral width (for exponential mode)
103
+ sw_hz = 1.0 / nifti.dwelltime
104
+
105
+ # Apply apodization
106
+ if self.mode == 'truncate':
107
+ fid_apod = self._apply_truncation(fid)
108
+ else: # exponential
109
+ fid_apod = self._apply_exponential(fid, sw_hz)
110
+
111
+ # Update NIFTI_MRS data
112
+ if fid_apod.shape[-1] != fid.shape[-1]:
113
+ # Shape changed (truncation) — must build a new NIFTI_MRS object
114
+ spec_freq = nifti.spectrometer_frequency[0]
115
+ nucleus = nifti.nucleus[0] if nifti.nucleus else '1H'
116
+ dim_tags = list(nifti.dim_tags) if hasattr(nifti, 'dim_tags') else [None, None, None]
117
+ affine = nifti.getAffine('voxel', 'world') if hasattr(nifti, 'getAffine') else None
118
+
119
+ new_nifti = gen_nifti_mrs(
120
+ data=fid_apod,
121
+ dwelltime=nifti.dwelltime,
122
+ spec_freq=spec_freq,
123
+ nucleus=nucleus,
124
+ dim_tags=dim_tags,
125
+ affine=affine,
126
+ )
127
+ # Copy across any extra header fields
128
+ if hasattr(nifti, 'hdr_ext'):
129
+ for key in nifti.hdr_ext:
130
+ if key not in ('SpectrometerFrequency', 'ResonantNucleus',
131
+ 'dim_5', 'dim_6', 'dim_7'):
132
+ try:
133
+ new_nifti.add_hdr_field(key, nifti.hdr_ext[key])
134
+ except Exception:
135
+ pass
136
+ nifti = new_nifti
137
+ else:
138
+ nifti[:] = fid_apod
139
+ processed_data.append(nifti)
140
+
141
+ return processed_data, water_list
142
+
143
+ def process_tensor(self, data_array, water_array=None, backend=None, **kwargs):
144
+ """
145
+ Apply apodization to tensor/array data (**any backend**).
146
+
147
+ **Truncation mode**: slices the last axis with ``data[..., :n_keep]``
148
+ — works natively in NumPy, PyTorch, JAX, and TensorFlow without any
149
+ conversion.
150
+
151
+ **Exponential mode**: the decay envelope is a numpy array (no data
152
+ dependency) multiplied via ``data * match_backend(envelope, data)``
153
+ — fully backend-native multiply, gradients preserved.
154
+
155
+ Args:
156
+ data_array: Input tensor of shape ``(batch, ..., n_points)``
157
+ water_array: Optional water reference (unchanged)
158
+ backend: Backend enum (unused)
159
+ **kwargs: ``'sw_hz'`` required for exponential mode
160
+
161
+ Returns:
162
+ Tuple of (processed_data, water_array)
163
+ """
164
+ if self.mode == 'truncate':
165
+ N = data_array.shape[-1]
166
+ if self.n_pts is not None:
167
+ n_keep = int(np.clip(self.n_pts, 1, N))
168
+ elif self.frac_pts is not None:
169
+ if not (0 < self.frac_pts <= 1):
170
+ raise ValueError("frac_pts must be in range (0, 1]")
171
+ n_keep = int(np.ceil(self.frac_pts * N))
172
+ else:
173
+ raise ValueError("Must provide n_pts or frac_pts for truncation mode")
174
+ # Native slice — valid in numpy, torch, jax, tf
175
+ return data_array[..., :n_keep], water_array
176
+
177
+ else: # exponential
178
+ sw_hz = kwargs.get('sw_hz')
179
+ if sw_hz is None:
180
+ raise ValueError("Apodization.process_tensor requires 'sw_hz' for exponential mode")
181
+
182
+ if self.auto_lb:
183
+ if self.target_pts is None:
184
+ raise ValueError("Must provide target_pts when auto_lb=True")
185
+ lb = self._calc_lb(None, sw_hz, self.target_pts, self.target_damp)
186
+ elif self.lb_hz is not None:
187
+ lb = self.lb_hz
188
+ else:
189
+ raise ValueError("Must provide lb_hz or set auto_lb=True for exponential mode")
190
+
191
+ N = data_array.shape[-1]
192
+ t = np.arange(N, dtype=np.float64) / float(sw_hz)
193
+ t_shape = [1] * (len(data_array.shape) - 1) + [N]
194
+ t = t.reshape(t_shape)
195
+
196
+ envelope = np.exp(-np.pi * float(lb) * t)
197
+ # Backend-native multiply (envelope is data-independent numpy array)
198
+ return data_array * match_backend(envelope, data_array), water_array
199
+
200
+ def _apply_truncation(self, fid: np.ndarray) -> np.ndarray:
201
+ """
202
+ Apply truncation apodization.
203
+
204
+ Args:
205
+ fid: Input FID data
206
+
207
+ Returns:
208
+ Truncated FID
209
+ """
210
+ N = fid.shape[-1]
211
+
212
+ # Determine number of points to keep
213
+ if self.n_pts is not None:
214
+ data_keep = int(np.clip(self.n_pts, 1, N))
215
+ elif self.frac_pts is not None:
216
+ if not (0 < self.frac_pts <= 1):
217
+ raise ValueError("frac_pts must be in range (0, 1]")
218
+ data_keep = int(np.ceil(self.frac_pts * N))
219
+ else:
220
+ raise ValueError("Must provide n_pts or frac_pts for truncation mode")
221
+
222
+ # Truncate along last dimension
223
+ return fid[..., :data_keep]
224
+
225
+ def _apply_exponential(self, fid: np.ndarray, sw_hz: float) -> np.ndarray:
226
+ """
227
+ Apply exponential apodization.
228
+
229
+ Args:
230
+ fid: Input FID data
231
+ sw_hz: Spectral width in Hz
232
+
233
+ Returns:
234
+ Apodized FID
235
+ """
236
+ # Determine lb_hz
237
+ if self.auto_lb:
238
+ if self.target_pts is None:
239
+ raise ValueError("Must provide target_pts when auto_lb=True")
240
+ lb = self._calc_lb(fid, sw_hz, self.target_pts, self.target_damp)
241
+ elif self.lb_hz is not None:
242
+ lb = self.lb_hz
243
+ else:
244
+ raise ValueError("Must provide lb_hz or set auto_lb=True for exponential mode")
245
+
246
+ if lb <= 0:
247
+ return fid
248
+
249
+ # Get time points for last dimension
250
+ N = fid.shape[-1]
251
+ t = np.arange(N, dtype=float) / float(sw_hz)
252
+
253
+ # Reshape t to broadcast correctly
254
+ t_shape = tuple([1] * (fid.ndim - 1) + [N])
255
+ t = t.reshape(t_shape)
256
+
257
+ # Apply exponential decay
258
+ envelope = np.exp(-np.pi * float(lb) * t)
259
+ return fid * envelope
260
+
261
+ @staticmethod
262
+ def _calc_lb(fid, sw_hz: float, desired_npts: int, target_damp: float = 0.01) -> float:
263
+ """
264
+ Calculate Lorentzian broadening to achieve target damping.
265
+
266
+ Args:
267
+ fid: FID data
268
+ sw_hz: Spectral width in Hz
269
+ desired_npts: Point where signal is mostly noise
270
+ target_damp: Target damping factor (default: 0.01 = 1% of signal)
271
+
272
+ Returns:
273
+ Lorentzian broadening in Hz
274
+ """
275
+ return (-sw_hz / (np.pi * desired_npts)) * np.log(target_damp)