Open In Colab

Echo Planar Imaging#

Qualitative imaging with an EPI readout.

Imports#

import importlib

if not importlib.util.find_spec('mrseq'):
    %pip install mrseq[examples]
import tempfile
from pathlib import Path

import matplotlib.pyplot as plt
import MRzeroCore as mr0
import numpy as np
from mrpro.algorithms.reconstruction import DirectReconstruction
from mrpro.data import KData
from mrpro.data.traj_calculators import KTrajectoryIsmrmrd
from raw2ismrmrd.utils import combine_ismrmrd_files

from mrseq.sequences.epi2d_fid import main as create_seq_fid
from mrseq.sequences.epi2d_se import main as create_seq_se
from mrseq.utils import sys_defaults
from mrseq.utils.EpiReadout import EpiReadout
/opt/hostedtoolcache/Python/3.12.14/x64/lib/python3.12/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
  from .autonotebook import tqdm as notebook_tqdm

Settings#

We are going to use a numerical phantom with a matrix size of 128 x 128.

image_matrix_size = [100, 100]
flip_angle_degree = 12
n_dummy_excitations = 200

tmp = tempfile.TemporaryDirectory()

Create the digital phantom#

We use the standard Brainweb phantom from MRzero, but we choose the B1-field to be constant everywhere.

phantom = mr0.util.load_phantom(image_matrix_size)
phantom.B1[:] = 1.0
Downloaded simulation data

EPI readouts#

Single-shot EPI readouts are a very efficient way to obtain a full image after a single excitation. There are different options of how they can be carried out.

We can have a symmetric readout where we alternate between going from left to right and right to left through k-space.

epi_readout = EpiReadout(n_readout=12, n_phase_encoding=12, ramp_sampling=False, readout_type='symmetric')
fig = epi_readout.plot_trajectory()
_images/b73c26e568febdbdcf10e1591361e32db6fc25960b33061d8dd584c55ddd8f2e.png

Here we are sampling only during the flat time of the readout gradient. We can make the sequence faster by also sampling during the ramp-up and ramp-down part of the readout gradient. Nevertheless, this means that we don’t have a strictly Cartesian trajectory anymore and gridding/nufft is required during image reconstruction.

epi_readout = EpiReadout(n_readout=12, n_phase_encoding=12, ramp_sampling=True, readout_type='symmetric')
fig = epi_readout.plot_trajectory()
_images/d94d6837c22ed27308f1fe63b9ba97474fd924e329c26a8bf3876ae1bc44b5cf.png

We can shorten the echo time by applying partial Fourier along the phase encoding direction.

epi_readout = EpiReadout(
    n_readout=12, n_phase_encoding=12, ramp_sampling=True, partial_fourier_factor=0.75, readout_type='symmetric'
)
fig = epi_readout.plot_trajectory()
_images/981dc78670041c01780f518ccdcf0c7209b171ef162b5b5e057b8d29d1a20990.png

Another approach is to always go back to one side of k-space after each readout such that the readout direction is always from left to right. This is less efficient but minimized any artifacts due to asymmetry between positive and negative readout gradients.

epi_readout = EpiReadout(
    n_readout=12, n_phase_encoding=12, partial_fourier_factor=0.75, ramp_sampling=True, readout_type='flyback'
)
fig = epi_readout.plot_trajectory()
_images/d85e457cdf0a021c3eb0a35981035a4c1d4dff9eb52fbef481534e9a7175918c.png

EPI sequences#

To create the EPI FID and SE sequences, we use the previously imported epi2d_fid script and epi2d_se script.

We are going to simulate different types of EPI sequences and compare the obtained images.

EPI FID with symmetric readout without ramp sampling#

sequence, fname_seq = create_seq_fid(
    system=sys_defaults,
    test_report=False,
    timing_check=False,
    show_plots=False,
    te=0.073,
    fov_xy=float(phantom.size.numpy()[0]),
    n_readout=image_matrix_size[0],
    n_phase_encoding=image_matrix_size[0],
    partial_fourier_factor=0.75,
    ramp_sampling=False,
    readout_type='symmetric',
)

mr0_sequence = mr0.Sequence.import_file(str(fname_seq.with_suffix('.seq')))
signal, ktraj_adc = mr0.util.simulate(mr0_sequence, phantom, accuracy=1e0)
fname_mrd = Path(tmp.name) / 'epi_fid_sym_no_ramp.mrd'
mr0.sig_to_mrd(fname_mrd, signal, sequence)
combine_ismrmrd_files(fname_mrd, Path(f'{fname_seq}_header.h5'))

