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,378 @@
1
+ ####################################################################################################
2
+ # spurious_echoes.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 delayed echo replicas (ghosting artifacts) to MRS data. #
11
+ # Supports simple replica model and hybrid model (Kyathanahally et al. 2021, #
12
+ # Bugler et al. 2025). #
13
+ # #
14
+ ####################################################################################################
15
+
16
+ import numpy as np
17
+ from typing import Optional, List, Tuple, Union
18
+ from augmentrum.core.base_module import BaseModule
19
+ from augmentrum.core.nifti_mrs_plus import Backend, NIfTI_MRS_Plus
20
+ from augmentrum.utils.tensor_ops import to_numpy, match_backend
21
+
22
+
23
+ class SpuriousEchoes(BaseModule):
24
+ """
25
+ Add spurious echo artifacts to MRS data.
26
+
27
+ Simulates ghosting artifacts by adding delayed, attenuated copies
28
+ of the FID signal at specified time delays.
29
+
30
+ Supports two modes:
31
+ - 'replica': Simple multiplicative echo — multiplies FID by a decaying
32
+ envelope after a delay (classic replica model).
33
+ - 'hybrid': Physically shifts the FID in time, then shapes with a
34
+ localized envelope (Kyathanahally/Bugler model). Produces more
35
+ realistic ghosting artifacts.
36
+
37
+ Parameters
38
+ ----------
39
+ echoes : list of dicts or tuples
40
+ Each echo specifies:
41
+ - delay_s / tau: Echo delay time in seconds
42
+ - amp / alpha: Relative amplitude (vs max |FID|)
43
+ - phase_deg: Additional phase for this echo (degrees)
44
+ - decay_hz: Exponential decay in Hz (replica mode)
45
+ - freq_hz / df_hz: Frequency offset of echo (Hz)
46
+
47
+ For hybrid mode, additional per-echo keys:
48
+ - t_echo: Echo center time in seconds (defaults to tau)
49
+ - T2: Envelope time constant in seconds (defaults to 0.04)
50
+ - gaussian_env: Use Gaussian envelope (bool, default False)
51
+
52
+ mode : str
53
+ 'replica' (default) or 'hybrid'
54
+ global_phase_deg : float
55
+ Global phase offset for all echoes (default: 0.0)
56
+ alpha_reference : str
57
+ How amplitude is referenced in hybrid mode:
58
+ 'max' (default) = fraction of max(|FID|),
59
+ 'tau' = fraction of |FID| at delay time
60
+
61
+ Examples
62
+ --------
63
+ >>> # Replica mode (simple)
64
+ >>> se = SpuriousEchoes(
65
+ ... echoes=[{'delay_s': 0.1, 'amp': 0.3, 'phase_deg': 0,
66
+ ... 'decay_hz': 5.0, 'freq_hz': 0.0}]
67
+ ... )
68
+ >>> result_data, _ = se(nifti_plus, None)
69
+
70
+ >>> # Hybrid mode (matches legacy add_spurious_echo_artifact)
71
+ >>> se = SpuriousEchoes(
72
+ ... mode='hybrid',
73
+ ... echoes=[{'tau': 0.18, 'alpha': 0.32, 'phase_deg': 25,
74
+ ... 't_echo': 0.26, 'T2': 0.03, 'df_hz': 35.0}]
75
+ ... )
76
+ >>> result_data, _ = se(nifti_plus, None)
77
+ """
78
+
79
+ SUPPORTED_BACKENDS = [] # Supports all backends
80
+
81
+ def __init__(self, echoes=None,
82
+ mode: str = 'replica',
83
+ global_phase_deg: float = 0.0,
84
+ alpha_reference: str = 'max'):
85
+ """Initialize spurious echoes module."""
86
+ if echoes is None:
87
+ echoes = [{'delay_s': 0.1, 'amp': 0.2, 'phase_deg': 0.0,
88
+ 'decay_hz': 5.0, 'freq_hz': 0.0}]
89
+
90
+ super().__init__(echoes=echoes, mode=mode,
91
+ global_phase_deg=global_phase_deg,
92
+ alpha_reference=alpha_reference)
93
+
94
+ self.mode = mode.lower()
95
+ if self.mode not in ('replica', 'hybrid'):
96
+ raise ValueError(f"mode must be 'replica' or 'hybrid', got '{mode}'")
97
+
98
+ self.global_phase_deg = global_phase_deg
99
+ self.alpha_reference = alpha_reference
100
+
101
+ # Normalize echoes to a list of dicts
102
+ self.echoes = []
103
+ for echo in echoes:
104
+ if isinstance(echo, dict):
105
+ self.echoes.append(dict(echo))
106
+ elif isinstance(echo, (tuple, list)):
107
+ # Legacy tuple: (delay_s, amp, phase_deg, decay_hz, freq_hz)
108
+ self.echoes.append({
109
+ 'delay_s': echo[0],
110
+ 'amp': echo[1],
111
+ 'phase_deg': echo[2] if len(echo) > 2 else 0.0,
112
+ 'decay_hz': echo[3] if len(echo) > 3 else 5.0,
113
+ 'freq_hz': echo[4] if len(echo) > 4 else 0.0,
114
+ })
115
+ else:
116
+ raise ValueError(f"Echo must be a dict or tuple, got {type(echo)}")
117
+
118
+ def process_nifti_list(self, data_list: List, water_list: Optional[List] = None, **kwargs):
119
+ """
120
+ Add spurious echoes to list of NIFTI_MRS objects.
121
+
122
+ Args:
123
+ data_list: List of NIFTI_MRS objects
124
+ water_list: Optional list of water reference NIFTI_MRS objects
125
+ **kwargs: Additional arguments
126
+
127
+ Returns:
128
+ Tuple of (processed_data_list, processed_water_list)
129
+ """
130
+ processed_data = []
131
+
132
+ for nifti in data_list:
133
+ # Get FID data
134
+ fid = nifti[:]
135
+
136
+ # Get spectral width
137
+ sw_hz = 1.0 / nifti.dwelltime
138
+
139
+ # Add echoes
140
+ fid_with_echoes = self._add_echoes(fid, sw_hz)
141
+
142
+ # Update NIFTI_MRS data
143
+ nifti[:] = fid_with_echoes
144
+ processed_data.append(nifti)
145
+
146
+ return processed_data, water_list
147
+
148
+ def process_tensor(self, data_array, water_array=None, backend=None, **kwargs):
149
+ """
150
+ Add spurious echo artifacts to tensor/array data (**any backend**).
151
+
152
+ **Replica mode**: echo envelopes are data-independent (pure numpy)
153
+ and applied via ``data + data * match_backend(envelope, data)`` —
154
+ fully backend-native multiply/add.
155
+
156
+ **Hybrid mode**: echo amplitude is referenced to ``max|FID|`` which
157
+ requires a scalar pull to numpy; the additive ghost is then applied
158
+ via ``data + match_backend(ghost_additive, data)``.
159
+
160
+ Args:
161
+ data_array: Input tensor of shape ``(batch, ..., n_points)``
162
+ water_array: Optional water reference (unchanged)
163
+ backend: Backend enum (unused)
164
+ **kwargs: Must contain ``'sw_hz'`` (spectral width in Hz)
165
+
166
+ Returns:
167
+ Tuple of (processed_data, water_array)
168
+ """
169
+ sw_hz = kwargs.get('sw_hz')
170
+ if sw_hz is None:
171
+ raise ValueError("SpuriousEchoes.process_tensor requires 'sw_hz' in kwargs")
172
+
173
+ original_shape = data_array.shape
174
+ N = original_shape[-1]
175
+
176
+ if self.mode == 'replica':
177
+ # Envelope is purely time-based — no data dependency at all
178
+ t = np.arange(N, dtype=np.float64) / float(sw_hz)
179
+ t_shape = [1] * (len(original_shape) - 1) + [N]
180
+ t = t.reshape(t_shape)
181
+
182
+ gphi = np.deg2rad(self.global_phase_deg)
183
+ echo_sum = np.zeros(t_shape, dtype=np.complex128)
184
+
185
+ for echo in self.echoes:
186
+ delay_s = echo.get('delay_s', echo.get('tau', 0.1))
187
+ amp = echo.get('amp', echo.get('alpha', echo.get('amplitude', 0.1)))
188
+ phase_deg = echo.get('phase_deg', 0.0)
189
+ decay_hz = echo.get('decay_hz', echo.get('decay', 5.0))
190
+ freq_hz = echo.get('freq_hz', echo.get('df_hz', 0.0))
191
+
192
+ if delay_s > 1.0:
193
+ delay_s = delay_s / float(sw_hz)
194
+
195
+ u = (t >= delay_s).astype(np.float64)
196
+ td = (t - delay_s) * u
197
+ env = (amp
198
+ * np.exp(1j * (gphi + np.deg2rad(phase_deg)))
199
+ * np.exp(-np.pi * float(decay_hz) * td)
200
+ * np.exp(1j * 2.0 * np.pi * float(freq_hz) * td))
201
+ echo_sum += env
202
+
203
+ # Apply: fid_out = fid + fid * sum_of_envelopes (backend-native)
204
+ return data_array + data_array * match_backend(echo_sum, data_array), water_array
205
+
206
+ else: # hybrid mode — amplitude references max(|fid|): pull per-FID to numpy
207
+ n_batch = int(np.prod(original_shape[:-1]))
208
+ data_np = to_numpy(data_array).reshape(n_batch, N)
209
+ ghost_np = np.zeros_like(data_np)
210
+
211
+ for i in range(n_batch):
212
+ fid_1d = data_np[i]
213
+ result = self._add_echoes_hybrid_1d(fid_1d, sw_hz)
214
+ ghost_np[i] = result - fid_1d # additive contribution only
215
+
216
+ ghost_np = ghost_np.reshape(original_shape)
217
+ return data_array + match_backend(ghost_np, data_array), water_array
218
+
219
+ def _add_echoes(self, fid: np.ndarray, sw_hz: float) -> np.ndarray:
220
+ """
221
+ Add spurious echoes to FID data.
222
+
223
+ Args:
224
+ fid: Input FID data
225
+ sw_hz: Spectral width in Hz
226
+
227
+ Returns:
228
+ FID with echoes added
229
+ """
230
+ # Work with last dimension (FID points)
231
+ original_shape = fid.shape
232
+ N = original_shape[-1]
233
+
234
+ # Reshape to 2D for processing
235
+ fid_2d = fid.reshape(-1, N)
236
+ result = np.zeros_like(fid_2d)
237
+
238
+ # Process each FID
239
+ for i in range(fid_2d.shape[0]):
240
+ fid_1d = fid_2d[i]
241
+ if self.mode == 'hybrid':
242
+ result[i] = self._add_echoes_hybrid_1d(fid_1d, sw_hz)
243
+ else:
244
+ result[i] = self._add_echoes_replica_1d(fid_1d, sw_hz)
245
+
246
+ # Reshape back to original shape
247
+ return result.reshape(original_shape)
248
+
249
+ # ── Replica mode (original simple model) ──────────────────────────────
250
+
251
+ def _add_echoes_replica_1d(self, fid: np.ndarray, sw_hz: float) -> np.ndarray:
252
+ """
253
+ Add echoes to 1D FID using replica model.
254
+
255
+ The echo is the original FID multiplied by a decaying, frequency-shifted
256
+ envelope that starts at the delay time.
257
+
258
+ Args:
259
+ fid: 1D FID data
260
+ sw_hz: Spectral width in Hz
261
+
262
+ Returns:
263
+ FID with echoes
264
+ """
265
+ f = np.asarray(fid, dtype=np.complex128)
266
+ n = f.size
267
+ t = np.arange(n, dtype=float) / float(sw_hz)
268
+
269
+ out = f.copy()
270
+ gphi = np.deg2rad(self.global_phase_deg)
271
+
272
+ for echo in self.echoes:
273
+ delay_val = echo.get('delay_s', echo.get('tau', 0.1))
274
+ amp = echo.get('amp', echo.get('alpha', echo.get('amplitude', 0.1)))
275
+ phase_deg = echo.get('phase_deg', 0.0)
276
+ decay_hz = echo.get('decay_hz', echo.get('decay', 5.0))
277
+ freq_hz = echo.get('freq_hz', echo.get('df_hz', 0.0))
278
+
279
+ # delay_val might be in seconds or points - convert to seconds
280
+ if delay_val > 1.0:
281
+ delay_s = delay_val / float(sw_hz)
282
+ else:
283
+ delay_s = delay_val
284
+
285
+ # Step function: 1 after delay, 0 before
286
+ u = (t >= delay_s).astype(float)
287
+ td = (t - delay_s) * u
288
+
289
+ # Echo signal
290
+ echo_env = (
291
+ amp
292
+ * np.exp(1j * (gphi + np.deg2rad(phase_deg)))
293
+ * np.exp(-np.pi * float(decay_hz) * td)
294
+ * np.exp(1j * 2.0 * np.pi * float(freq_hz) * td)
295
+ )
296
+
297
+ # Add delayed replica
298
+ out += f * echo_env
299
+
300
+ return out
301
+
302
+ # ── Hybrid mode (Kyathanahally / Bugler model) ───────────────────────
303
+
304
+ def _add_echoes_hybrid_1d(self, fid: np.ndarray, sw_hz: float) -> np.ndarray:
305
+ """
306
+ Add echoes to 1D FID using hybrid model.
307
+
308
+ Physically shifts the FID in time, then applies a localized envelope
309
+ and frequency modulation. Matches the legacy add_spurious_echo_artifact
310
+ (Kyathanahally et al. 2021, Bugler et al. 2025).
311
+
312
+ Args:
313
+ fid: 1D FID data
314
+ sw_hz: Spectral width in Hz
315
+
316
+ Returns:
317
+ FID with echoes
318
+ """
319
+ f = np.asarray(fid, dtype=np.complex128)
320
+ n = f.size
321
+ dt = 1.0 / float(sw_hz)
322
+ t = np.arange(n, dtype=float) * dt
323
+
324
+ out = f.copy()
325
+ gphi = np.deg2rad(self.global_phase_deg)
326
+
327
+ for echo in self.echoes:
328
+ # Extract parameters (support both naming conventions)
329
+ alpha = echo.get('alpha', echo.get('amp', echo.get('amplitude', 0.05)))
330
+ phase_deg = echo.get('phase_deg', 0.0)
331
+ T2 = echo.get('T2', 0.04)
332
+ df_hz = echo.get('df_hz', echo.get('freq_hz', 0.0))
333
+ gaussian_env = echo.get('gaussian_env', False)
334
+
335
+ # Determine shift in samples — support delay_pts directly
336
+ if 'delay_pts' in echo:
337
+ shift_pts = int(echo['delay_pts'])
338
+ tau = shift_pts * dt
339
+ else:
340
+ tau = echo.get('tau', echo.get('delay_s', 0.18))
341
+ shift_pts = int(round(tau / dt))
342
+
343
+ t_echo = echo.get('t_echo', tau)
344
+
345
+ # Delayed (physically shifted) copy of the FID
346
+ delayed_fid = np.zeros_like(f)
347
+ if shift_pts < n:
348
+ delayed_fid[shift_pts:] = f[:n - shift_pts]
349
+
350
+ # Phase + frequency modulation
351
+ phase_rad = gphi + np.deg2rad(phase_deg)
352
+ mod = np.exp(1j * (2.0 * np.pi * df_hz * (t - tau) + phase_rad))
353
+
354
+ # Build ghost: handle no-envelope case (T2 >= 1e4) for exact
355
+ # match with simple shift-and-add legacy model
356
+ if T2 >= 1e4:
357
+ # No envelope — simple: ghost = alpha * delayed_fid * mod
358
+ ghost = alpha * delayed_fid * mod
359
+ else:
360
+ # Full hybrid: normalized FID × envelope × amplitude
361
+ if self.alpha_reference == 'tau':
362
+ tau_idx = min(shift_pts, n - 1)
363
+ Ag = alpha * np.abs(f[tau_idx])
364
+ else: # 'max'
365
+ Ag = alpha * (np.max(np.abs(f)) + 1e-30)
366
+
367
+ delayed_norm = delayed_fid / (np.max(np.abs(f)) + 1e-30)
368
+
369
+ if gaussian_env:
370
+ env = np.exp(-((t - t_echo) ** 2) / (2.0 * T2 ** 2))
371
+ else:
372
+ env = np.exp(-np.abs(t - t_echo) / T2)
373
+
374
+ ghost = Ag * delayed_norm * env * mod
375
+
376
+ out += ghost
377
+
378
+ return out
@@ -0,0 +1,163 @@
1
+ ####################################################################################################
2
+ # zero_fill.py #
3
+ ####################################################################################################
4
+ # #
5
+ # Authors: K. C. Igwe (kci2104@columbia.edu) #
6
+ # J. P. Merkofer (j.p.merkofer@tue.nl) #
7
+ # #
8
+ # Created: 2026-03-29 #
9
+ # #
10
+ # Purpose: Zero-fill (zero-pad) or crop FID to a fixed target number of spectral points. #
11
+ # #
12
+ ####################################################################################################
13
+
14
+ import numpy as np
15
+ from typing import Optional, List
16
+
17
+ from augmentrum.core.base_module import BaseModule
18
+ from augmentrum.utils.tensor_ops import _is_torch, _is_jax, _is_tf
19
+ from fsl_mrs.core.nifti_mrs import gen_nifti_mrs
20
+
21
+
22
+ class ZeroFill(BaseModule):
23
+ """
24
+ Zero-pad (or crop) the FID to a fixed target number of spectral points.
25
+
26
+ Zero-filling increases apparent spectral resolution by interpolating
27
+ between frequency bins. Cropping (``target_pts < N_PTS``) is equivalent
28
+ to ``Apodization(mode='truncate')``.
29
+
30
+ Because ``target_pts`` is a module-level scalar, the same length is
31
+ applied to **every item in the batch** — so the output batch remains
32
+ uniform regardless of backend.
33
+
34
+ On non-NIfTI tensor backends, if N_PTS changes the base class detects
35
+ the shape change, rebuilds each NIfTI_MRS object at the new length, and
36
+ emits a ``RuntimeWarning`` (the "medium-speed" path). For maximum
37
+ throughput keep N_PTS the same throughout the whole pipeline.
38
+
39
+ Parameters
40
+ ----------
41
+ target_pts : int
42
+ Target number of spectral points after zero-filling / cropping.
43
+
44
+ Examples
45
+ --------
46
+ >>> zf = ZeroFill(target_pts=2048) # double a 1024-point FID
47
+ >>> out, _ = zf(nifti_plus, None)
48
+ """
49
+
50
+ SUPPORTED_BACKENDS = [] # All backends via process_tensor
51
+
52
+ def __init__(self, target_pts: int):
53
+ super().__init__(target_pts=target_pts)
54
+ if int(target_pts) < 1:
55
+ raise ValueError(f"target_pts must be >= 1, got {target_pts}")
56
+ self.target_pts = int(target_pts)
57
+
58
+ # ── NIfTI-list path ────────────────────────────────────────────────────────
59
+
60
+ def process_nifti_list(self, data_list: List, water_list: Optional[List] = None, **kwargs):
61
+ """Apply zero-fill to each NIFTI_MRS object in the list."""
62
+ processed_data = []
63
+ for nifti in data_list:
64
+ fid = nifti[:]
65
+ fid_out = self._apply_numpy(fid)
66
+
67
+ if fid_out.shape[-1] != fid.shape[-1]:
68
+ # N_PTS changed — rebuild NIfTI_MRS with new length
69
+ try:
70
+ affine = nifti.getAffine('voxel', 'world')
71
+ except Exception:
72
+ affine = None
73
+ nucleus = (nifti.nucleus[0]
74
+ if hasattr(nifti, 'nucleus') and nifti.nucleus else '1H')
75
+ dim_tags = (list(nifti.dim_tags)
76
+ if hasattr(nifti, 'dim_tags') else [None, None, None])
77
+ new_nifti = gen_nifti_mrs(
78
+ data=fid_out,
79
+ dwelltime=nifti.dwelltime,
80
+ spec_freq=nifti.spectrometer_frequency[0],
81
+ nucleus=nucleus,
82
+ dim_tags=dim_tags,
83
+ affine=affine,
84
+ )
85
+ if hasattr(nifti, 'hdr_ext'):
86
+ for key in nifti.hdr_ext:
87
+ if key not in ('SpectrometerFrequency', 'ResonantNucleus',
88
+ 'dim_5', 'dim_6', 'dim_7'):
89
+ try:
90
+ new_nifti.add_hdr_field(key, nifti.hdr_ext[key])
91
+ except Exception:
92
+ pass
93
+ processed_data.append(new_nifti)
94
+ else:
95
+ nifti[:] = fid_out
96
+ processed_data.append(nifti)
97
+
98
+ return processed_data, water_list
99
+
100
+ # ── Tensor path (all non-NIfTI backends) ──────────────────────────────────
101
+
102
+ def process_tensor(self, data_array, water_array=None, backend=None, **kwargs):
103
+ """Apply zero-fill natively in whatever backend format the data is in.
104
+
105
+ Cropping (target_pts < N_PTS) is a slice — free.
106
+ Padding (target_pts > N_PTS) creates a zeros tensor in the native
107
+ backend and concatenates along the last axis — no numpy round-trip,
108
+ gradients preserved.
109
+ """
110
+ n_in = data_array.shape[-1]
111
+ n_out = self.target_pts
112
+
113
+ if n_out == n_in:
114
+ return data_array, water_array
115
+
116
+ if n_out < n_in:
117
+ # Crop — native slice, works in every backend
118
+ return data_array[..., :n_out], water_array
119
+
120
+ # ── Zero-pad ──────────────────────────────────────────────────────────
121
+ pad_n = n_out - n_in
122
+ pad_shape = list(data_array.shape)
123
+ pad_shape[-1] = pad_n
124
+
125
+ if _is_torch(data_array):
126
+ import torch
127
+ zeros = torch.zeros(pad_shape, dtype=data_array.dtype, device=data_array.device)
128
+ return torch.cat([data_array, zeros], dim=-1), water_array
129
+
130
+ if _is_jax(data_array):
131
+ import jax.numpy as jnp
132
+ zeros = jnp.zeros(pad_shape, dtype=data_array.dtype)
133
+ return jnp.concatenate([data_array, zeros], axis=-1), water_array
134
+
135
+ if _is_tf(data_array):
136
+ import tensorflow as tf
137
+ zeros = tf.zeros(pad_shape, dtype=data_array.dtype)
138
+ return tf.concat([data_array, zeros], axis=-1), water_array
139
+
140
+ # NumPy (and Keras when backed by NumPy / TF caught above)
141
+ dt = data_array.dtype if hasattr(data_array, 'dtype') else np.complex64
142
+ zeros = np.zeros(pad_shape, dtype=dt)
143
+ try:
144
+ return np.concatenate([data_array, zeros], axis=-1), water_array
145
+ except TypeError:
146
+ # Keras tensor — fall back to TF ops
147
+ import tensorflow as tf
148
+ zeros_tf = tf.zeros(pad_shape, dtype=data_array.dtype)
149
+ return tf.concat([data_array, zeros_tf], axis=-1), water_array
150
+
151
+ # ── Internal numpy helper (used by process_nifti_list) ───────────────────
152
+
153
+ def _apply_numpy(self, fid: np.ndarray) -> np.ndarray:
154
+ n_in = fid.shape[-1]
155
+ n_out = self.target_pts
156
+ if n_out == n_in:
157
+ return fid
158
+ if n_out < n_in:
159
+ return fid[..., :n_out]
160
+ pad_width = [(0, 0)] * (fid.ndim - 1) + [(0, n_out - n_in)]
161
+ return np.pad(fid, pad_width)
162
+
163
+
@@ -0,0 +1,10 @@
1
+ """
2
+ Core data structures for Augmentrum.
3
+ """
4
+
5
+ from augmentrum.core.nifti_mrs_plus import NIfTI_MRS_Plus, Backend
6
+ from augmentrum.core.augmentrum import Augmentrum
7
+ from augmentrum.core.base_module import BaseModule
8
+ from augmentrum.core.pipeline import AugmentationPipeline
9
+
10
+ __all__ = ['NIfTI_MRS_Plus', 'Backend', 'Augmentrum', 'BaseModule', 'AugmentationPipeline']