Note
Go to the end to download the full example code.
End-to-end CS reconstruction with sigpy, deepinv and mrpro#
PyGROG ships native adapters for the full preprocessing chain:
coil_compress(),coil_compress(),coil_compress()nlinv_calib(),nlinv_calib(),nlinv_calib()GrogInterpolator,GrogInterpolator,GrogInterpolator
plus operator wrappers for each framework’s solver:
GrogLinop (sigpy),
GrogLinearPhysics (deepinv) and
GrogLinearOp (mrpro).
Every adapter speaks the native shape convention of its target
framework, so the same noncartesian acquisition is fed to each pipeline
in its preferred container — numpy.ndarray for sigpy, a
(B, n_coils, n_samples) torch.Tensor for deepinv and a
mrpro.data.KData for mrpro — without any manual rearrangement.
For each framework we run the full chain
coil_compress → nlinv_calib → GrogInterpolator → L1-wavelet FISTA
starting from the same raw multi-coil 16-arm spiral acquisition of a T1 BrainWeb slice. This is an interoperability showcase, not a benchmark.
import numpy as np
import torch
import matplotlib.pyplot as plt
from brainweb_dl import get_mri
from mrinufft import get_operator, initialize_2D_spiral
from mrinufft.density import voronoi
/home/docs/checkouts/readthedocs.org/user_builds/pygrog/envs/latest/lib/python3.12/site-packages/mrinufft/_utils.py:67: UserWarning: Samples will be rescaled to [-pi, pi), assuming they were in [-0.5, 0.5)
warnings.warn(
image shape : (256, 256)
# spiral arms : 16
raw k-space : (8, 9600) (numpy)
target # coils : 4
sigpy: Full Preprocessing Pipeline via Native Adapters#
Run the complete chain using sigpy’s native numpy.ndarray
containers: coil compression → NLINV calibration → GROG gridding →
L1-wavelet FISTA reconstruction.
Every step is adapted from PyGROG via pygrog.interop.sigpy:
coil_compress()— PCA compression on flat(n_coils, n_samples)k-space.nlinv_calib()— self-calibration returning coil sensitivities and calibration patch.GrogInterpolator— GROG with interpolate() returning sparse k-space and GROG plan.GrogLinop— wraps the SparseFFT as a sigpysigpy.linop.Linopfor use in solvers.
print("\n[sigpy] coil_compress → nlinv_calib → GROG → L1-wavelet FISTA")
import sigpy as sp
from pygrog.interop import GrogLinop
from pygrog.interop import sigpy as pg_sigpy
from pygrog.operator import SparseFFT
# 1. Coil compression: (n_coils, n_samples) → (n_v, n_samples).
ksp_s_compressed, _vh_s = pg_sigpy.coil_compress(kspace_raw, n_compressed)
ksp_s_arms = ksp_s_compressed.reshape(n_compressed, n_shots, n_read)
# 2. NLINV self-calibration on a 24x24 patch. ``ret_cal=True`` returns
# both the coil sensitivity maps *and* the synthesised Cartesian
# calibration k-space patch needed by GROG — no need to fabricate one.
smaps_s, calib_s = pg_sigpy.nlinv_calib(
ksp_s_compressed,
coords.reshape(-1, 2),
shape,
cal_width=24,
max_iter=12,
ret_cal=True,
)
# 3. GROG planning + interpolation — calibration patch comes straight
# from NLINV.
grog_s = pg_sigpy.GrogInterpolator(
coords, shape, kernel_width=2, oversamp=1.25, image_shape=shape
)
grog_s.calc_interp_table(calib_s, lamda=0.01, precision=1)
sparse_s, plan_s = grog_s.interpolate(ksp_s_arms)
sparse_s_t = torch.as_tensor(sparse_s)
# pre_weights is flat (n_samples,); reshape to natural to align with the
# natural-shape sparse output ``(n_coils, *natural_shape)``.
pre_w = plan_s.pre_weights.reshape(*plan_s.natural_shape)
sparse_s_t = sparse_s_t * pre_w
# 4. SparseFFT + sigpy linop wrapper.
op_s = SparseFFT(plan=plan_s, smaps=smaps_s)
img_zf = op_s.adjoint(sparse_s_t).numpy()
# 5. L1-wavelet FISTA via ``LinearLeastSquares``.
M_sigpy = GrogLinop(op_s).H
W_sigpy = sp.linop.Wavelet(shape, axes=(-2, -1), wave_name="db4", level=3)
A_sigpy = M_sigpy * W_sigpy.H
y_sigpy = sparse_s_t.reshape(n_compressed, op_s.indices.shape[0]).numpy()
proxg = sp.prox.L1Reg(W_sigpy.oshape, lamda=5e-4)
sigpy_lipschitz = float(
sp.app.MaxEig(
A_sigpy.N,
dtype=np.complex64,
device=sp.cpu_device,
max_iter=80,
show_pbar=False,
).run()
)
wav_hat = sp.app.LinearLeastSquares(
A=A_sigpy,
y=y_sigpy,
proxg=proxg,
alpha=0.9 / sigpy_lipschitz,
max_iter=80,
show_pbar=False,
).run()
img_sigpy = np.abs(W_sigpy.H(wav_hat))
print(f" done — output shape {img_sigpy.shape}")
[sigpy] coil_compress → nlinv_calib → GROG → L1-wavelet FISTA
done — output shape (256, 256)
deepinv: Full Preprocessing Pipeline via Native Adapters#
Run the complete chain using deepinv’s native batched
torch.Tensor containers: coil compression → NLINV calibration →
GROG gridding → L1-wavelet FISTA reconstruction.
Every step is adapted from PyGROG via pygrog.interop.deepinv:
coil_compress()— PCA compression on(B, n_coils, n_samples)batched tensors.nlinv_calib()— self-calibration returning(B, n_v, *shape)coil sensitivities.GrogInterpolator— GROG with batched input/output handling.GrogLinearPhysics— wraps the SparseFFT as a deepinvdeepinv.physics.LinearPhysicsfor solvers.
print("\n[deepinv] coil_compress → nlinv_calib → GROG → L1-wavelet FISTA")
from deepinv.physics.functional import power_method
from deepinv.optim.prior import WaveletPrior
from deepinv.optim.data_fidelity import L2
from deepinv.optim.optimizers import optim_builder
from pygrog.interop import GrogLinearPhysics
from pygrog.interop import deepinv as pg_deepinv
ksp_d_flat = torch.as_tensor(kspace_raw).unsqueeze(0) # (1, n_coils, n_samples)
# 1. Coil compression on the batched tensor.
ksp_d_compressed, _vh_d = pg_deepinv.coil_compress(ksp_d_flat, n_compressed)
ksp_d_arms = ksp_d_compressed.reshape(1, n_compressed, n_shots, n_read)
# 2. NLINV self-calibration (returns smaps + calibration k-space patch).
smaps_d, calib_d = pg_deepinv.nlinv_calib(
ksp_d_compressed,
coords.reshape(-1, 2),
shape,
cal_width=24,
max_iter=12,
ret_cal=True,
) # smaps: (1, n_v, H, W); calib: (n_v, *cal_shape)
# 3. GROG using the NLINV-derived calibration patch.
grog_d = pg_deepinv.GrogInterpolator(
coords, shape, kernel_width=2, oversamp=1.25, image_shape=shape
)
grog_d.calc_interp_table(calib_d[0], lamda=0.01, precision=1)
sparse_d, plan_d = grog_d.interpolate(ksp_d_arms) # (1, n_v, *natural, kw)
sparse_d = sparse_d * plan_d.pre_weights.reshape(*plan_d.natural_shape).to(
sparse_d.dtype
)
# 4. SparseFFT + deepinv physics wrapper.
op_d = SparseFFT(plan=plan_d, smaps=smaps_d[0])
# 5. L1-wavelet FISTA via ``optim_builder``.
physics = GrogLinearPhysics(op_d)
y_di = sparse_d.reshape(1, n_compressed, op_d.indices.shape[0]).clone()
x_dagger = physics.A_adjoint(y_di) # (1, 1, H, W)
with torch.no_grad():
x0 = x_dagger / (x_dagger.norm() + 1e-12)
deepinv_lipschitz = power_method(
lambda v: physics.A_adjoint(physics.A(v)),
x0,
max_iter=100,
).item()
prior = WaveletPrior(wv="db4", wvdim=2, level=3, is_complex=True)
prior.explicit_prior = False
data_fidelity = L2()
params = {"stepsize": 0.9 / deepinv_lipschitz, "lambda": 1e-2, "a": 3}
recon = optim_builder(
iteration="FISTA",
prior=prior,
data_fidelity=data_fidelity,
early_stop=False,
max_iter=80,
params_algo=params,
verbose=False,
)
x_di = recon(y_di, physics)
img_deepinv = np.abs(x_di.squeeze().detach().cpu().numpy())
print(f" done — output shape {img_deepinv.shape}")
[deepinv] coil_compress → nlinv_calib → GROG → L1-wavelet FISTA
done — output shape (256, 256)
mrpro: Full Preprocessing Pipeline via Native Adapters#
Run the complete chain using mrpro’s native mrpro.data.KData
containers: coil compression → NLINV calibration → GROG gridding →
L1-wavelet FISTA reconstruction.
Every step is adapted from PyGROG via pygrog.interop.mrpro:
coil_compress()— dispatches to mrpro’s PCA compressor onKData.nlinv_calib()— self-calibration returning mrpro-ordered smaps and calibration patch.GrogInterpolator— GROG gridding with trajectory snapping onKData.GrogLinearOp— wraps the SparseFFT as anmrpro.operators.LinearOperatorfor solver composition.
print("\n[mrpro] coil_compress → nlinv_calib → GROG → L1-wavelet FISTA")
from mrpro.algorithms.optimizers import pgd
from mrpro.data import KData, KHeader, KTrajectory, SpatialDimension
from mrpro.operators import WaveletOp
from mrpro.operators.functionals import L1NormViewAsReal, L2NormSquared
from pygrog.interop import GrogLinearOp
from pygrog.interop import mrpro as pg_mrpro
# Build a minimal KData wrapping the raw acquisition.
kx_t = torch.as_tensor(coords[..., 1]).reshape(1, 1, 1, n_shots, n_read)
ky_t = torch.as_tensor(coords[..., 0]).reshape(1, 1, 1, n_shots, n_read)
kz_t = torch.zeros_like(kx_t)
traj = KTrajectory(kz=kz_t, ky=ky_t, kx=kx_t)
data_t = (
torch.as_tensor(kspace_raw).reshape(n_coils_full, 1, n_shots, n_read).unsqueeze(0)
)
spatial = SpatialDimension(z=1, y=shape[0], x=shape[1])
header = KHeader(
recon_matrix=spatial,
encoding_matrix=spatial,
recon_fov=SpatialDimension(z=1.0, y=1.0, x=1.0),
encoding_fov=SpatialDimension(z=1.0, y=1.0, x=1.0),
)
kdata = KData(header=header, data=data_t, traj=traj)
# 1. Coil compression — dispatches to mrpro's PCA compressor.
kdata = pg_mrpro.coil_compress(kdata, n_compressed)
# 2. NLINV self-calibration through pygrog (returns smaps + cal patch).
smaps_m, calib_m = pg_mrpro.nlinv_calib(
kdata,
cal_width=24,
max_iter=12,
ret_cal=True,
) # smaps: (n_v, 1, H, W); calib: (n_v, *cal_shape)
# 3. GROG — interpolation snaps the trajectory onto the grid and fuses
# the kernel-width axis into ``k0``; calibration patch comes from NLINV.
grog_m = pg_mrpro.GrogInterpolator(kdata, kernel_width=2, oversamp=1.25)
grog_m.calc_interp_table(calib_m, lamda=0.01, precision=1)
kdata_grog, plan_m = grog_m.interpolate(kdata)
# 4. SparseFFT + mrpro linop wrapper.
op_m = SparseFFT(plan=plan_m, smaps=smaps_m[:, 0])
# 5. L1-wavelet FISTA via ``pgd``.
mrpro_op = GrogLinearOp(op_m)
A_mr = mrpro_op.H
wavelet_op = WaveletOp(domain_shape=shape, dim=(-2, -1), wavelet_name="db4", level=3)
acq = A_mr @ wavelet_op.H
# k-space layout from the adapter: (other=1, coils, k2=1, k1, k0*kw)
y_mr = kdata_grog.data.squeeze(0).squeeze(1) # (coils, k1, k0*kw)
y_mr = y_mr * plan_m.pre_weights.to(y_mr.dtype).reshape(1, n_shots, -1)
f = 0.5 * L2NormSquared(target=y_mr, divide_by_n=False) @ acq
g = 1e-3 * L1NormViewAsReal(divide_by_n=False)
(init_wave,) = wavelet_op(torch.zeros(shape, dtype=torch.complex64))
mrpro_norm_init = torch.randn_like(init_wave)
mrpro_lipschitz = acq.operator_norm(
initial_value=mrpro_norm_init,
dim=None,
max_iterations=80,
).item()
stepsize = 0.9 / mrpro_lipschitz
(wave_hat,) = pgd(
f=f,
g=g,
initial_value=init_wave,
stepsize=stepsize,
max_iterations=80,
backtrack_factor=1.0,
)
(img_mr_t,) = wavelet_op.H(wave_hat)
img_mrpro = np.abs(img_mr_t.detach().cpu().numpy())
print(f" done — output shape {img_mrpro.shape}")
[mrpro] coil_compress → nlinv_calib → GROG → L1-wavelet FISTA
done — output shape (256, 256)
Display#
Total running time of the script: (0 minutes 9.219 seconds)