Turbulent flow with velocity encoded spoiled GRE#

Imports#

[ ]:
!pip install -U cmrseq --index-url https://gitlab.ethz.ch/api/v4/projects/30300/packages/pypi/simple
[3]:
import sys
sys.path.append("../")
sys.path.insert(0, "../../")
# sys.path.insert(0, "../../../cmrseq")

import os
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3"
os.environ['CUDA_VISIBLE_DEVICES'] = "3"

import tensorflow as tf
gpu = tf.config.get_visible_devices("GPU")
if gpu:
    tf.config.experimental.set_memory_growth(gpu[0], True)
[4]:
from IPython.display import display, clear_output, Image

import pyvista
from pint import Quantity
import numpy as np
from tqdm.notebook import tqdm
import matplotlib.pyplot as plt
%matplotlib inline

import cmrsim
import cmrseq

sys.path.append("../")
import local_functions
clear_output()

Sequence Definition#

[5]:
# Define MR-system specifications
system_specs = cmrseq.SystemSpec(max_grad=Quantity(80, "mT/m"),
                                 max_slew=Quantity(200., "mT/m/ms"),
                                 grad_raster_time=Quantity(0.01, "ms"),
                                 rf_raster_time=Quantity(0.01, "ms"),
                                 adc_raster_time=Quantity(0.01, "ms"))

# Sequence Parameters (deliberately set too short --> get shortest)
TR = Quantity(5, 'ms')
TE = Quantity(0, 'ms')

# Define Fourier-Encoding parameters
fov = Quantity([222, 142], "mm")
matrix_size = (111, 71)
res = fov / matrix_size
print("Resolution:", res)

# Define imaging slice parameters
slice_thickness = Quantity(5, "mm")
slice_position = Quantity([0., 0., 0.], "m")
adc_duration = Quantity(4., "ms")
readout_direction = np.array([0., 0., 1.])   # readout along scanner Z, (FH)
slice_normal = np.array([1., 0., 0.])
slice_position_offset = Quantity(0, "cm")
rotation_matrix_to_mps = cmrseq.utils.get_rotation_matrix(slice_normal, readout_direction, target_orientation='mps').T
rotation_matrix_to_xyz = cmrseq.utils.get_rotation_matrix(slice_normal, readout_direction, target_orientation='xyz').T
print("MPS-rotation matrix to mps: \n", *rotation_matrix_to_mps)
print("MPS-rotation matrix to xyz: \n", *rotation_matrix_to_xyz)

# Define RF-exication parameters (coupled with seeding slice atm)
flip_angle = Quantity(np.pi / 12, "rad")
pulse_duration = Quantity(1, "ms")
RF_slice_normal = np.array([1., 0., 0.])  # Slice along scanner Y, (AP)
RF_slice_position = Quantity([0., 0., 0.], "m")
time_bandwidth_product = 4

# Define crusher area
crusher_duration = Quantity(1, "ms")
crusher_area = Quantity(np.pi * 4, "rad") / system_specs.gamma_rad / res[0]

# Velocity encoding settings:
venc_list = [Quantity(0., 'm/s'), Quantity(5.5, 'm/s'), Quantity(0.5, 'm/s'),
             Quantity(2.5, 'm/s'), Quantity(0.5, 'm/s'), Quantity(1, 'm/s'), Quantity(0.5, 'm/s')]
all_venc_directions = [np.array([0., 0., 1.]), *np.repeat(np.eye(3, 3)[::-1, :], 2, axis=0)]
print("Venc directions:\n", all_venc_directions)
venc_duration = Quantity(1, 'ms')  # Duration = 0 will result in shortest possible venc
all_venc_directions_MPS = [rotation_matrix_to_mps@d for d in all_venc_directions]
print("Rotated vencs:\n", all_venc_directions_MPS)
Resolution: [2.0 2.0] millimeter
MPS-rotation matrix to mps:
 [ 0. -0.  1.] [ 0. -1.  0.] [ 1. -0.  0.]
MPS-rotation matrix to xyz:
 [0. 0. 1.] [ 0. -1.  0.] [1. 0. 0.]
