Skip to content
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

Draft
wants to merge 58 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 44 commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
8ca012c
Added
Apr 26, 2024
3a980c2
Merge branch 'mind-inria:master' into master
chaithyagr Apr 26, 2024
093edfb
Fix
Apr 26, 2024
f8a6a4a
Merge branch 'master' of github.com:chaithyagr/mri-nufft
Apr 26, 2024
74c1ecd
Remove bymistake add
Apr 26, 2024
0250aa8
Fix
Apr 26, 2024
060a8bd
Fixed lint
Apr 26, 2024
aecb844
Lint
Apr 26, 2024
3130bc1
Added refbackend
Apr 26, 2024
bc014b8
Fix NDFT
Apr 26, 2024
0cc73c4
feat: use finufft as ref backend.
paquiteau Apr 29, 2024
21e090f
feat(tests): move ndft vs nufft tests to own file.
paquiteau Apr 29, 2024
6869a4a
Merge branch 'master' of github.com:mind-inria/mri-nufft
Apr 29, 2024
f8364d4
Merge branch 'master' of github.com:mind-inria/mri-nufft
Apr 30, 2024
23a63da
Merge branch 'master' of github.com:mind-inria/mri-nufft
Jun 17, 2024
3709e74
Merge branch 'master' of github.com:mind-inria/mri-nufft
Jul 1, 2024
8d6b9b4
Merge branch 'master' of github.com:chaithyagr/mri-nufft
chaithyagr Aug 1, 2024
398fb28
Merge branch 'master' of github.com:mind-inria/mri-nufft
chaithyagr Sep 5, 2024
bcf7ce3
git Merge branch 'master' of github.com:mind-inria/mri-nufft
chaithyagr Sep 19, 2024
3da762f
Add support for pipe
chaithyagr Sep 20, 2024
d644cf6
\!docs_build try to run cufinufft tests
chaithyagr Sep 20, 2024
0dca8f6
\!docs_build fix style
chaithyagr Sep 20, 2024
643e1e9
Added next235 for stability
chaithyagr Sep 23, 2024
af6bbfa
Fix lint
chaithyagr Sep 23, 2024
02c834f
Fix CUPY
chaithyagr Sep 23, 2024
d50f427
Merge branch 'master' into cufinufft
chaithyagr Oct 24, 2024
8cfd427
WIP
chaithyagr Oct 24, 2024
ab6eaa4
merge
chaithyagr Oct 24, 2024
3c3f1c8
Updates
chaithyagr Oct 24, 2024
bb28eb9
fix back learn examples
chaithyagr Oct 25, 2024
cdf75af
move tto flatiron
chaithyagr Oct 25, 2024
986fb96
fix black
chaithyagr Oct 25, 2024
d4edc58
Move to test on GPU
chaithyagr Nov 8, 2024
1d01484
Update pyproject toml and use it in test-ci, to prevent duplication o…
chaithyagr Nov 13, 2024
9714ca9
Make CI build shorter
chaithyagr Nov 13, 2024
aba2c8f
Merge branch 'master' into cufinufft
chaithyagr Nov 13, 2024
78c60f9
Test run to run
chaithyagr Nov 13, 2024
d5fc2f6
Merge branch 'cufinufft' of github.com:chaithyagr/mri-nufft into cufi…
chaithyagr Nov 13, 2024
d844816
\!docs_build Added
chaithyagr Nov 13, 2024
2d58e02
Merge branch 'master' into cufinufft
chaithyagr Nov 14, 2024
7a6f4a0
adding density normalization
Dec 5, 2024
ddebaf5
Merge branch 'master' into cufinufft
chaithyagr Dec 6, 2024
cc08826
Merge branch 'master' into cufinufft
chaithyagr Dec 11, 2024
a94d530
Add support for 2.3.1
chaithyagr Dec 11, 2024
e5e7d10
Merge branch 'master' into cufinufft
chaithyagr Dec 20, 2024
67d6c75
Multiple fixes
chaithyagr Dec 20, 2024
906f363
Prevent circular import
chaithyagr Dec 20, 2024
9917045
fix Lint
chaithyagr Dec 20, 2024
119ee92
Fix
chaithyagr Dec 20, 2024
ba53be9
Some fixes and updates
chaithyagr Dec 20, 2024
3ef45a6
Update test-ci.yml
chaithyagr Jan 2, 2025
7989caa
Update test-ci.yml
chaithyagr Jan 2, 2025
f29a020
Updates
chaithyagr Jan 2, 2025
57b1ffa
Merge branch 'cufinufft' of github.com:chaithyagr/mri-nufft into cufi…
chaithyagr Jan 2, 2025
477349e
Make more fixes
chaithyagr Jan 2, 2025
5f21f6e
Careful repro test
chaithyagr Jan 2, 2025
134f70a
Fixes
chaithyagr Jan 6, 2025
35ca5b8
Fixes
chaithyagr Jan 6, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 19 additions & 64 deletions .github/workflows/test-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,29 +45,9 @@ jobs:
shell: bash
run: |
python -m pip install --upgrade pip
python -m pip install -e .[test]

