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.
- augmentrum/__init__.py +23 -0
- augmentrum/augmentation/__init__.py +32 -0
- augmentrum/augmentation/amplitude_scaling.py +170 -0
- augmentrum/augmentation/apodization.py +275 -0
- augmentrum/augmentation/artificial_peaks.py +334 -0
- augmentrum/augmentation/baseline_augmentation.py +544 -0
- augmentrum/augmentation/eddy_current.py +278 -0
- augmentrum/augmentation/gaussian_noise.py +211 -0
- augmentrum/augmentation/line_broadening.py +179 -0
- augmentrum/augmentation/phase_frequency.py +259 -0
- augmentrum/augmentation/residual_water.py +264 -0
- augmentrum/augmentation/spatial_augmentations.py +859 -0
- augmentrum/augmentation/spurious_echoes.py +378 -0
- augmentrum/augmentation/zero_fill.py +163 -0
- augmentrum/core/__init__.py +10 -0
- augmentrum/core/augmentrum.py +1172 -0
- augmentrum/core/base_module.py +423 -0
- augmentrum/core/dataset_utils.py +417 -0
- augmentrum/core/nifti_mrs_plus.py +1079 -0
- augmentrum/core/pipeline.py +358 -0
- augmentrum/dataset/brainbeats.py +537 -0
- augmentrum/dataset/cows.py +302 -0
- augmentrum/dataset/fmrsinpain.py +208 -0
- augmentrum/processing/bicubic_interpolator.py +296 -0
- augmentrum/processing/nifti_raw_processor.py +389 -0
- augmentrum/processing/tricubic_interpolator.py +293 -0
- augmentrum/processing/utils.py +414 -0
- augmentrum/resources/__init__.py +14 -0
- augmentrum/resources/scans/__init__.py +0 -0
- augmentrum/sampling/coil_average_sampler.py +274 -0
- augmentrum/sampling/kspace_reconstructor.py +275 -0
- augmentrum/sampling/kspace_sampling.py +2562 -0
- augmentrum/sampling/subject_splitter.py +131 -0
- augmentrum/utils/__init__.py +10 -0
- augmentrum/utils/philips.py +90 -0
- augmentrum/utils/plotting.py +651 -0
- augmentrum/utils/tensor_ops.py +203 -0
- augmentrum-0.0.1.dist-info/METADATA +189 -0
- augmentrum-0.0.1.dist-info/RECORD +42 -0
- augmentrum-0.0.1.dist-info/WHEEL +5 -0
- augmentrum-0.0.1.dist-info/licenses/LICENSE +202 -0
- augmentrum-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
####################################################################################################
|
|
2
|
+
# base_module.py #
|
|
3
|
+
####################################################################################################
|
|
4
|
+
# #
|
|
5
|
+
# Authors: J. P. Merkofer (j.p.merkofer@tue.nl) #
|
|
6
|
+
# #
|
|
7
|
+
# Created: 2026-02-06 #
|
|
8
|
+
# #
|
|
9
|
+
# Purpose: Unified base class for all pipeline modules - handles backends, logging, everything #
|
|
10
|
+
# #
|
|
11
|
+
####################################################################################################
|
|
12
|
+
|
|
13
|
+
from abc import ABC, abstractmethod
|
|
14
|
+
from typing import List, Optional, Tuple, Union
|
|
15
|
+
from augmentrum.core import NIfTI_MRS_Plus, Backend
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BaseModule(ABC):
|
|
19
|
+
"""
|
|
20
|
+
Unified base class for ALL augmentation/processing pipeline modules.
|
|
21
|
+
|
|
22
|
+
Handles:
|
|
23
|
+
- Multiple backend support (NIfTI list, NumPy, PyTorch, TensorFlow, JAX, Keras)
|
|
24
|
+
- Automatic backend detection and conversion
|
|
25
|
+
- Provenance logging (only if not volatile)
|
|
26
|
+
- Smart dispatching to appropriate processing method
|
|
27
|
+
|
|
28
|
+
Subclasses just implement the methods they support:
|
|
29
|
+
- process_nifti_list() for FSL-MRS style processing
|
|
30
|
+
- process_tensor() for array/tensor operations
|
|
31
|
+
- forward() as alias (for compatibility)
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
# Subclasses can override - if empty, module supports all backends
|
|
35
|
+
SUPPORTED_BACKENDS: List[Backend] = []
|
|
36
|
+
|
|
37
|
+
def __init__(self, **kwargs):
|
|
38
|
+
"""
|
|
39
|
+
Initialize module with optional parameters.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
**kwargs: Module-specific parameters, stored in self.params
|
|
43
|
+
"""
|
|
44
|
+
self.params = kwargs
|
|
45
|
+
|
|
46
|
+
def __call__(self, data: NIfTI_MRS_Plus, water: Optional[NIfTI_MRS_Plus] = None,
|
|
47
|
+
**kwargs) -> Tuple[NIfTI_MRS_Plus, Optional[NIfTI_MRS_Plus]]:
|
|
48
|
+
"""
|
|
49
|
+
Main entry point - handles everything automatically.
|
|
50
|
+
|
|
51
|
+
- Checks backend compatibility
|
|
52
|
+
- Routes to appropriate processing method
|
|
53
|
+
- Handles logging/provenance (if not volatile)
|
|
54
|
+
- Returns NIfTI_MRS_Plus objects
|
|
55
|
+
"""
|
|
56
|
+
# Determine which method to use based on backend
|
|
57
|
+
backend = data.backend
|
|
58
|
+
|
|
59
|
+
# Check if we support this backend (if SUPPORTED_BACKENDS specified)
|
|
60
|
+
if self.SUPPORTED_BACKENDS and backend not in self.SUPPORTED_BACKENDS:
|
|
61
|
+
# Use first supported backend
|
|
62
|
+
backend = self.SUPPORTED_BACKENDS[0] if self.SUPPORTED_BACKENDS else Backend.NIFTI_LIST
|
|
63
|
+
|
|
64
|
+
# Check which methods are actually implemented (overridden from BaseModule)
|
|
65
|
+
has_process_nifti_list = (
|
|
66
|
+
self.__class__.process_nifti_list is not BaseModule.process_nifti_list
|
|
67
|
+
)
|
|
68
|
+
has_process_tensor = (
|
|
69
|
+
self.__class__.process_tensor is not BaseModule.process_tensor
|
|
70
|
+
)
|
|
71
|
+
has_forward = (
|
|
72
|
+
self.__class__.forward is not BaseModule.forward
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Try methods in order of preference
|
|
76
|
+
if backend == Backend.NIFTI_LIST:
|
|
77
|
+
# Use NIfTI list processing
|
|
78
|
+
if has_process_nifti_list:
|
|
79
|
+
return self._process_via_nifti_list(data, water, **kwargs)
|
|
80
|
+
elif has_forward:
|
|
81
|
+
return self._process_via_forward(data, water, **kwargs)
|
|
82
|
+
else:
|
|
83
|
+
# Use tensor processing
|
|
84
|
+
if has_process_tensor:
|
|
85
|
+
return self._process_via_tensor(data, water, backend, **kwargs)
|
|
86
|
+
elif has_forward:
|
|
87
|
+
return self._process_via_forward(data, water, **kwargs)
|
|
88
|
+
elif has_process_nifti_list:
|
|
89
|
+
# Fallback: use NIfTI list processing even for tensor backends
|
|
90
|
+
return self._process_via_nifti_list(data, water, **kwargs)
|
|
91
|
+
|
|
92
|
+
# Fallback to forward if nothing else works
|
|
93
|
+
if has_forward:
|
|
94
|
+
return self._process_via_forward(data, water, **kwargs)
|
|
95
|
+
|
|
96
|
+
# Last resort: try nifti list
|
|
97
|
+
if has_process_nifti_list:
|
|
98
|
+
return self._process_via_nifti_list(data, water, **kwargs)
|
|
99
|
+
|
|
100
|
+
raise NotImplementedError(
|
|
101
|
+
f"{self.__class__.__name__} must implement at least one of: "
|
|
102
|
+
f"process_nifti_list(), process_tensor(), or forward()"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def _process_via_nifti_list(self, data: NIfTI_MRS_Plus, water: Optional[NIfTI_MRS_Plus],
|
|
106
|
+
**kwargs) -> Tuple[NIfTI_MRS_Plus, Optional[NIfTI_MRS_Plus]]:
|
|
107
|
+
"""Process using NIfTI list method."""
|
|
108
|
+
# Get lists
|
|
109
|
+
data_list = data.list()
|
|
110
|
+
water_list = water.list() if water is not None else None
|
|
111
|
+
|
|
112
|
+
# Process
|
|
113
|
+
processed_data, processed_water = self.process_nifti_list(
|
|
114
|
+
data_list, water_list, **kwargs
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Log provenance (only if not volatile)
|
|
118
|
+
operation_name = self.__class__.__name__
|
|
119
|
+
operation_details = {'method': 'process_nifti_list', 'params': self.params, **kwargs}
|
|
120
|
+
|
|
121
|
+
# Wrap back into NIfTI_MRS_Plus
|
|
122
|
+
data_out = NIfTI_MRS_Plus(
|
|
123
|
+
nifti_list=processed_data,
|
|
124
|
+
backend=data.backend,
|
|
125
|
+
volatile=data.volatile
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Update metadata if not volatile
|
|
129
|
+
if not data.volatile:
|
|
130
|
+
data_out.update_metadata(operation_name, operation_details)
|
|
131
|
+
|
|
132
|
+
water_out = None
|
|
133
|
+
if processed_water is not None:
|
|
134
|
+
water_out = NIfTI_MRS_Plus(
|
|
135
|
+
nifti_list=processed_water,
|
|
136
|
+
backend=water.backend if water else Backend.NIFTI_LIST,
|
|
137
|
+
volatile=water.volatile if water else data.volatile
|
|
138
|
+
)
|
|
139
|
+
if water and not water.volatile:
|
|
140
|
+
water_out.update_metadata(operation_name, operation_details)
|
|
141
|
+
|
|
142
|
+
return data_out, water_out
|
|
143
|
+
|
|
144
|
+
def _process_via_tensor(self, data: NIfTI_MRS_Plus, water: Optional[NIfTI_MRS_Plus],
|
|
145
|
+
backend: Backend, **kwargs) -> Tuple[NIfTI_MRS_Plus, Optional[NIfTI_MRS_Plus]]:
|
|
146
|
+
"""Process using tensor method.
|
|
147
|
+
|
|
148
|
+
Following the zea pattern, data is passed to ``process_tensor()`` in its
|
|
149
|
+
**native backend format** (NumPy / PyTorch / JAX / TensorFlow) so that
|
|
150
|
+
gradients and device placement are preserved. The write-back into
|
|
151
|
+
NIFTI_MRS storage always goes through NumPy (unavoidable since NIfTI is
|
|
152
|
+
a NumPy-backed format).
|
|
153
|
+
|
|
154
|
+
Performance tiers
|
|
155
|
+
-----------------
|
|
156
|
+
1. **Fast** — same N_PTS in and out (all augmentations except truncate/zerofill):
|
|
157
|
+
one vectorised tensor op, then ``nifti[:] = sample`` in-place. Zero extra
|
|
158
|
+
allocations.
|
|
159
|
+
|
|
160
|
+
2. **Medium** — N_PTS changes (e.g. truncation, zero-fill): tensor slice/pad is
|
|
161
|
+
still vectorised, but write-back must rebuild each NIfTI_MRS object
|
|
162
|
+
(O(B) constructor calls). A ``RuntimeWarning`` is emitted.
|
|
163
|
+
|
|
164
|
+
3. **Slow (~ routing)** — module only implements ``process_nifti_list``:
|
|
165
|
+
falls back to the NIfTI-list path, processes each subject individually.
|
|
166
|
+
This is what sampling modules (CoilAverageSampler, NIfTI_RawProcessor)
|
|
167
|
+
always use because they change DIM_COIL / DIM_DYN, not just values.
|
|
168
|
+
|
|
169
|
+
Rule of thumb: keep **N_PTS and all extra dimensions uniform** across the
|
|
170
|
+
whole batch for tier-1 speed everywhere.
|
|
171
|
+
"""
|
|
172
|
+
from augmentrum.utils.tensor_ops import to_numpy
|
|
173
|
+
|
|
174
|
+
# Inject spectral metadata so modules can use sw_hz / sf_mhz without
|
|
175
|
+
# requiring FSL-MRS objects.
|
|
176
|
+
# Primary source: metadata_common (populated when volatile=False).
|
|
177
|
+
# Fallback: read directly from nifti_list[0] (works even in volatile mode).
|
|
178
|
+
if 'sw_hz' not in kwargs:
|
|
179
|
+
dwell = data.metadata_common.get('dwelltime')
|
|
180
|
+
if dwell is None and data.n_subjects > 0:
|
|
181
|
+
try:
|
|
182
|
+
dwell = data.nifti_list[0].dwelltime
|
|
183
|
+
except Exception:
|
|
184
|
+
pass
|
|
185
|
+
if dwell is not None:
|
|
186
|
+
kwargs['sw_hz'] = 1.0 / dwell
|
|
187
|
+
|
|
188
|
+
if 'sf_mhz' not in kwargs:
|
|
189
|
+
sf = data.metadata_common.get('spectrometer_frequency')
|
|
190
|
+
if sf is None and data.n_subjects > 0:
|
|
191
|
+
try:
|
|
192
|
+
sf = data.nifti_list[0].spectrometer_frequency
|
|
193
|
+
except Exception:
|
|
194
|
+
pass
|
|
195
|
+
if sf is not None:
|
|
196
|
+
kwargs['sf_mhz'] = sf[0] if hasattr(sf, '__getitem__') else sf
|
|
197
|
+
|
|
198
|
+
# ── Get data in native backend format (not forced to numpy!) ──
|
|
199
|
+
data_array = data.get_data(backend)
|
|
200
|
+
water_array = water.get_data(backend) if water is not None else None
|
|
201
|
+
|
|
202
|
+
# ── Process (receives native tensors — preserves gradients) ──
|
|
203
|
+
processed_data, processed_water = self.process_tensor(
|
|
204
|
+
data_array, water_array, backend=backend, **kwargs
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# ── Write back into NIfTI_MRS list (must go through numpy) ──
|
|
208
|
+
nifti_list_out = data.list()
|
|
209
|
+
if processed_data is not None:
|
|
210
|
+
processed_np = to_numpy(processed_data)
|
|
211
|
+
# Detect N_PTS change (e.g. truncation, zero-fill)
|
|
212
|
+
in_npts = data_array.shape[-1] if hasattr(data_array, 'shape') else None
|
|
213
|
+
out_npts = processed_np.shape[-1] if processed_np is not None else None
|
|
214
|
+
if in_npts is not None and out_npts is not None and out_npts != in_npts:
|
|
215
|
+
import warnings
|
|
216
|
+
warnings.warn(
|
|
217
|
+
f"{self.__class__.__name__}: N_PTS changed {in_npts} → {out_npts} "
|
|
218
|
+
f"(same for all {processed_np.shape[0]} batch members — module params "
|
|
219
|
+
f"are uniform by design). "
|
|
220
|
+
f"NIfTI objects will be rebuilt with the new length. "
|
|
221
|
+
f"This is correct but ~slower than the normal in-place write-back path. "
|
|
222
|
+
f"For maximum throughput, keep N_PTS the same throughout the pipeline "
|
|
223
|
+
f"(e.g. prefer Apodization[exponential] over Apodization[truncate] "
|
|
224
|
+
f"when batching on a tensor backend).",
|
|
225
|
+
stacklevel=3,
|
|
226
|
+
)
|
|
227
|
+
for i, nifti in enumerate(nifti_list_out):
|
|
228
|
+
if i < processed_np.shape[0]:
|
|
229
|
+
sample_np = processed_np[i]
|
|
230
|
+
if sample_np.shape == nifti[:].shape:
|
|
231
|
+
nifti[:] = sample_np
|
|
232
|
+
else:
|
|
233
|
+
# N_PTS changed — rebuild this NIfTI_MRS object from scratch
|
|
234
|
+
from fsl_mrs.core.nifti_mrs import gen_nifti_mrs
|
|
235
|
+
try:
|
|
236
|
+
affine = nifti.getAffine('voxel', 'world')
|
|
237
|
+
except Exception:
|
|
238
|
+
affine = None
|
|
239
|
+
nucleus = (nifti.nucleus[0]
|
|
240
|
+
if hasattr(nifti, 'nucleus') and nifti.nucleus else '1H')
|
|
241
|
+
dim_tags = (list(nifti.dim_tags)
|
|
242
|
+
if hasattr(nifti, 'dim_tags') else [None, None, None])
|
|
243
|
+
new_nifti = gen_nifti_mrs(
|
|
244
|
+
data=sample_np,
|
|
245
|
+
dwelltime=nifti.dwelltime,
|
|
246
|
+
spec_freq=nifti.spectrometer_frequency[0],
|
|
247
|
+
nucleus=nucleus,
|
|
248
|
+
dim_tags=dim_tags,
|
|
249
|
+
affine=affine,
|
|
250
|
+
)
|
|
251
|
+
# Copy extra header fields
|
|
252
|
+
if hasattr(nifti, 'hdr_ext'):
|
|
253
|
+
for key in nifti.hdr_ext:
|
|
254
|
+
if key not in ('SpectrometerFrequency', 'ResonantNucleus',
|
|
255
|
+
'dim_5', 'dim_6', 'dim_7'):
|
|
256
|
+
try:
|
|
257
|
+
new_nifti.add_hdr_field(key, nifti.hdr_ext[key])
|
|
258
|
+
except Exception:
|
|
259
|
+
pass
|
|
260
|
+
nifti_list_out[i] = new_nifti
|
|
261
|
+
|
|
262
|
+
# Log provenance
|
|
263
|
+
operation_name = self.__class__.__name__
|
|
264
|
+
operation_details = {'method': 'process_tensor', 'backend': backend.value,
|
|
265
|
+
'params': self.params}
|
|
266
|
+
|
|
267
|
+
data_out = NIfTI_MRS_Plus(
|
|
268
|
+
nifti_list=nifti_list_out,
|
|
269
|
+
backend=backend,
|
|
270
|
+
volatile=data.volatile
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
if not data.volatile:
|
|
274
|
+
data_out.update_metadata(operation_name, operation_details)
|
|
275
|
+
|
|
276
|
+
water_out = None
|
|
277
|
+
if water is not None:
|
|
278
|
+
water_list_out = water.list()
|
|
279
|
+
if processed_water is not None:
|
|
280
|
+
processed_water_np = to_numpy(processed_water)
|
|
281
|
+
for i, nifti in enumerate(water_list_out):
|
|
282
|
+
if i < processed_water_np.shape[0]:
|
|
283
|
+
sample_np = processed_water_np[i]
|
|
284
|
+
if sample_np.shape == nifti[:].shape:
|
|
285
|
+
nifti[:] = sample_np
|
|
286
|
+
else:
|
|
287
|
+
from fsl_mrs.core.nifti_mrs import gen_nifti_mrs
|
|
288
|
+
try:
|
|
289
|
+
affine = nifti.getAffine('voxel', 'world')
|
|
290
|
+
except Exception:
|
|
291
|
+
affine = None
|
|
292
|
+
nucleus = (nifti.nucleus[0]
|
|
293
|
+
if hasattr(nifti, 'nucleus') and nifti.nucleus else '1H')
|
|
294
|
+
dim_tags = (list(nifti.dim_tags)
|
|
295
|
+
if hasattr(nifti, 'dim_tags') else [None, None, None])
|
|
296
|
+
new_nifti = gen_nifti_mrs(
|
|
297
|
+
data=sample_np,
|
|
298
|
+
dwelltime=nifti.dwelltime,
|
|
299
|
+
spec_freq=nifti.spectrometer_frequency[0],
|
|
300
|
+
nucleus=nucleus,
|
|
301
|
+
dim_tags=dim_tags,
|
|
302
|
+
affine=affine,
|
|
303
|
+
)
|
|
304
|
+
if hasattr(nifti, 'hdr_ext'):
|
|
305
|
+
for key in nifti.hdr_ext:
|
|
306
|
+
if key not in ('SpectrometerFrequency', 'ResonantNucleus',
|
|
307
|
+
'dim_5', 'dim_6', 'dim_7'):
|
|
308
|
+
try:
|
|
309
|
+
new_nifti.add_hdr_field(key, nifti.hdr_ext[key])
|
|
310
|
+
except Exception:
|
|
311
|
+
pass
|
|
312
|
+
water_list_out[i] = new_nifti
|
|
313
|
+
|
|
314
|
+
water_out = NIfTI_MRS_Plus(
|
|
315
|
+
nifti_list=water_list_out,
|
|
316
|
+
backend=backend,
|
|
317
|
+
volatile=water.volatile
|
|
318
|
+
)
|
|
319
|
+
if not water.volatile:
|
|
320
|
+
water_out.update_metadata(operation_name, operation_details)
|
|
321
|
+
|
|
322
|
+
return data_out, water_out
|
|
323
|
+
|
|
324
|
+
def _process_via_forward(self, data: NIfTI_MRS_Plus, water: Optional[NIfTI_MRS_Plus],
|
|
325
|
+
**kwargs) -> Tuple[NIfTI_MRS_Plus, Optional[NIfTI_MRS_Plus]]:
|
|
326
|
+
"""Process using legacy forward method (for compatibility)."""
|
|
327
|
+
# Get appropriate data format based on backend
|
|
328
|
+
if data.backend == Backend.NIFTI_LIST:
|
|
329
|
+
data_in = data.list()
|
|
330
|
+
water_in = water.list() if water is not None else None
|
|
331
|
+
else:
|
|
332
|
+
data_in = data.get_data()
|
|
333
|
+
water_in = water.get_data() if water is not None else None
|
|
334
|
+
|
|
335
|
+
# Process using forward
|
|
336
|
+
processed_data, processed_water = self.forward(data_in, water_in, **kwargs)
|
|
337
|
+
|
|
338
|
+
# Log provenance
|
|
339
|
+
operation_name = self.__class__.__name__
|
|
340
|
+
operation_details = {'method': 'forward', 'params': self.params, **kwargs}
|
|
341
|
+
|
|
342
|
+
# Wrap results
|
|
343
|
+
if isinstance(processed_data, list):
|
|
344
|
+
data_out = NIfTI_MRS_Plus(
|
|
345
|
+
nifti_list=processed_data,
|
|
346
|
+
backend=data.backend,
|
|
347
|
+
volatile=data.volatile
|
|
348
|
+
)
|
|
349
|
+
else:
|
|
350
|
+
# If forward returns something else, keep original structure
|
|
351
|
+
data_out = data.copy()
|
|
352
|
+
|
|
353
|
+
if not data.volatile:
|
|
354
|
+
data_out.update_metadata(operation_name, operation_details)
|
|
355
|
+
|
|
356
|
+
water_out = None
|
|
357
|
+
if processed_water is not None:
|
|
358
|
+
if isinstance(processed_water, list):
|
|
359
|
+
water_out = NIfTI_MRS_Plus(
|
|
360
|
+
nifti_list=processed_water,
|
|
361
|
+
backend=water.backend if water else Backend.NIFTI_LIST,
|
|
362
|
+
volatile=water.volatile if water else data.volatile
|
|
363
|
+
)
|
|
364
|
+
else:
|
|
365
|
+
water_out = water.copy() if water else None
|
|
366
|
+
|
|
367
|
+
if water_out and not water_out.volatile:
|
|
368
|
+
water_out.update_metadata(operation_name, operation_details)
|
|
369
|
+
|
|
370
|
+
return data_out, water_out
|
|
371
|
+
|
|
372
|
+
def supports_backend(self, backend: Backend) -> bool:
|
|
373
|
+
"""Check if this module supports the given backend."""
|
|
374
|
+
if not self.SUPPORTED_BACKENDS:
|
|
375
|
+
return True # Supports all if not specified
|
|
376
|
+
return backend in self.SUPPORTED_BACKENDS
|
|
377
|
+
|
|
378
|
+
def get_preferred_backend(self) -> Backend:
|
|
379
|
+
"""Get the preferred backend for this module."""
|
|
380
|
+
if self.SUPPORTED_BACKENDS:
|
|
381
|
+
return self.SUPPORTED_BACKENDS[0]
|
|
382
|
+
return Backend.NIFTI_LIST
|
|
383
|
+
|
|
384
|
+
# Subclasses can implement these methods as needed
|
|
385
|
+
def process_nifti_list(self, data_list: List, water_list: Optional[List] = None,
|
|
386
|
+
**kwargs) -> Tuple[List, Optional[List]]:
|
|
387
|
+
"""
|
|
388
|
+
Process lists of NIfTI-MRS objects (for FSL-MRS style processing).
|
|
389
|
+
|
|
390
|
+
Override this if your module uses FSL-MRS functions.
|
|
391
|
+
"""
|
|
392
|
+
raise NotImplementedError(
|
|
393
|
+
f"{self.__class__.__name__} does not implement process_nifti_list()"
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
def process_tensor(self, data_array, water_array=None, backend: Backend = Backend.NUMPY,
|
|
397
|
+
**kwargs) -> Tuple:
|
|
398
|
+
"""
|
|
399
|
+
Process tensor/array data (for vectorized operations).
|
|
400
|
+
|
|
401
|
+
Override this if your module uses NumPy/PyTorch/etc. operations.
|
|
402
|
+
"""
|
|
403
|
+
raise NotImplementedError(
|
|
404
|
+
f"{self.__class__.__name__} does not implement process_tensor()"
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
def forward(self, data, water=None, **kwargs):
|
|
408
|
+
"""
|
|
409
|
+
Legacy processing method (for compatibility with old modules).
|
|
410
|
+
|
|
411
|
+
Override this for simple modules that don't need backend-specific logic.
|
|
412
|
+
"""
|
|
413
|
+
raise NotImplementedError(
|
|
414
|
+
f"{self.__class__.__name__} does not implement forward()"
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
def __repr__(self) -> str:
|
|
418
|
+
backends = [b.value for b in self.SUPPORTED_BACKENDS] if self.SUPPORTED_BACKENDS else ['all']
|
|
419
|
+
return f"{self.__class__.__name__}(backends={backends})"
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
|