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,293 @@
1
+ ####################################################################################################
2
+ # tricubic_interpolator.py #
3
+ ####################################################################################################
4
+ # #
5
+ # Authors: J. T. LaMaster (john.t.lamaster@gmail.com) #
6
+ # #
7
+ # Created: 2025-10-22 #
8
+ # #
9
+ # Purpose: Implements TricubicHermiteMAkima3D for sampling 3D k-space/image domain volumes #
10
+ # using non-Cartesian trajectories. #
11
+ # #
12
+ ####################################################################################################
13
+
14
+ import torch
15
+ import torch.nn as nn
16
+
17
+
18
+ __all__ = ['TricubicHermiteMAkima3D']
19
+
20
+
21
+ class TricubicHermiteMAkima3D(nn.Module):
22
+ def __init__(self, signal: torch.Tensor):
23
+ """
24
+ Tricubic Hermite Modified Akima Interpolation for 3D data.
25
+
26
+ Args:
27
+ signal (torch.Tensor): 4D tensor of signal values, shape (B, Nx, Ny, Nz).
28
+ It is internally reshaped to [B, S=1, Nx, Ny, Nz].
29
+ """
30
+ super().__init__()
31
+ # --- Dimension Check and Reshape to 5D [B, S, Nx, Ny, Nz] ---
32
+ if signal.dim() == 4:
33
+ # Assume shape is [B, Nx, Ny, Nz] and insert S=1 dimension
34
+ signal = signal.unsqueeze(1)
35
+ elif signal.dim() != 5:
36
+ raise ValueError(f"Input signal must be 4D [B, Nx, Ny, Nz] or 5D [B, S, Nx, Ny, Nz], but got {signal.dim()}D tensor.")
37
+
38
+ self.dtype = signal.dtype
39
+ self.device = signal.device
40
+
41
+ # signal.shape is now [B, S, Nx, Ny, Nz]
42
+ B, S, Nx, Ny, Nz = signal.shape
43
+ # S = 1
44
+ self.B, self.S, self.Nx, self.Ny, self.Nz = B, S, Nx, Ny, Nz
45
+
46
+ # Store axes and signal
47
+ self.xaxis = torch.linspace(start=-1., end=1., steps=Nx, device=self.device)
48
+ self.yaxis = torch.linspace(start=-1., end=1., steps=Ny, device=self.device)
49
+ self.zaxis = torch.linspace(start=-1., end=1., steps=Nz, device=self.device)
50
+ self.signal = signal.to(self.device)
51
+
52
+ # Compute slopes for Modified Akima interpolation (axes are dimensions 2, 3, 4)
53
+ # The axis tensor needs to be unsqueezed to match the 5D signal structure for broadcasting
54
+ self.m_x = self._prepare_slopes(signal, self.xaxis.view(1, 1, Nx, 1, 1), dim=2)
55
+ self.m_y = self._prepare_slopes(signal, self.yaxis.view(1, 1, 1, Ny, 1), dim=3)
56
+ self.m_z = self._prepare_slopes(signal, self.zaxis.view(1, 1, 1, 1, Nz), dim=4)
57
+
58
+ def _prepare_slopes(self, signal, axis, dim):
59
+ """
60
+ Compute Modified Akima slopes along a given axis. (Original implementation retained)
61
+ """
62
+ # helper to build a tuple-of-slices selecting slice(start, end) on axis `dim`
63
+ def slc(dim, start=None, end=None):
64
+ s = [slice(None)] * signal.dim()
65
+ s[dim] = slice(start, end)
66
+ return tuple(s)
67
+
68
+ m = (signal.diff(dim=dim) / axis.unsqueeze(0).diff(dim=dim))
69
+ d0 = 2 * m.select(dim, 0) - m.select(dim, 1)
70
+ dn = 2 * m.select(dim, -1) - m.select(dim, -2)
71
+ m0 = 2 * d0 - m.select(dim, 0)
72
+ mn = 2 * dn - m.select(dim, -1)
73
+
74
+ m = torch.cat([m0.unsqueeze(dim), d0.unsqueeze(dim), m, dn.unsqueeze(dim), mn.unsqueeze(dim)], dim=dim)
75
+ m_prev, m_next = m[slc(dim, None, -1)], m[slc(dim, 1, None)]
76
+ weights = torch.abs(m_next - m_prev) + torch.abs((m_prev + m_next) / 2.0)
77
+
78
+ w1, w2 = weights[slc(dim, None, -2)], weights[slc(dim, 2, None)]
79
+ delta1, delta2 = m[slc(dim, 1, -2)], m[slc(dim, 2, -1)]
80
+ w_sum = w1 + w2 + 1e-6
81
+ slopes = (w2 / w_sum) * delta1 + (w1 / w_sum) * delta2
82
+ slopes[w_sum == 0] = 0.0
83
+ return slopes
84
+
85
+ @staticmethod
86
+ def h_poly(t):
87
+ """
88
+ Compute cubic Hermite basis functions.
89
+ """
90
+ h00 = 2 * t**3 - 3 * t**2 + 1
91
+ h10 = t**3 - 2 * t**2 + t
92
+ h01 = -2 * t**3 + 3 * t**2
93
+ h11 = t**3 - t**2
94
+ return h00, h10, h01, h11
95
+
96
+ def _interpolate_tricubic_hermite(self, B, S, L, I_x, I_y, I_z, t_x, t_y, t_z, dx_cell, dy_cell, dz_cell):
97
+ """
98
+ Vectorized core logic for tricubic Hermite interpolation.
99
+
100
+ Input indices and local coordinates have shape [B, S, L].
101
+ """
102
+ # FIX 1: Cast local coordinates (t) to self.dtype (e.g., complex64) for multiplication
103
+ # The basis functions are computed based on the real coordinates, but the result must match P's dtype.
104
+ H_x = torch.stack(self.h_poly(t_x), dim=-1).to(self.dtype)
105
+ H_y = torch.stack(self.h_poly(t_y), dim=-1).to(self.dtype)
106
+ H_z = torch.stack(self.h_poly(t_z), dim=-1).to(self.dtype)
107
+
108
+ # --- Subgrid Indexing ---
109
+ # The 4 neighbours for interpolation: i-1, i, i+1, i+2
110
+ ix_n = torch.stack([I_x - 1, I_x, I_x + 1, I_x + 2], dim=-1).clamp(0, self.xaxis.numel() - 1)
111
+ iy_n = torch.stack([I_y - 1, I_y, I_y + 1, I_y + 2], dim=-1).clamp(0, self.yaxis.numel() - 1)
112
+ iz_n = torch.stack([I_z - 1, I_z, I_z + 1, I_z + 2], dim=-1).clamp(0, self.zaxis.numel() - 1)
113
+
114
+ # Target coefficient shape [B, S, L, 4, 4, 4]
115
+ target_shape = (B, S, L, 4, 4, 4)
116
+
117
+ # Batch and Shot indexing: [B, S, L, 4, 4, 4]
118
+ b_idx = torch.arange(B, device=self.device, dtype=torch.long).view(B, 1, 1, 1, 1, 1) # [B, 1, 1, 1, 1, 1]
119
+ s_idx = torch.arange(S, device=self.device, dtype=torch.long).view(1, S, 1, 1, 1, 1) # [1, S, 1, 1, 1, 1]
120
+
121
+ # X, Y, Z grid indices: [B, S, L, 4, 4, 4] - relying on broadcasting
122
+ ix_idx = ix_n.unsqueeze(-1).unsqueeze(-1) # [B, S, L, 4, 1, 1]
123
+ iy_idx = iy_n.unsqueeze(-2).unsqueeze(-1) # [B, S, L, 1, 4, 1]
124
+ iz_idx = iz_n.unsqueeze(-2).unsqueeze(-2) # [B, S, L, 1, 1, 4]
125
+
126
+ # FIX 2: Correct indexing using B and S=1 indices (5D signal tensor)
127
+ signal_subgrid = self.signal[b_idx, s_idx, ix_idx, iy_idx, iz_idx] # [B,S,L,4,4,4]
128
+ m_x_subgrid = self.m_x[b_idx, s_idx, ix_idx, iy_idx, iz_idx]
129
+ m_y_subgrid = self.m_y[b_idx, s_idx, ix_idx, iy_idx, iz_idx]
130
+ m_z_subgrid = self.m_z[b_idx, s_idx, ix_idx, iy_idx, iz_idx]
131
+
132
+ # --- 1. Compute Cross-Derivatives (Central Difference Approximation) ---
133
+
134
+ # Denominators are real floats, cast to complex dtype for division if signal is complex
135
+ dtype_for_division = self.signal.dtype
136
+ denom_y = (self.yaxis[iy_n[..., 2]] - self.yaxis[iy_n[..., 0]]).clamp(min=1e-12).to(dtype_for_division) # [B,S,L]
137
+ denom_z = (self.zaxis[iz_n[..., 2]] - self.zaxis[iz_n[..., 0]]).clamp(min=1e-12).to(dtype_for_division) # [B,S,L]
138
+
139
+ # Central difference at grid point (I_x, I_y, I_z) - which is index 1 in the 4-neighbour array
140
+
141
+ # fxy
142
+ fx_y_plus = m_x_subgrid[..., 1, 2, 1];
143
+ fx_y_minus = m_x_subgrid[..., 1, 0, 1]
144
+ fxy_base = (fx_y_plus - fx_y_minus) / denom_y
145
+
146
+ # fxz
147
+ fx_z_plus = m_x_subgrid[..., 1, 1, 2];
148
+ fx_z_minus = m_x_subgrid[..., 1, 1, 0]
149
+ fxz_base = (fx_z_plus - fx_z_minus) / denom_z
150
+
151
+ # fyz
152
+ fy_z_plus = m_y_subgrid[..., 1, 1, 2];
153
+ fy_z_minus = m_y_subgrid[..., 1, 1, 0]
154
+ fyz_base = (fy_z_plus - fy_z_minus) / denom_z
155
+
156
+ # fxyz
157
+ fx_yplus_zplus = m_x_subgrid[..., 1, 2, 2];
158
+ fx_yminus_zplus = m_x_subgrid[..., 1, 0, 2]
159
+ fxy_z_plus = (fx_yplus_zplus - fx_yminus_zplus) / denom_y
160
+ fx_yplus_zminus = m_x_subgrid[..., 1, 2, 0];
161
+ fx_yminus_zminus = m_x_subgrid[..., 1, 0, 0]
162
+ fxy_z_minus = (fx_yplus_zminus - fx_yminus_zminus) / denom_y
163
+ fxyz_base = (fxy_z_plus - fxy_z_minus) / denom_z
164
+
165
+ # --- 2. Build the $4 \times 4 \times 4$ Coefficient Tensor $\mathbf{P}$ ---
166
+ P = torch.zeros(target_shape, dtype=self.dtype, device=self.device)
167
+
168
+ # Helper variables for broadcasting cell widths (must be same dtype as P)
169
+ dx_val = dx_cell.unsqueeze(-1).unsqueeze(-1).to(self.dtype)
170
+ dy_val = dy_cell.unsqueeze(-1).unsqueeze(-1).to(self.dtype)
171
+ dz_val = dz_cell.unsqueeze(-1).unsqueeze(-1).to(self.dtype)
172
+
173
+ # Extract the $2 \times 2 \times 2$ cell corner values (indices 1:3)
174
+ f_c = signal_subgrid[..., 1:3, 1:3, 1:3]
175
+ mx_c = m_x_subgrid[..., 1:3, 1:3, 1:3]
176
+ my_c = m_y_subgrid[..., 1:3, 1:3, 1:3]
177
+ mz_c = m_z_subgrid[..., 1:3, 1:3, 1:3]
178
+
179
+ # Populate the 8 sub-blocks of P (indices 0:2 and 2:4 correspond to value/derivative in basis)
180
+ P[..., 0:2, 0:2, 0:2] = f_c # f
181
+ P[..., 2:4, 0:2, 0:2] = mx_c * dx_val.unsqueeze(-1) # fx * dx
182
+ P[..., 0:2, 2:4, 0:2] = my_c * dy_val.unsqueeze(-1) # fy * dy
183
+ P[..., 0:2, 0:2, 2:4] = mz_c * dz_val.unsqueeze(-1) # fz * dz
184
+
185
+ # Use the central difference approximations for mixed derivatives, broadcasting to $2 \times 2 \times 2$
186
+ dxdy_val = (dx_val * dy_val).unsqueeze(-1)
187
+ dxdz_val = (dx_val * dz_val).unsqueeze(-1)
188
+ dydz_val = (dy_val * dz_val).unsqueeze(-1)
189
+ dxdydz_val = (dxdy_val * dz_val.unsqueeze(-1))
190
+
191
+ fxy_broadcast = fxy_base.unsqueeze(-1).unsqueeze(-1).unsqueeze(-1)
192
+ fxz_broadcast = fxz_base.unsqueeze(-1).unsqueeze(-1).unsqueeze(-1)
193
+ fyz_broadcast = fyz_base.unsqueeze(-1).unsqueeze(-1).unsqueeze(-1)
194
+ fxyz_broadcast = fxyz_base.unsqueeze(-1).unsqueeze(-1).unsqueeze(-1)
195
+
196
+ P[..., 2:4, 2:4, 0:2] = fxy_broadcast * dxdy_val # fxy * dx * dy
197
+ P[..., 2:4, 0:2, 2:4] = fxz_broadcast * dxdz_val # fxz * dx * dz
198
+ P[..., 0:2, 2:4, 2:4] = fyz_broadcast * dydz_val # fyz * dy * dz
199
+ P[..., 2:4, 2:4, 2:4] = fxyz_broadcast * dxdydz_val # fxyz * dx * dy * dz
200
+
201
+ # --- 3. Compute the final interpolation result using tensor contraction (einsum) ---
202
+ return torch.einsum('bsli, bslj, bslk, bslijk -> bsl', H_x, H_y, H_z, P)
203
+
204
+ def forward(self, coords):
205
+ """
206
+ Perform tricubic interpolation for a batch of coordinates.
207
+ This simplified version only handles pre-processing and post-processing.
208
+
209
+ Args:
210
+ coords (torch.Tensor): Tensor of shape [batchSize, num_shots, 3, shot_length].
211
+
212
+ Returns:
213
+ torch.Tensor: Interpolated values, shape [batchSize, num_shots, 1, shot_length].
214
+ """
215
+ batch_size, num_shots, D, shot_length = coords.shape
216
+ coords = coords.transpose(1,2).contiguous().view(batch_size, 1, D, num_shots*shot_length)
217
+ x_coords, y_coords, z_coords = coords.split(1, dim=2)
218
+
219
+ # --- PRE-PROCESSING: Reshape and Find Indices/Local Coords ---
220
+ x_coords = x_coords.squeeze(2) # -> [B, S, L]
221
+ y_coords = y_coords.squeeze(2)
222
+ z_coords = z_coords.squeeze(2)
223
+
224
+ B, S, L = x_coords.shape
225
+
226
+ # Find grid cell indices I and clamp
227
+ I_x = (torch.searchsorted(self.xaxis, x_coords.clamp(self.xaxis.min(), self.xaxis.max())) - 1).clamp(0, self.xaxis.numel() - 2)
228
+ I_y = (torch.searchsorted(self.yaxis, y_coords.clamp(self.yaxis.min(), self.yaxis.max())) - 1).clamp(0, self.yaxis.numel() - 2)
229
+ I_z = (torch.searchsorted(self.zaxis, z_coords.clamp(self.zaxis.min(), self.zaxis.max())) - 1).clamp(0, self.zaxis.numel() - 2)
230
+
231
+ # Compute cell widths (denominator for t)
232
+ dx_cell = (self.xaxis[I_x + 1] - self.xaxis[I_x])
233
+ dy_cell = (self.yaxis[I_y + 1] - self.yaxis[I_y])
234
+ dz_cell = (self.zaxis[I_z + 1] - self.zaxis[I_z])
235
+
236
+ # Compute local coordinates t (0 <= t < 1)
237
+ t_x = (x_coords - self.xaxis[I_x]) / dx_cell
238
+ t_y = (y_coords - self.yaxis[I_y]) / dy_cell
239
+ t_z = (z_coords - self.zaxis[I_z]) / dz_cell
240
+
241
+ # --- CORE LOGIC: Call the private interpolation method ---
242
+ interpolated_vals = self._interpolate_tricubic_hermite(
243
+ B, S, L, I_x, I_y, I_z, t_x.to(self.dtype), t_y.to(self.dtype), t_z.to(self.dtype), dx_cell, dy_cell, dz_cell
244
+ )
245
+
246
+ # --- POST-PROCESSING: Reshape back to API output format ---
247
+ return interpolated_vals.view(batch_size, 1, num_shots, shot_length).transpose(1,2)
248
+
249
+
250
+ if __name__ == "__ main __":
251
+ device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
252
+
253
+ # 1. Define parameters and complex data
254
+ B, Nx, Ny, Nz = 4, 10, 10, 10 # Batch=4, Grid=10x10x10
255
+ S, L, D = 8, 100, 3 # Shots=2, Length=100, Dim=3
256
+
257
+ # Example signal: 4D signal [B, Nx, Ny, Nz] which will become [B, S=1, Nx, Ny, Nz] internally
258
+ # Use complex64 to test type compatibility
259
+ signal = torch.randn(B, Nx, Ny, Nz, dtype=torch.complex64, device=device)
260
+
261
+ # Example trajectory coordinates: [B, S, D, L]
262
+ coords = (torch.rand(B, S, D, L) * 2 - 1).to(dtype=torch.float32, device=device)
263
+ # coords = coords.transpose(1,2).contiguous().view(B, 1, D, S*L)
264
+
265
+ # Ensure coordinates require grad for autograd test
266
+ coords.requires_grad_(True)
267
+
268
+ # 2. Instantiate and interpolate
269
+ interpolator = TricubicHermiteMAkima3D(signal)
270
+
271
+ print('\n--- Running Forward Pass ---')
272
+ output = interpolator(coords)
273
+ output = output.view(B, S, 1, L)
274
+
275
+ print(f"Input Signal Shape (B, Nx, Ny, Nz): {signal.shape}")
276
+ print(f"Internal Signal Shape (B, S, Nx, Ny, Nz): {interpolator.signal.shape}")
277
+ print(f"Input Coords Shape (B, S, D, L): {coords.shape}")
278
+ print(f"Interpolated Output Shape (B, S, C=1, L): {output.shape}")
279
+ print(f"Output Dtype: {output.dtype}")
280
+
281
+ # 3. Test Autograd Compatibility
282
+ dummy_loss = torch.sum(torch.abs(output))
283
+ print(f"\n--- Checking Autograd ---")
284
+ try:
285
+ # Perform backward pass
286
+ dummy_loss.backward()
287
+ print("Backward pass successful. The interpolator is differentiable.")
288
+
289
+ # Check if gradients were computed for the coordinates
290
+ grad_exists = coords.grad is not None and torch.any(coords.grad != 0)
291
+ print(f"Gradient w.r.t input coords exists: {grad_exists}")
292
+ except RuntimeError as e:
293
+ print(f"Backward pass failed: {e}")