- name: Install pynfft
if: ${{ matrix.backend == 'pynfft' || env.ref_backend == 'pynfft' }}
shell: bash
run: |
python -m pip install "pynfft2>=1.4.3"

- name: Install pynufft
if: ${{ matrix.backend == 'pynufft-cpu' || env.ref_backend == 'pynufft-cpu' }}
run: python -m pip install pynufft

- name: Install finufft
if: ${{ matrix.backend == 'finufft' || env.ref_backend == 'finufft'}}
shell: bash
run: python -m pip install finufft

- name: Install Sigpy
if: ${{ matrix.backend == 'sigpy' || env.ref_backend == 'sigpy'}}
shell: bash
run: python -m pip install sigpy

- name: Install BART
python -m pip install -e .[test,${{ env.ref_backend }},${{ matrix.backend }}]

- name: Install BART if needed
if: ${{ matrix.backend == 'bart' || env.ref_backend == 'bart'}}
shell: bash
run: |
Expand All @@ -79,11 +59,6 @@ jobs:
make
echo $PWD >> $GITHUB_PATH

- name: Install torchkbnufft-cpu
if: ${{ matrix.backend == 'torchkbnufft-cpu' || env.ref_backend == 'torchkbnufft-cpu'}}
run: python -m pip install torchkbnufft


- name: Run Tests
shell: bash
run: |
Expand Down Expand Up @@ -117,14 +92,7 @@ jobs:
source $RUNNER_WORKSPACE/venv/bin/activate
pip install --upgrade pip wheel
pip install -e mri-nufft[test]
pip install cupy-cuda12x finufft "numpy<2.0"

- name: Install torch with CUDA 12.1
shell: bash
if: ${{ matrix.backend != 'tensorflow'}}
run: |
source $RUNNER_WORKSPACE/venv/bin/activate
pip install torch

- name: Install backend
shell: bash
Expand All @@ -133,15 +101,7 @@ jobs:
export CUDA_BIN_PATH=/usr/local/cuda-12.1/
export PATH=/usr/local/cuda-12.1/bin/:${PATH}
export LD_LIBRARY_PATH=/usr/local/cuda-12.1/lib64/:${LD_LIBRARY_PATH}
if [[ ${{ matrix.backend }} == "torchkbnufft-gpu" ]]; then
pip install torchkbnufft
elif [[ ${{ matrix.backend }} == "tensorflow" ]]; then
pip install tensorflow-mri==0.21.0 tensorflow-probability==0.17.0 tensorflow-io==0.27.0 matplotlib==3.7
elif [[ ${{ matrix.backend }} == "cufinufft" ]]; then
pip install "cufinufft<2.3"
else
pip install ${{ matrix.backend }}
fi
pip install -e .[${{ matrix.backend }},autodiff]

- name: Run Tests
shell: bash
Expand Down Expand Up @@ -202,21 +162,18 @@ jobs:
path: ~/.cache/brainweb
key: ${{ runner.os }}-Brainweb

- name: Install Python deps
shell: bash
run: |
python -m pip install --upgrade pip
python -m pip install -e .[test,dev]
python -m pip install finufft pooch brainweb-dl torch fastmri

- name: Install GPU related interfaces
- name: Point to CUDA 12.1 #TODO: This can be combined from other jobs
Copy link
Member

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 ?

Copy link
Member Author

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

run: |
export CUDA_BIN_PATH=/usr/local/cuda-12.1/
export PATH=/usr/local/cuda-12.1/bin/:${PATH}
export LD_LIBRARY_PATH=/usr/local/cuda-12.1/lib64/:${LD_LIBRARY_PATH}
pip install cupy-cuda12x torch
python -m pip install gpuNUFFT "cufinufft<2.3" sigpy scikit-image fastmri

- name: Install Python deps
shell: bash
run: |
python -m pip install --upgrade pip
python -m pip install -e .[test,dev,finufft,cufinufft,gpuNUFFT,sigpy,smaps,autodiff,doc]

- name: Run examples
shell: bash
run: |
Expand Down Expand Up @@ -313,21 +270,19 @@ jobs:
with:
python-version: "3.10"

- name: Install dependencies
shell: bash -l {0}
run: |
python -m pip install --upgrade pip
python -m pip install .[doc]
python -m pip install finufft

