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

Densities, random walks & travelling salesman #206

Merged
merged 21 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
a26e6ca
Split trajectory.maths.py into a submodule
Daval-G Nov 1, 2024
0f12b7e
Add densities, random walk and travelling salesman trajectories
Daval-G Nov 1, 2024
e72f52e
fixup! Add densities, random walk and travelling salesman trajectories
Daval-G Nov 3, 2024
1665906
Add docstrings for random walk and TSP solver
Daval-G Nov 3, 2024
55b3ba4
fixup! Add densities, random walk and travelling salesman trajectories
Daval-G Nov 7, 2024
74b08f8
Add docstrings to TSP & sampling densities
Daval-G Nov 11, 2024
362ca9f
Add samplings to random walk initializations and fix boundaries
Daval-G Nov 23, 2024
fcf699b
Add utils to examples to show densities
Daval-G Nov 23, 2024
fdcda55
Add sampling density example skeleton
Daval-G Nov 24, 2024
3d2d043
Merge branch 'master' into chauffert_update
Daval-G Nov 24, 2024
2f631ca
Add descriptions to sampling density examples
Daval-G Nov 25, 2024
83b9081
Add dependency to pywavelet
Daval-G Nov 28, 2024
3fc0bba
Merge branch 'master' into chauffert_update
Daval-G Nov 28, 2024
82172be
Improve cutoff/decay density, fix dependencies
Daval-G Dec 2, 2024
34144b8
Gather extra dependencies, homogeneize extra imports
Daval-G Dec 10, 2024
e7589f2
Fix imports to pywavelets
Daval-G Dec 10, 2024
432df36
Fix black format in smaps
Daval-G Dec 10, 2024
4ff1bf0
Remove except duplicate in io/siemens
Daval-G Dec 10, 2024
cbb1f83
Merge branch 'master' into chauffert_update
chaithyagr Dec 11, 2024
e60da32
Update CI with new [extra] dependencies
Daval-G Dec 12, 2024
2f34957
[docs] Trigger documentation build
Daval-G Dec 16, 2024
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
85 changes: 50 additions & 35 deletions src/mrinufft/trajectories/__init__.py
Original file line number Diff line number Diff line change
@@ -1,55 +1,62 @@
"""Collection of trajectories and tools used for non-Cartesian MRI."""

from .densities import (
create_chauffert_density,
create_cutoff_decay_density,
create_energy_density,
create_fast_chauffert_density,
create_polynomial_density,
sample_from_density,
)
from .display import display_2D_trajectory, display_3D_trajectory, displayConfig
from .gradients import patch_center_anomaly
from .inits import (
initialize_2D_random_walk,
initialize_2D_travelling_salesman,
initialize_3D_random_walk,
initialize_3D_travelling_salesman,
)
from .tools import (
conify,
duplicate_along_axes,
precess,
radialize_center,
rotate,
shellify,
stack,
stack_spherically,
)
from .trajectory2D import (
initialize_2D_radial,
initialize_2D_spiral,
initialize_2D_fibonacci_spiral,
initialize_2D_cones,
initialize_2D_sinusoide,
initialize_2D_fibonacci_spiral,
initialize_2D_lissajous,
initialize_2D_polar_lissajous,
initialize_2D_propeller,
initialize_2D_rosette,
initialize_2D_radial,
initialize_2D_rings,
initialize_2D_polar_lissajous,
initialize_2D_lissajous,
initialize_2D_rosette,
initialize_2D_sinusoide,
initialize_2D_spiral,
initialize_2D_waves,
)

from .trajectory3D import (
initialize_3D_phyllotaxis_radial,
initialize_3D_golden_means_radial,
initialize_3D_wong_radial,
initialize_3D_park_radial,
initialize_3D_annular_shells,
initialize_3D_cones,
initialize_3D_floret,
initialize_3D_wave_caipi,
initialize_3D_seiffert_spiral,
initialize_3D_golden_means_radial,
initialize_3D_helical_shells,
initialize_3D_annular_shells,
initialize_3D_park_radial,
initialize_3D_phyllotaxis_radial,
initialize_3D_repi,
initialize_3D_seiffert_shells,
initialize_3D_seiffert_spiral,
initialize_3D_turbine,
initialize_3D_repi,
)

from .tools import (
stack,
rotate,
precess,
conify,
stack_spherically,
shellify,
duplicate_along_axes,
radialize_center,
)