Venc directions:
 [array([0., 0., 1.]), array([0., 0., 1.]), array([0., 0., 1.]), array([0., 1., 0.]), array([0., 1., 0.]), array([1., 0., 0.]), array([1., 0., 0.])]
Rotated vencs:
 [array([1., 0., 0.]), array([1., 0., 0.]), array([1., 0., 0.]), array([ 0., -1.,  0.]), array([ 0., -1.,  0.]), array([0., 0., 1.]), array([0., 0., 1.])]

Each list of sequence objects in the variable multi_sequence_list corresponds to all repetitions for one VENC. The sequence illustration tries to highlight this structure.

[6]:
multi_sequence_list: list[list[cmrseq.Sequence]] = cmrseq.contrib.pc_gre_multivenc(
                                                                    system_specs, slice_thickness=slice_thickness,
                                                                    flip_angle=flip_angle, pulse_duration=pulse_duration,
                                                                    time_bandwidth_product=time_bandwidth_product,
                                                                    matrix_size=matrix_size, inplane_resolution=res,
                                                                    adc_duration=adc_duration, echo_time=TE,
                                                                    repetition_time=TR, slice_position_offset=slice_position_offset,
                                                                    venc_list=venc_list, venc_direction_list=all_venc_directions,
                                                                    venc_duration=venc_duration, crusher_area=crusher_area,
                                                                    crusher_duration=crusher_duration)

# Velocity encoding is already defined in XYZ, so only rotate imaging gradients to XYZ
for list_per_venc in multi_sequence_list:
    for seq_per_tr in list_per_venc:
        tmp_seq = seq_per_tr.partial_sequence(copy_blocks=False, partial_string_match="velocity", invert_pattern=True)
        tmp_seq.rotate_gradients(rotation_matrix_to_xyz)

clear_output()
[8]:
# This animation is created in the appendix of this notebook
display(Image("mr_sequence_animation.gif"))
<IPython.core.display.Image object>

Phantom preparation#

Load U-bend mesh#

[9]:
mesh = pyvista.read("../example_resources/sUbend_219_9_rotated.vtu")
mesh
[9]:
HeaderData Arrays
UnstructuredGridInformation
N Cells1034880
N Points1061341
X Bounds-1.603e-02, 1.602e-02
Y Bounds-6.312e-02, 5.603e-02
Z Bounds-1.000e-01, 9.140e-02
N Arrays7
NameFieldTypeN CompMinMax
UPointsfloat643-2.534e+005.380e+00
pPointsfloat641-3.984e+009.466e+00
RSTPointsfloat646-9.636e-012.484e+00
velocityPointsfloat643-2.534e-035.380e-03
ReflectionmapPointsfloat6411.000e+001.000e+00
FrequencyPointsfloat6414.770e-012.330e+03
cholPointsfloat646-2.362e+022.374e+02

Create 3D Rendering of mean velocity and the trace of Reynolds stress tensor on the meshed domain#

[15]:
_tmpmesh = mesh.copy()
_tmpmesh["abs_vel"] = np.linalg.norm(mesh["velocity"], axis=1)
_tmpmesh["trace_rst"] = np.mean(mesh["RST"][:, 0:3], axis=1)

pyvista.close_all()
pyvista.start_xvfb()
plotter = pyvista.Plotter(off_screen=True, window_size=(800, 400), shape=(1, 2))
plotter.subplot(0, 0)
plotter.add_mesh(_tmpmesh, scalars="abs_vel", scalar_bar_args=dict(title=r"$\| \vec{v} \|$"), cmap="twilight", opacity=0.5)
plotter.add_mesh(_tmpmesh.slice_orthogonal(), scalars="abs_vel", show_scalar_bar=False, cmap="twilight")
plotter.subplot(0, 1)
plotter.add_mesh(_tmpmesh, scalars="trace_rst", scalar_bar_args=dict(title="Tr(RST)"), cmap="rainbow", opacity=0.5)
plotter.add_mesh(_tmpmesh.slice_orthogonal(), scalars="trace_rst", show_scalar_bar=False, cmap="rainbow")
plotter.screenshot("input_mesh.png");
display(Image("input_mesh.png"))
../../_images/example_gallery_bloch_simulation_flow_venc_sGRE_turbUBend_12_0.png