kdata = KData.from_file(str(fname_mrd).replace('.mrd', '_with_traj.mrd'), trajectory=KTrajectoryIsmrmrd())
recon = DirectReconstruction(kdata, csm=None)
idata_fid_sym_no_ramp = recon(kdata)
Saving sequence file 'epi2d_fid_200fov_100px_sym_fid_2ro_nors_0p75pf_withnoise_withnav.seq' into folder '/home/runner/work/mrseq/mrseq/examples/output'.
>>>> Rust - compute_graph(...) >>>
Converting Python -> Rust: 0.000059608 s
Compute Graph
Computing Graph: 0.001833782 s
Analyze Graph
Analyzing Graph: 0.000001241 s
Converting Rust -> Python: 0.000008743 s
<<<< Rust <<<<
/opt/hostedtoolcache/Python/3.12.14/x64/lib/python3.12/site-packages/mrpro/data/KData.py:166: UserWarning: No vendor information found. Assuming Siemens time stamp format.
  warnings.warn('No vendor information found. Assuming Siemens time stamp format.', stacklevel=1)
/opt/hostedtoolcache/Python/3.12.14/x64/lib/python3.12/site-packages/mrpro/operators/FourierOp.py:124: UserWarning: If both FFT and NUFFT dims are present, Cartesian FFT dims need to be aligned with the k-space dimension, i.e. kx along k0, ky along k1 and kz along k2. We are going to use NUFFT for all dimensions. Creating your own FourierOp with the desired combination of FFT and NUFFT dimensions might be more efficient. 
  return cls(

EPI FID with symmetric readout and ramp sampling#

sequence, fname_seq = create_seq_fid(
    system=sys_defaults,
    test_report=False,
    timing_check=False,
    show_plots=False,
    te=0.073,
    fov_xy=float(phantom.size.numpy()[0]),
    n_readout=image_matrix_size[0],
    n_phase_encoding=image_matrix_size[0],
    partial_fourier_factor=0.75,
    readout_type='symmetric',
)

mr0_sequence = mr0.Sequence.import_file(str(fname_seq.with_suffix('.seq')))
signal, ktraj_adc = mr0.util.simulate(mr0_sequence, phantom, accuracy=1e0)
fname_mrd = Path(tmp.name) / 'epi_fid_sym_ramp.mrd'
mr0.sig_to_mrd(fname_mrd, signal, sequence)
combine_ismrmrd_files(fname_mrd, Path(f'{fname_seq}_header.h5'))

kdata = KData.from_file(str(fname_mrd).replace('.mrd', '_with_traj.mrd'), trajectory=KTrajectoryIsmrmrd())
recon = DirectReconstruction(kdata, csm=None)
idata_fid_sym_ramp = recon(kdata)
Saving sequence file 'epi2d_fid_200fov_100px_sym_fid_2ro_rs_0p75pf_withnoise_withnav.seq' into folder '/home/runner/work/mrseq/mrseq/examples/output'.
>>>> Rust - compute_graph(...) >>>
Converting Python -> Rust: 0.000135429 s
Compute Graph
Computing Graph: 0.002329183 s
Analyze Graph
Analyzing Graph: 0.000001513 s
Converting Rust -> Python: 0.000008282 s
<<<< Rust <<<<
/opt/hostedtoolcache/Python/3.12.14/x64/lib/python3.12/site-packages/mrpro/data/KData.py:166: UserWarning: No vendor information found. Assuming Siemens time stamp format.
  warnings.warn('No vendor information found. Assuming Siemens time stamp format.', stacklevel=1)
/opt/hostedtoolcache/Python/3.12.14/x64/lib/python3.12/site-packages/mrpro/operators/FourierOp.py:124: UserWarning: If both FFT and NUFFT dims are present, Cartesian FFT dims need to be aligned with the k-space dimension, i.e. kx along k0, ky along k1 and kz along k2. We are going to use NUFFT for all dimensions. Creating your own FourierOp with the desired combination of FFT and NUFFT dimensions might be more efficient. 
  return cls(

EPI FID with flyback readout and ramp sampling#

sequence, fname_seq = create_seq_fid(
    system=sys_defaults,
    test_report=False,
    timing_check=False,
    show_plots=False,
    te=0.073,
    fov_xy=float(phantom.size.numpy()[0]),
    n_readout=image_matrix_size[0],
    n_phase_encoding=image_matrix_size[0],
    partial_fourier_factor=0.75,
    readout_type='flyback',
)

mr0_sequence = mr0.Sequence.import_file(str(fname_seq.with_suffix('.seq')))
signal, ktraj_adc = mr0.util.simulate(mr0_sequence, phantom, accuracy=1e0)
fname_mrd = Path(tmp.name) / 'epi_fid_flyback_ramp.mrd'
mr0.sig_to_mrd(fname_mrd, signal, sequence)
combine_ismrmrd_files(fname_mrd, Path(f'{fname_seq}_header.h5'))

kdata = KData.from_file(str(fname_mrd).replace('.mrd', '_with_traj.mrd'), trajectory=KTrajectoryIsmrmrd())
recon = DirectReconstruction(kdata, csm=None)
idata_fid_flyback_ramp = recon(kdata)
Saving sequence file 'epi2d_fid_200fov_100px_flyb_fid_2ro_rs_0p75pf_withnoise_withnav.seq' into folder '/home/runner/work/mrseq/mrseq/examples/output'.
>>>> Rust - compute_graph(...) >>>
Converting Python -> Rust: 0.000160777 s
Compute Graph
Computing Graph: 0.002320018 s
Analyze Graph
Analyzing Graph: 0.000001302 s
Converting Rust -> Python: 0.000009785 s
<<<< Rust <<<<
/opt/hostedtoolcache/Python/3.12.14/x64/lib/python3.12/site-packages/mrpro/data/KData.py:166: UserWarning: No vendor information found. Assuming Siemens time stamp format.
  warnings.warn('No vendor information found. Assuming Siemens time stamp format.', stacklevel=1)
/opt/hostedtoolcache/Python/3.12.14/x64/lib/python3.12/site-packages/mrpro/operators/FourierOp.py:104: UserWarning: K-space points lie outside of the encoding_matrix and will be ignored. Increase the encoding_matrix to include these points.
  self._cart_sampling_op: CartesianSamplingOp | None = CartesianSamplingOp(

EPI SE with symmetric readout and ramp sampling#

sequence, fname_seq = create_seq_se(
    system=sys_defaults,
    test_report=False,
    timing_check=False,
    show_plots=False,
    n_slices=1,
    fov_xy=float(phantom.size.numpy()[0]),
    n_readout=image_matrix_size[0],
    n_phase_encoding=image_matrix_size[0],
    partial_fourier_factor=0.75,
    readout_type='symmetric',
)

mr0_sequence = mr0.Sequence.import_file(str(fname_seq.with_suffix('.seq')))
signal, ktraj_adc = mr0.util.simulate(mr0_sequence, phantom, accuracy=1e-6)
fname_mrd = Path(tmp.name) / 'epi_se_sym_ramp.mrd'
mr0.sig_to_mrd(fname_mrd, signal, sequence)
combine_ismrmrd_files(fname_mrd, Path(f'{fname_seq}_header.h5'))

kdata = KData.from_file(str(fname_mrd).replace('.mrd', '_with_traj.mrd'), trajectory=KTrajectoryIsmrmrd())
recon = DirectReconstruction(kdata, csm=None)
idata_se_sym_ramp = recon(kdata)
Saving sequence file 'epi2d_se_200fov_100px_sym_se_2ro_rs_0p75pf_withnoise_withnav.seq' into folder '/home/runner/work/mrseq/mrseq/examples/output'.
>>>> Rust - compute_graph(...) >>>
Converting Python -> Rust: 0.000090694 s
Compute Graph
Computing Graph: 0.002220831 s
Analyze Graph
Analyzing Graph: 0.000001453 s
Converting Rust -> Python: 0.000009705 s
<<<< Rust <<<<
/opt/hostedtoolcache/Python/3.12.14/x64/lib/python3.12/site-packages/mrpro/data/KData.py:166: UserWarning: No vendor information found. Assuming Siemens time stamp format.
  warnings.warn('No vendor information found. Assuming Siemens time stamp format.', stacklevel=1)
/opt/hostedtoolcache/Python/3.12.14/x64/lib/python3.12/site-packages/mrpro/operators/FourierOp.py:124: UserWarning: If both FFT and NUFFT dims are present, Cartesian FFT dims need to be aligned with the k-space dimension, i.e. kx along k0, ky along k1 and kz along k2. We are going to use NUFFT for all dimensions. Creating your own FourierOp with the desired combination of FFT and NUFFT dimensions might be more efficient. 
  return cls(
fig, ax = plt.subplots(2, 4, figsize=(16, 8))
for cax in ax.flatten():
    cax.set_xticks([])
    cax.set_yticks([])

for idx, (idata, label) in enumerate(
    zip(
        [idata_fid_sym_no_ramp, idata_fid_sym_ramp, idata_fid_flyback_ramp, idata_se_sym_ramp],
        ['symmetric no ramp', 'symmetric ramp', 'flyback ramp', 'se symmetric ramp'],
        strict=True,
    )
):
    idat = idata.data.abs().squeeze().numpy()
    idat /= np.sort(idat.flatten())[int(idat.size * 0.99)]
    if idx == 0:
        idat_ref = idat
        relative_error = 0.0
    relative_error += np.sum(np.abs(idat - idat_ref)) / np.sum(np.abs(idat_ref))

    ax[0, idx].imshow(idat, vmin=0, vmax=1, cmap='gray')
    ax[0, idx].set_title(label)
    ax[1, idx].imshow(idat - idat_ref, vmin=-1, vmax=1, cmap='bwr')

relative_error /= 3

print(f'Relative error {relative_error}')
assert relative_error < 0.20
Relative error 0.18769095838069916
_images/9e679332232728179df870aa12d11ec34d6d083046bd7fe78550515604eebae8.png