from .display import (
displayConfig,
display_2D_trajectory,
display_3D_trajectory,
initialize_3D_wave_caipi,
initialize_3D_wong_radial,
)

from .gradients import patch_center_anomaly

__all__ = [
# Trajectories
"initialize_2D_radial",
"initialize_2D_spiral",
"initialize_2D_fibonacci_spiral",
Expand All @@ -75,6 +82,7 @@
"initialize_3D_seiffert_shells",
"initialize_3D_turbine",
"initialize_3D_repi",
# Tools
"stack",
"rotate",
"precess",
Expand All @@ -87,4 +95,11 @@
"display_2D_trajectory",
"display_3D_trajectory",
"patch_center_anomaly",
# Densities
"sample_from_density",
"create_cutoff_decay_density",
"create_polynomial_density",
"create_energy_density",
"create_chauffert_density",
"create_fast_chauffert_density",
]
134 changes: 134 additions & 0 deletions src/mrinufft/trajectories/densities.py
Daval-G marked this conversation as resolved.
Show resolved Hide resolved
Daval-G marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
"""Sampling densities and methods."""

import numpy as np
import numpy.fft as nf
import numpy.linalg as nl
import numpy.random as nr
chaithyagr marked this conversation as resolved.
Show resolved Hide resolved
import pywt as pw
from sklearn.cluster import BisectingKMeans, KMeans
from tqdm.auto import tqdm

from .utils import KMAX


def sample_from_density(nb_samples, density, method="random"):
rng = nr.default_rng()
chaithyagr marked this conversation as resolved.
Show resolved Hide resolved

shape = density.shape
nb_dims = len(shape)
max_nb_samples = np.prod(shape)

if nb_samples > max_nb_samples:
raise ValueError("`nb_samples` must be lower than the size of `density`.")

if method == "random":
choices = rng.choice(
np.arange(max_nb_samples),
size=nb_samples,
p=density.flatten(),
replace=False,
)
locations = np.indices(shape).reshape((nb_dims, -1))[:, choices]
locations = locations.T
locations = 2 * KMAX * locations / np.max(locations) - KMAX
elif method == "lloyd":
kmeans = (
KMeans(n_clusters=nb_samples)
if nb_dims <= 2
else BisectingKMeans(n_clusters=nb_samples)
)
kmeans.fit(
np.indices(density.shape).reshape((nb_dims, -1)).T,
sample_weight=density.flatten(),
)
locations = kmeans.cluster_centers_ - np.array(density.shape) / 2
locations = KMAX * locations / np.max(np.abs(locations))
else:
raise ValueError(f"Unknown sampling method {method}.")
return locations


def create_cutoff_decay_density(shape, cutoff, decay, resolution=None):
Copy link
Member

Choose a reason for hiding this comment

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

How about a more generic density defined in Eq. 10 in https://onlinelibrary.wiley.com/doi/full/10.1002/mrm.29702 ?
Its a very short update.
Also, having support for elliptical sampling enabled or not can be helpful (we soon plan to disable it :P )

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I am confused, this density is already based on that equation. What difference do you see ?
And what do you mean by elliptical sampling ? Anisotropic ? Why do you plan to disable it and from where?

Copy link
Member

Choose a reason for hiding this comment

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

Anisotropic and more importantly elliptical sampling, a way to enable or disable mask.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Somehow that dodges all of my questions xD Could you reformulate pls ?

Copy link
Member

Choose a reason for hiding this comment

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

We need to support elliptical or non elliptical density

We also need to support anisotropy, but I think that we already do?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I had to fix it but I think it can now handle anisotropy and elliptical densities

shape = np.array(shape)
nb_dims = len(shape)

if not resolution:
resolution = np.ones(nb_dims)

distances = np.indices(shape).astype(float)
for i in range(nb_dims):
distances[i] = distances[i] + 0.5 - shape[i] / 2
distances[i] = distances[i] / shape[i] * resolution[i]
distances = nl.norm(distances, axis=0)

cutoff = cutoff * np.max(distances) if cutoff else np.min(distances)
density = np.ones(shape)
decay_mask = np.where(distances > cutoff, True, False)

density[decay_mask] = (cutoff / distances[decay_mask]) ** decay
density = density / np.sum(density)
return density


def create_polynomial_density(shape, decay, resolution=None):
return create_cutoff_decay_density(
shape, cutoff=0, decay=decay, resolution=resolution
)