Define Seeding volumes and options#

[16]:
# Define Reseeding slice parameters
seeding_slice_position = Quantity([0, 0.048, -0.045], "m")
seeding_slice_normal = np.array([1., 0., 0.])
seeding_slice_thickness = Quantity(50, "mm")
seeding_pixel_spacing = np.array((0.25e-3, 0.25e-3, 0.25e-3))  # resolution of seeding mesh, mm
seeding_slice_bb = Quantity([5, 5.1, 5], "cm")
seeding_particle_density = 5  # per 1/mm*3

# Define initial filling
initial_fill_normal = np.array([1., 0., 0.])
initial_fill_position = Quantity([-0.0005, 0., 0.], "m")
initial_fill_thickness = Quantity(50, "mm")
initial_fill_bounding_box = Quantity([25, 25, 25], "cm")
lookup_mesh_spacing = np.array((0.5e-3, 0.5e-3, 0.5e-3))  # resolution of lookup mesh, mm

Create 3D rendering of Re-seeding Volume at the inlet of the meshed domain#

[19]:
pyvista.close_all()
pyvista.start_xvfb()
plotter = pyvista.Plotter(off_screen=True, window_size=(800, 400), shape=(1, 2))
plotter.add_mesh(mesh, scalars=np.linalg.norm(mesh["velocity"], axis=1),
                 scalar_bar_args=dict(title=r"$\| \vec{v} \|$"), cmap="twilight", opacity=0.9)
box = local_functions.transformed_box(np.eye(3,3), slice_normal=seeding_slice_normal, readout_direction=np.array([0, 0, 1]),
                                      slice_position=seeding_slice_position, extend=seeding_slice_bb)
box2 = local_functions.transformed_box(np.eye(3,3), slice_normal=initial_fill_normal, readout_direction=np.array([0, 0, 1]),
                                       slice_position=initial_fill_position, extend=initial_fill_bounding_box)
box3 = local_functions.transformed_box(np.eye(3,3), slice_normal=slice_normal, readout_direction=readout_direction,
                                       slice_position=slice_position, extend=Quantity([*fov.m_as("cm"), slice_thickness.m_as("cm")], "cm"))

plotter.add_mesh(box, opacity=0.9, color="orange")
# plotter.add_mesh(box2, opacity=0.6, color="green")
plotter.subplot(0, 1)
plotter.add_mesh(mesh, scalars=np.linalg.norm(mesh["velocity"], axis=1),
                 scalar_bar_args=dict(title=r"$\| \vec{v} \|$"), cmap="twilight", opacity=0.9)
plotter.add_mesh(box3, opacity=0.6, color="gray")
local_functions.add_custom_axes(plotter)
plotter.screenshot("reseeding_box.png");
display(Image("reseeding_box.png"))
../../_images/example_gallery_bloch_simulation_flow_venc_sGRE_turbUBend_16_0.png

Instantiate RefillingFlowDataset#

[20]:
import cmrsim.utils.particle_properties as part_factory

# setup initial fill dict
initial_fill_dict = {'slice_normal': initial_fill_normal,
                     'slice_position': initial_fill_position.m_as('m'),
                     'slice_thickness': initial_fill_thickness.m_as('m'),
                     'slice_bounding_box': initial_fill_bounding_box.m_as('m')}

# Setup flow dataset with seeding mesh information
particle_creation_fncs = {"magnetization": part_factory.norm_magnetization(),
                          "T1": part_factory.uniform(default_value=300.),
                          "T2": part_factory.uniform(default_value=100.),
                          "M0": part_factory.uniform(default_value=1),
                          "omega_t2s": part_factory.t2star_lorentzian(T2p=50, prctile_cutoff=0.002)
                         }

