-
Notifications
You must be signed in to change notification settings - Fork 13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for density compensation estimation with cufinufft #195
base: master
Are you sure you want to change the base?
Changes from 44 commits
8ca012c
3a980c2
093edfb
f8a6a4a
74c1ecd
0250aa8
060a8bd
aecb844
3130bc1
bc014b8
0cc73c4
21e090f
6869a4a
f8364d4
23a63da
3709e74
8d6b9b4
398fb28
bcf7ce3
3da762f
d644cf6
0dca8f6
643e1e9
af6bbfa
02c834f
d50f427
8cfd427
ab6eaa4
3c3f1c8
bb28eb9
cdf75af
986fb96
d4edc58
1d01484
9714ca9
aba2c8f
78c60f9
d5fc2f6
d844816
2d58e02
7a6f4a0
ddebaf5
cc08826
a94d530
e5e7d10
67d6c75
906f363
9917045
119ee92
ba53be9
3ef45a6
7989caa
f29a020
57b1ffa
477349e
5f21f6e
134f70a
35ca5b8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since there is two backend, you could plot the different density compensation vectors to show the differences (as cufinufft and gpunufft does not use the same interpolation kernel) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -54,7 +54,7 @@ def __init__(self, inital_trajectory): | |
data=torch.Tensor(inital_trajectory), | ||
requires_grad=True, | ||
) | ||
self.operator = get_operator("gpunufft", wrt_data=True, wrt_traj=True)( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why do you prefer cufinufft ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The idea is to run cufinufft with density compensation, basically increase the coverage |
||
self.operator = get_operator("cufinufft", wrt_data=True, wrt_traj=True)( | ||
self.trajectory.detach().cpu().numpy(), | ||
shape=(256, 256), | ||
density=True, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,11 +12,21 @@ dynamic = ["version"] | |
[project.optional-dependencies] | ||
|
||
gpunufft = ["gpuNUFFT>=0.9.0", "cupy-cuda12x"] | ||
|
||
torchkbnufft = ["torchkbnufft", "cupy-cuda12x"] | ||
cufinufft = ["cufinufft<2.3", "cupy-cuda12x"] | ||
finufft = ["finufft"] | ||
torchkbnufft-cpu = ["torchkbnufft", "cupy-cuda12x"] | ||
torchkbnufft-gpu = ["torchkbnufft", "cupy-cuda12x"] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. those "CI backends" could be put all together and with a comment above for explaining. |
||
|
||
cufinufft = ["cufinufft>=2.3.1", "cupy-cuda12x"] | ||
tensorflow = ["tensorflow-mri==0.21.0", "tensorflow-probability==0.17.0", "tensorflow-io==0.27.0", "matplotlib==3.7"] | ||
finufft = ["finufft>=2.3"] | ||
sigpy = ["sigpy"] | ||
pynfft = ["pynfft2>=1.4.3; python_version < '3.12'", "numpy>=2.0.0; python_version < '3.12'"] | ||
|
||
pynufft = ["pynufft"] | ||
pynufft-cpu = ["pynufft"] | ||
pynufft-gpu = ["pynufft"] | ||
|
||
io = ["pymapvbvd"] | ||
smaps = ["scikit-image"] | ||
autodiff = ["torch"] | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -40,9 +40,30 @@ | |
DTYPE_R2C = {"float32": "complex64", "float64": "complex128"} | ||
|
||
|
||
def _error_check(ier, msg): | ||
if ier != 0: | ||
raise RuntimeError(msg) | ||
def _next235beven(n, b): | ||
"""Find the next even integer not less than n. | ||
|
||
This function finds the next even integer not less than n, with prime factors no | ||
larger than 5, and is a multiple of b (where b is a number that only | ||
has prime factors 2, 3, and 5). | ||
It is used in particular with `pipe` density compensation estimation. | ||
""" | ||
if n <= 2: | ||
return 2 | ||
if n % 2 == 1: | ||
n += 1 # make it even | ||
nplus = n - 2 # to cancel out the +=2 at start of loop | ||
numdiv = 2 # a dummy that is >1 | ||
while numdiv > 1 or nplus % b != 0: | ||
nplus += 2 # stays even | ||
numdiv = nplus | ||
while numdiv % 2 == 0: | ||
numdiv //= 2 # remove all factors of 2, 3, 5... | ||
while numdiv % 3 == 0: | ||
numdiv //= 3 | ||
while numdiv % 5 == 0: | ||
numdiv //= 5 | ||
return nplus | ||
|
||
|
||
class RawCufinufftPlan: | ||
|
@@ -65,10 +86,12 @@ def __init__( | |
# and type 2 with 2. | ||
self.plans = [None, None, None] | ||
self.grad_plan = None | ||
|
||
self._kx = cp.array(samples[:, 0], copy=False) | ||
self._ky = cp.array(samples[:, 1], copy=False) | ||
self._kz = cp.array(samples[:, 2], copy=False) if self.ndim == 3 else None | ||
for i in [1, 2]: | ||
self._make_plan(i, **kwargs) | ||
self._set_pts(i, samples) | ||
self._set_pts(i) | ||
|
||
@property | ||
def dtype(self): | ||
|
@@ -88,13 +111,15 @@ def _make_plan(self, typ, **kwargs): | |
**kwargs, | ||
) | ||
|
||
def _set_pts(self, typ, samples): | ||
def _set_kxyz(self, samples): | ||
self._kx.set(samples[:, 0]) | ||
self._ky.set(samples[:, 1]) | ||
if self.ndim == 3: | ||
self._kz.set(samples[:, 2]) | ||
|
||
def _set_pts(self, typ): | ||
plan = self.grad_plan if typ == "grad" else self.plans[typ] | ||
plan.setpts( | ||
cp.array(samples[:, 0], copy=False), | ||
cp.array(samples[:, 1], copy=False), | ||
cp.array(samples[:, 2], copy=False) if self.ndim == 3 else None, | ||
) | ||
plan.setpts(self._kx, self._ky, self._kz) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. just to be sure, this does not create copies of the arrays ( maybe related to #147 as well) |
||
|
||
def _destroy_plan(self, typ): | ||
if self.plans[typ] is not None: | ||
|
@@ -274,10 +299,11 @@ def samples(self, samples): | |
self._samples = np.asfortranarray( | ||
proper_trajectory(samples, normalize="pi").astype(np.float32, copy=False) | ||
) | ||
self.raw_op._set_kxyz(self._samples) | ||
for typ in [1, 2, "grad"]: | ||
if typ == "grad" and not self._grad_wrt_traj: | ||
continue | ||
self.raw_op._set_pts(typ, self._samples) | ||
self.raw_op._set_pts(typ) | ||
self.compute_density(self._density_method) | ||
|
||
@FourierOperatorBase.density.setter | ||
|
@@ -810,7 +836,7 @@ def _make_plan_grad(self, **kwargs): | |
isign=1, | ||
**kwargs, | ||
) | ||
self.raw_op._set_pts(typ="grad", samples=self.samples) | ||
self.raw_op._set_pts(typ="grad") | ||
|
||
def get_lipschitz_cst(self, max_iter=10, **kwargs): | ||
"""Return the Lipschitz constant of the operator. | ||
|
@@ -849,3 +875,57 @@ def toggle_grad_traj(self): | |
if self.uses_sense: | ||
self.smaps = self.smaps.conj() | ||
self.raw_op.toggle_grad_traj() | ||
|
||
@classmethod | ||
def pipe( | ||
cls, | ||
kspace_loc, | ||
volume_shape, | ||
num_iterations=10, | ||
osf=2, | ||
normalize=True, | ||
**kwargs, | ||
): | ||
"""Compute the density compensation weights for a given set of kspace locations. | ||
|
||
Parameters | ||
---------- | ||
kspace_loc: np.ndarray | ||
the kspace locations | ||
volume_shape: np.ndarray | ||
the volume shape | ||
num_iterations: int default 10 | ||
the number of iterations for density estimation | ||
osf: float or int | ||
The oversampling factor the volume shape | ||
normalize: bool | ||
Whether to normalize the density compensation. | ||
We normalize such that the energy of PSF = 1 | ||
""" | ||
if CUFINUFFT_AVAILABLE is False: | ||
raise ValueError( | ||
"gpuNUFFT is not available, cannot " "estimate the density compensation" | ||
) | ||
original_shape = volume_shape | ||
volume_shape = np.array([_next235beven(int(osf * i), 1) for i in volume_shape]) | ||
grid_op = MRICufiNUFFT( | ||
samples=kspace_loc, | ||
shape=volume_shape, | ||
upsampfac=1, | ||
gpu_spreadinterponly=1, | ||
gpu_kerevalmeth=0, | ||
**kwargs, | ||
) | ||
density_comp = cp.ones(kspace_loc.shape[0], dtype=grid_op.cpx_dtype) | ||
for _ in range(num_iterations): | ||
density_comp /= cp.abs( | ||
grid_op.op( | ||
grid_op.adj_op(density_comp.astype(grid_op.cpx_dtype)) | ||
).squeeze() | ||
) | ||
if normalize: | ||
test_op = MRICufiNUFFT(samples=kspace_loc, shape=original_shape, **kwargs) | ||
test_im = cp.ones(original_shape, dtype=test_op.cpx_dtype) | ||
test_im_recon = test_op.adj_op(density_comp * test_op.op(test_im)) | ||
density_comp /= cp.mean(cp.abs(test_im_recon)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this could be refactored into a |
||
return density_comp.squeeze() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't get your comment, what combinaison do you have in mind ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We do this even in other places in the CI, we not really re-do it again and again