def create_energy_density(dataset):
nb_dims = len(dataset.shape) - 1
axes = range(1, nb_dims + 1)

kspace = nf.fftshift(nf.fftn(nf.fftshift(dataset, axes=axes), axes=axes), axes=axes)
energy = np.mean(np.abs(kspace), axis=0)
density = energy / np.sum(energy)
return density


def create_chauffert_density(shape, wavelet_basis, nb_wavelet_scales, verbose=False):
nb_dims = len(shape)
indices = np.indices(shape).reshape((nb_dims, -1)).T

density = np.zeros(shape)
unit_vector = np.zeros(shape)
for ids in tqdm(indices, disable=not verbose):
ids = tuple(ids)
unit_vector[ids] = 1
fourier_vector = nf.ifftn(unit_vector)
coeffs = pw.wavedecn(
fourier_vector, wavelet=wavelet_basis, level=nb_wavelet_scales
)
Copy link
Member

Choose a reason for hiding this comment

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

You seem to be doing \Psi F*, the paper describes F* \psi right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I based this on the original code from Nicolas:

I checked that the code had an identical behavior, and it is the case except for one major difference: he used hardcoded wavelets banks and I use pywavelets instead, and their values seem to differ.

coeffs, _ = pw.coeffs_to_array(coeffs)
density[ids] = np.max(np.abs(coeffs)) ** 2
unit_vector[ids] = 0

density = density / np.sum(density)
return nf.ifftshift(density)


def create_fast_chauffert_density(shape, wavelet_basis, nb_wavelet_scales):
Copy link
Member

Choose a reason for hiding this comment

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

To keep in mind: https://arxiv.org/pdf/1910.14513, some other interesting results with Anna.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Is there something specific in the paper you would like to highlight ? Should we open an issue to create some extension in the future ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Also @chaithyagr I note in the paper figure that there is a TWIRL implementation sleeping somewhere in our cardboards. Do you know where it is so it can be added to the package ?

Copy link
Member

Choose a reason for hiding this comment

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

Isnt TWIRL just TPI in 2D? It should be fairly straightforward from our codes.

Copy link
Member

Choose a reason for hiding this comment

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

Is there something specific in the paper you would like to highlight ? Should we open an issue to create some extension in the future ?

I think @philouc can comment better than me. I just felt its good to add in docs, Its been long having gone through this work, but from what I remember its extension of the earlier work for anisotropic sampling, which can be of interest too.. But surely not to be pursued in this work

nb_dims = len(shape)

density = np.ones(shape)
for k, s in enumerate(shape):
unit_vector = np.zeros(s)
density_1d = np.zeros(s)

for i in range(s):
unit_vector[i] = 1
fourier_vector = nf.ifft(unit_vector)
coeffs = pw.wavedec(
fourier_vector, wavelet=wavelet_basis, level=nb_wavelet_scales
)
coeffs, _ = pw.coeffs_to_array(coeffs)
density_1d[i] = np.max(np.abs(coeffs)) ** 2
unit_vector[i] = 0
Copy link
Member

Choose a reason for hiding this comment

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

Is it possible to vectorize this? How slow is it right now for 3D?

Copy link
Member

Choose a reason for hiding this comment

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

@paquiteau , maybe numba?

Copy link
Collaborator Author

@Daval-G Daval-G Nov 5, 2024

Choose a reason for hiding this comment

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

create_fast_chauffert_density here is super fast for any matrix size. It is not the same for create_chauffert_density above

pywavelets allows the vectorization, but for create_chauffert_density we are speaking about an NxN matrix with N the total number of voxels. We explicit the linear operator to find for each row the max L2 norm. There are probably smart ways to save some computations though.


reshape = np.ones(nb_dims).astype(int)
reshape[k] = s
density_1d = density_1d.reshape(reshape)
density = density * density_1d

density = density / np.sum(density)
return nf.ifftshift(density)
14 changes: 14 additions & 0 deletions src/mrinufft/trajectories/inits/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""Module containing all trajectories."""

from .random_walk import initialize_2D_random_walk, initialize_3D_random_walk
from .travelling_salesman import (
initialize_2D_travelling_salesman,
initialize_3D_travelling_salesman,
)

__all__ = [
"initialize_2D_random_walk",
"initialize_3D_random_walk",
"initialize_2D_travelling_salesman",
"initialize_3D_travelling_salesman",
]
Loading
Loading