dataset = cmrsim.datasets.RefillingFlowDataset(mesh,
                                               particle_creation_callables=particle_creation_fncs,
                                               slice_position=seeding_slice_position.m_as("m"),
                                               slice_normal=seeding_slice_normal,
                                               slice_thickness=seeding_slice_thickness.m_as("m"),
                                               slice_bounding_box=seeding_slice_bb.m_as("m"),
                                               lookup_map_spacing=lookup_mesh_spacing,
                                               seeding_vol_spacing=seeding_pixel_spacing,
                                               field_list=[("velocity", 3), ("chol", 6), ("Frequency", 1), ("Reflectionmap", 1)])
[21]:
# Setup turbulent particle trajectory module
lookup_table_3d, map_dimensions = dataset.get_lookup_table()
trajectory_module = cmrsim.trajectory.TurbulentTrajectory(velocity_field=lookup_table_3d.astype(np.float32),
                                                          map_dimensions=map_dimensions.astype(np.float32),
                                                          additional_dimension_mapping=dataset.field_list[3:],
                                                          device="GPU:0")

Simulation#

Setup modules#

  1. Define submodules to incorporate \(T_2^*\) effects

[22]:
T2s = cmrsim.bloch.submodules.T2starDistributionModel()
submodules = [T2s, ]
  1. Grid sequence objects (per TR)

[23]:
time_list = []
rf_list = []
grad_list = []
adc_list = []
for sequence_list in tqdm(multi_sequence_list, desc="Grid sequences per VENC..."):
    time, rf_grid, grad_grid, adc_on_grid = [np.stack(v) for v in cmrseq.utils.grid_sequence_list(sequence_list, force_uniform_grid=False)]
    time_list.append(time)
    rf_list.append(rf_grid)
    grad_list.append(grad_grid)
    adc_list.append(adc_on_grid)

time_ref = time_list[0][0]
adc_all = np.stack(adc_list, axis=1)
grad_all = np.stack(grad_list, axis=1)
rf_all = np.stack(rf_list, axis=1)
print("Bloch definition arrays: \n", time_ref.shape, "\n", adc_all.shape, "\n", grad_all.shape, "\n", rf_all.shape)
  1. Instantiate a Bloch-operator per TR (parallel execution of VENCS)

[24]:
bloch_operator_list = []
for tr in range(grad_all.shape[0]):
    bloch_operator_list.append(cmrsim.bloch.GeneralBlochOperator(name="rf", gamma=system_specs.gamma_rad.m_as("rad/mT/ms"),
                                                                 time_grid=time_ref, gradient_waveforms=grad_all[tr],
                                                                 rf_waveforms=rf_all[tr], adc_events=adc_all[tr], device="GPU:0",
                                                                 submodules=submodules))

Instantiate Simulator#

[25]:
from cmrsim.simulation_templates.flow import FlowSimulationMultiVenc
simulator = FlowSimulationMultiVenc(dataset, prep_modules=[], readout_modules=bloch_operator_list, trajectory_module=trajectory_module)

Run#

[26]:
r, m = simulator(particle_density=seeding_particle_density, dropping_distance=0.2,
                 averages=1, reseed_threshold=1, use_inital_filling=True,
                 initial_filling_dict=initial_fill_dict, return_final_magnetization=True)
[27]:
"Total number of particles at the end of simulation:", r.shape
[27]:
('Total number of particles at the end of simulation:', (1537101, 3))

Reconstruction#

[29]:
signal_list = []

for bloch_mod in bloch_operator_list:
    signal_list.append(tf.stack(bloch_mod.time_signal_acc, axis=0).numpy())

signals = np.stack(signal_list,axis=1)

image_list = []
kspace_list = []

for ind in range(np.shape(signals)[0]):
    time_signal = np.squeeze(signals[ind, :, :])
    n = (np.random.normal(0, 1, time_signal.shape)) * 0
    n = n*np.exp(1j*np.random.normal(0, 1, time_signal.shape))
    time_signal = time_signal + n

    center_index = np.array(matrix_size) // 2 + np.mod(matrix_size, 2)
    centered_projection = tf.signal.fft(time_signal).numpy()
    centered_k_space = tf.signal.fft(centered_projection).numpy()
    image_list.append(tf.signal.fftshift(tf.signal.ifft2d(tf.roll(tf.signal.ifftshift(centered_k_space, axes=(0, 1)),  -1, axis=1)), axes=(0,1)).numpy())
    kspace_list.append(centered_k_space)