- name: Install GPU related interfaces
- name: Point to CUDA 12.1
run: |
export CUDA_BIN_PATH=/usr/local/cuda-12.1/
export PATH=/usr/local/cuda-12.1/bin/:${PATH}
export LD_LIBRARY_PATH=/usr/local/cuda-12.1/lib64/:${LD_LIBRARY_PATH}
pip install cupy-cuda12x torch
python -m pip install gpuNUFFT "cufinufft<2.3"

- name: Install dependencies
shell: bash -l {0}
run: |
python -m pip install --upgrade pip
python -m pip install .[doc,finufft,autodiff,gpunufft,cufinufft]


- name: Build API documentation
run: |
python -m sphinx docs docs_build
Expand Down
20 changes: 19 additions & 1 deletion examples/GPU/example_density.py
Copy link
Member

Choose a reason for hiding this comment

The 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
Expand Up @@ -142,7 +142,7 @@
# If this method is widely used in the literature, there exists no convergence guarantees for it.
#
# .. note::
# The Pipe method is currently only implemented for gpuNUFFT.
# The Pipe method is currently only implemented for gpuNUFFT and cufinufft backend.

# %%
flat_traj = traj.reshape(-1, 2)
Expand All @@ -158,3 +158,21 @@
axs[2].imshow(abs(adjoint_manual))
axs[2].set_title("Pipe density compensation")
print(nufft.density)

# %%
# We can also do density compensation using cufinufft backend

# %%
flat_traj = traj.reshape(-1, 2)
nufft = get_operator("cufinufft")(
traj, shape=mri_2D.shape, density={"name": "pipe", "osf": 2}
)
adjoint_manual = nufft.adj_op(kspace)
fig, axs = plt.subplots(1, 3, figsize=(15, 5))
axs[0].imshow(abs(mri_2D))
axs[0].set_title("Ground Truth")
axs[1].imshow(abs(adjoint))
axs[1].set_title("no density compensation")
axs[2].imshow(abs(adjoint_manual))
axs[2].set_title("Pipe density compensation")
print(nufft.density)
2 changes: 1 addition & 1 deletion examples/GPU/example_learn_samples.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do you prefer cufinufft ?

Copy link
Member Author

Choose a reason for hiding this comment

The 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,
Expand Down
2 changes: 1 addition & 1 deletion examples/GPU/example_learn_samples_multicoil.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ def __init__(self, inital_trajectory, n_coils, img_size=(256, 256)):
squeeze_dims=False,
)
# A simple density compensated adjoint SENSE operator with sensitivity maps `smaps`.
self.sense_op = get_operator("gpunufft", wrt_data=True, wrt_traj=True)(
self.sense_op = get_operator("cufinufft", wrt_data=True, wrt_traj=True)(
sample_points,
shape=img_size,
density=True,
Expand Down
14 changes: 12 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Copy link
Member

Choose a reason for hiding this comment

The 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.
you could also do something like torchkbnufft-cpu = ["mri-nufft[torchkbnufft]"] to avoid repeating the dependency


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"]
Expand Down
15 changes: 12 additions & 3 deletions src/mrinufft/operators/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,21 @@

import numpy as np

from mrinufft._array_compat import with_numpy, with_numpy_cupy, AUTOGRAD_AVAILABLE
from mrinufft._array_compat import (
with_numpy,
with_numpy_cupy,
AUTOGRAD_AVAILABLE,
CUPY_AVAILABLE,
)
from mrinufft._utils import auto_cast, power_method
from mrinufft.density import get_density
from mrinufft.extras import get_smaps
from mrinufft.operators.interfaces.utils import is_cuda_array, is_host_array

if AUTOGRAD_AVAILABLE:
from mrinufft.operators.autodiff import MRINufftAutoGrad
if CUPY_AVAILABLE:
import cupy as cp


# Mapping between numpy float and complex types.
Expand Down Expand Up @@ -304,8 +311,10 @@ def compute_density(self, method=None):
if `backend` is `tensorflow`, `gpunufft` or `torchkbnufft-cpu`
or `torchkbnufft-gpu`.
"""
if isinstance(method, np.ndarray):
self._density = method
if isinstance(method, np.ndarray) or (
CUPY_AVAILABLE and isinstance(method, cp.ndarray)
):
self.density = method
return None
if not method:
self._density = None
Expand Down
106 changes: 93 additions & 13 deletions src/mrinufft/operators/interfaces/cufinufft.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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):
Expand All @@ -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)
Copy link
Member

Choose a reason for hiding this comment

The 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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could be refactored into a def _normalize_density(backend, samples, shape, density_comp) so that it could be used for other density compensation methods (e.g. voronoi)

return density_comp.squeeze()
Loading
Loading