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,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}")
|