[30]:
kspace_stacked = np.stack(kspace_list)
image_stacked = np.stack(image_list)
np.save("turb_images.npy", image_stacked)
np.save("turb_kspace.npy", kspace_stacked)
image_stacked.shape, kspace_stacked.shape
[30]:
((7, 71, 111), (7, 71, 111))

Infer velocity and turbulence#

  1. Get mask from reference amplitude:

[31]:
from skimage.morphology import opening, closing, remove_small_holes, disk

kspace_stacked = np.load("turb_kspace.npy")
image_stacked = np.load("turb_images.npy")

reference_image = image_stacked[0]
mask = np.abs(reference_image) > 550
mask = closing(remove_small_holes(opening(mask, disk(2))))
mask.shape, reference_image.shape
[31]:
((71, 111), (71, 111))
  1. Calculate velocity projections from phase differences

[32]:
phase_difference = np.mod(np.angle(image_stacked) - np.angle(reference_image) + np.pi, 2 * np.pi) - np.pi
masked_phase_difference = np.ma.masked_less(mask[np.newaxis], 0.5) * phase_difference
  1. Turbulent kinetic energy

[33]:
turb_estimation = np.log(np.abs(reference_image) / np.abs(image_stacked))
masked_trub_estimation = mask[np.newaxis] * turb_estimation

Plot results

[37]:
plt.rcParams["font.family"] = "serif"

kspace_stacked = np.load("turb_kspace.npy")
image_stacked = np.load("turb_images.npy")
concatenated_mask = np.ma.masked_less(np.concatenate([mask ] * 7, axis=1), 0.5)
concatenated_mask[concatenated_mask < 1] = np.nan

# Show images
plt.close("all")
fig, axes = plt.subplots(4, 1, figsize=(13, 7.), constrained_layout=True)
# axes[0].imshow(np.log(np.abs(np.concatenate(kspace_stacked, 1))), cmap="gray")

axes[0].imshow(np.abs(np.concatenate(image_stacked, axis=1)), cmap="gray", vmin=0,
               vmax=np.percentile(np.abs(np.squeeze(image_stacked[0])), 99.9))
axes[0].text(42, 10, "z", color="red", fontsize=12)
axes[0].arrow(x=10, y=14, dx=30, dy=0, color="red", width=1)
axes[0].text(12, 40, "y", color="green", fontsize=12)
axes[0].arrow(x=10, y=14, dx=0, dy=30, color="green", width=1)

phase_artist = axes[1].imshow(np.angle(np.concatenate(image_stacked, axis=1)), cmap="RdBu", vmin=-np.pi, vmax=np.pi)
fig.colorbar(phase_artist, location="bottom", orientation="horizontal", shrink=0.7, pad=0.04, fraction=0.8, aspect=100, label="rad")

axes[2].imshow(np.abs(np.concatenate(image_stacked, axis=1)) * concatenated_mask, cmap="gray", vmin=0, vmax=np.percentile(np.abs(np.squeeze(image_stacked[0])),99.9))
phase_arts = axes[2].imshow(np.concatenate(phase_difference, axis=1) * concatenated_mask, cmap="RdBu", vmin=-np.pi, vmax=np.pi, alpha=0.8)
fig.colorbar(phase_arts, location="bottom", orientation="horizontal", shrink=0.7, pad=0.04, fraction=0.8, aspect=100, label="rad")

axes[3].imshow(np.abs(np.concatenate(image_stacked, axis=1)) * concatenated_mask, cmap="gray", vmin=0, vmax=np.percentile(np.abs(np.squeeze(image_stacked[0])),99.9))
turb_arts = axes[3].imshow(np.concatenate(turb_estimation, axis=1) * concatenated_mask, cmap="gist_heat", vmin=0, vmax=1, alpha=0.8)
fig.colorbar(turb_arts, location="bottom", orientation="horizontal", shrink=0.7, pad=0.04, fraction=0.8, aspect=100, label="a.u.")



# axes[0].set_title((" " * 22).join([f"VENC #{i}" for i in range(image_stacked.shape[0])]))
title_string = (" " * 20).join(["No VENC"] + [f"{v.m_as('cm/s'):0.0f} cm/s" for v in venc_list[1:]])
title_string += "\n"
title_string += (" " * 20).join(["     ------      "] + [f"{v}" for v in all_venc_directions[1:]])

axes[0].set_title(title_string)
[_.set_ylabel(t, fontsize=14) for _, t in zip(axes, ["Magnitude", "$\phi$", "$\phi - \phi_{Ref}$", "TKE"])]
[_.set_xticks([]) for _ in axes.flatten()];
[_.set_yticks([]) for _ in axes.flatten()];
[axes[0].text(i/7, 1.325, "\u2193"+f"{t})", horizontalalignment='left', verticalalignment='top',
              transform=axes[0].transAxes, fontsize=16, fontweight="bold", color="k")
 for i, t in enumerate("abcdefg")]

# fig.savefig("Figure7.tiff", dpi=500)abs
fig.savefig("bloch_flow_venc_sGRE_turbUBend.png", dpi=200)
display(Image("bloch_flow_venc_sGRE_turbUBend.png"))
../../_images/example_gallery_bloch_simulation_flow_venc_sGRE_turbUBend_42_0.png

Appendix - Rendering#

Illustrate sequences#

[ ]:
import imageio
plt.ioff()
for tr_idx in tqdm(range(len(multi_sequence_list[0])), leave=False):
    plt.close("all");
    figure, axes = plt.subplot_mosaic("AABBCCDD;.EEFFGG.", figsize=(12, 5), sharex=True, sharey=True)
    axes = [axes[k] for k in "ABCDEFG"]
    for venc_idx in range(len(multi_sequence_list)):
        cmrseq.plotting.plot_sequence(multi_sequence_list[venc_idx][tr_idx], axes=axes[venc_idx], add_legend=False)
    [ax.set_title(f"VENC #{i}") for i, ax in enumerate(axes)]
    [(ax.set_ylabel(""), ax.set_yticklabels([])) for ax in axes[1:]]
    [(ax.set_ylabel(""), ax.set_yticklabels([])) for ax in figure.axes[8:]]
    figure.tight_layout()
    figure.savefig(f"venc_seq{tr_idx:02}.png")

plt.close("all")
figure, axis = plt.subplots(1, 1, figsize=(5, 5))
cmrseq.plotting.plot_kspace_2d(multi_sequence_list[0][-1], plot_raster_trajectory=False, ax=axis, k_axes=(1, 2), map_sampling_times="global", add_colorbar=True)
cmrseq.plotting.plot_kspace_2d(multi_sequence_list[0][0], plot_raster_trajectory=False, ax=axis, k_axes=(1, 2), map_sampling_times="global", add_colorbar=False)
xlims = axis.get_xlim()
ylims = axis.get_ylim()
figure.tight_layout()
axis.clear()
for idx, seq in tqdm(enumerate(multi_sequence_list[0]), total=71, leave=False):
    cmrseq.plotting.plot_kspace_2d(seq, plot_raster_trajectory=False, ax=axis, k_axes=(1, 2), map_sampling_times="global")
    axis.set_ylim(ylims); axis.set_xlim(xlims)
    figure.savefig(f"kspace{idx:02}")


_mr_seq_def_pngs = [imageio.v3.imread(f"venc_seq{idx:02}.png") for idx in tqdm(range(len(multi_sequence_list[0])), leave=False)]
_mr_kspace_pngs = [imageio.v3.imread(f"kspace{idx:02}.png") for idx in tqdm(range(len(multi_sequence_list[0])), leave=False)]
comb_pngs = [np.concatenate([i1, i2], axis=1) for i1, i2 in zip(_mr_seq_def_pngs, _mr_kspace_pngs)]
imageio.mimsave("mr_sequence_animation.gif", comb_pngs, duration=100, loop=0)

[os.remove(f"venc_seq{idx:02}.png") for idx in tqdm(range(len(multi_sequence_list[0])), leave=False)]
[os.remove(f"kspace{idx:02}.png") for idx in tqdm(range(len(multi_sequence_list[0])), leave=False)]