Note
Go to the end to download the full example code.
Multi-echo readouts#
The gradient echo of the first section acquires one echo per excitation. This lesson acquires several, by following the readout gradient with further readout gradients of alternating polarity. The rest of the repetition is unchanged, and the echoes sample the same k-space line at increasing echo times, from which a \(T_2^*\) estimate is computed.
The train is also the basis of the rest of this section: an echo planar readout is this train with a phase-encode blip between the echoes, which the next two lessons add. The measured quantities here are the echo spacing, which depends on the receiver bandwidth, and the number of echoes that fit in the repetition time at each bandwidth. The single-shot case is Single-shot echo planar.
Learning objectives#
After this lesson, you should be able to:
build a train of readout gradients of alternating polarity, each with its own ADC event;
locate the echoes of the train from the k-space analysis;
explain why the even echoes are acquired in reverse order;
relate the echo spacing and the train length to the receiver bandwidth, the gradient amplitude limit and the ramp times.
A train of readouts#
A readout gradient traverses one k-space line from one end to the other. A second gradient of the opposite polarity traverses it back, so a train of them needs no rewinder between the echoes and forms one echo per gradient. Every second echo is acquired in the opposite direction, and its samples are in the reverse order of the odd echoes’.
import numpy as np
import pypulseqpp as pp
system = pp.Opts(
max_grad=32.0,
grad_unit="mT/m",
max_slew=130.0,
slew_unit="T/m/s",
rf_dead_time=100e-6,
rf_ringdown_time=20e-6,
adc_dead_time=10e-6,
)
FOV = 220e-3
MATRIX = 128
THICKNESS = 5e-3
FLIP_ANGLE_DEG = 20.0
REPETITION_TIME = 40e-3
BANDWIDTH_HZ = 250e3
ECHOES = 6
rf, gz, gz_reph = pp.make_sinc_pulse(
flip_angle=np.deg2rad(FLIP_ANGLE_DEG),
duration=2e-3,
slice_thickness=THICKNESS,
apodization=0.5,
time_bw_product=4.0,
delay=system.rf_dead_time,
system=system,
use="excitation",
return_gz=True,
)
def readout(bandwidth_hz):
"""The readout gradient, its acquisition window and its prewinder.
The dwell time is put on the ADC raster and the flat top on the gradient
raster, which are different rasters, so the flat top is the acquisition
window rounded up rather than equal to it. The amplitude is set so that a
sample advances k-space by ``1 / FOV`` however long the window is.
"""
dwell = pp.round_to_raster(1.0 / bandwidth_hz, system.adc_raster_time)
acquisition = MATRIX * dwell
raster = system.grad_raster_time
gx = pp.make_trapezoid(
channel="x",
amplitude=MATRIX / FOV / acquisition,
flat_time=raster * np.ceil(acquisition / raster),
system=system,
)
adc = pp.make_adc(
num_samples=MATRIX, dwell=dwell, delay=gx.rise_time, system=system
)
# The prewinder cancels the ramp, one step per sample before the echo and
# the half step to the centre of the first sample.
gx_pre = pp.make_trapezoid(
channel="x",
area=-(gx.amplitude * gx.rise_time / 2 + (MATRIX / 2 + 0.5) / FOV),
duration=1e-3,
system=system,
)
return gx, adc, gx_pre
gx, adc, gx_pre = readout(BANDWIDTH_HZ)
gy_pre = pp.make_trapezoid(
channel="y", area=MATRIX / (2 * FOV), duration=1e-3, system=system
)
print(
f"dwell {1e6 * adc.dwell:.1f} us, "
f"readout {1e3 * pp.calc_duration(gx):.3f} ms, "
f"amplitude {1e3 * gx.amplitude / 42.576e6:.2f} mT/m, "
f"slew {gx.amplitude / gx.rise_time / 42.576e6:.0f} T/m/s"
)
dwell 4.0 us, readout 0.960 ms, amplitude 26.69 mT/m, slew 121 T/m/s
One repetition#
The train replaces the single readout block. The gradients of the train are played back to back, so the echo spacing is the duration of one readout gradient, ramps included.
def multi_echo(echoes, bandwidth_hz=BANDWIDTH_HZ, lines=MATRIX):
"""A multi-echo gradient echo with the given train length."""
gx, adc, gx_pre = readout(bandwidth_hz)
spoiler = pp.make_crusher(4.0, FOV / MATRIX, channel="z", system=system)[0]
played = (
pp.calc_duration(rf, gz)
+ pp.calc_duration(gx_pre, gy_pre, gz_reph)
+ echoes * pp.calc_duration(gx)
+ pp.calc_duration(spoiler)
)
seq = pp.Sequence(system=system)
for step in np.linspace(-1.0, 1.0, lines, endpoint=False):
seq.add_block(rf, gz)
seq.add_block(gx_pre, pp.scale_grad(gy_pre, step), gz_reph)
for echo in range(echoes):
seq.add_block(pp.scale_grad(gx, (-1.0) ** echo), adc)
seq.add_block(spoiler)
seq.add_block(
pp.make_delay(
pp.round_to_raster(
REPETITION_TIME - played, system.block_duration_raster
)
)
)
return seq
seq = multi_echo(ECHOES)
ok, errors = seq.check_timing()
print(
f"timing {ok}, {seq.num_blocks} blocks, "
f"{seq.duration()[0]:.3f} s for {ECHOES} echoes on each of {MATRIX} lines"
)
seq.paper_plot(tr=1)

timing True, 1280 blocks, 5.120 s for 6 echoes on each of 128 lines
Where the echoes land#
The analysis gives the k-space location of every sample of every acquisition window. Along the readout axis the train is a triangle wave between the two ends of the line, and an echo is where it crosses zero.
k_adc, _, t_excitation, _, t_adc = seq.calculate_kspacePP(block_range=[1, 3 + ECHOES])
kx = k_adc[0] * FOV / MATRIX * 2
echo_times = np.array(
[
t_adc[echo * MATRIX + int(np.argmin(np.abs(kx[echo * MATRIX :][:MATRIX])))]
- t_excitation[0]
for echo in range(ECHOES)
]
)
print(
"echo times (ms): "
+ ", ".join(f"{1e3 * time:.2f}" for time in echo_times)
+ f"\nspacing {1e3 * np.diff(echo_times).mean():.3f} ms, "
f"readout gradient {1e3 * pp.calc_duration(gx):.3f} ms"
)

echo times (ms): 2.56, 3.52, 4.48, 5.44, 6.40, 7.36
spacing 0.961 ms, readout gradient 0.960 ms
The samples the analysis reports are not one line acquired six times: the even echoes run from \(+k_\mathrm{max}\) to \(-k_\mathrm{max}\), so a reconstruction has to reverse them before they are lines of the same matrix. Any delay between the gradient and the acquisition then displaces the odd and the even echoes in opposite directions, which is the origin of the ghost a multi-echo or echo planar acquisition is corrected for.
Echo spacing against receiver bandwidth#
The readout gradient has to cover the same area whatever the bandwidth, so a shorter flat top is a proportionally stronger gradient, until the amplitude limit is reached and the flat top can shorten no further. The echo spacing follows the flat top, and the train that fits in a repetition follows the echo spacing.
# The dwell time is the quantity the ADC raster quantizes, so the sweep is over
# dwell times and the bandwidth is read from them.
DWELLS = np.array([16e-6, 12e-6, 10e-6, 8e-6, 6e-6, 4e-6, 2e-6])
BANDWIDTHS = 1.0 / DWELLS
overhead = (
pp.calc_duration(rf, gz)
+ pp.calc_duration(gx_pre, gy_pre, gz_reph)
+ pp.calc_duration(
pp.make_crusher(4.0, FOV / MATRIX, channel="z", system=system)[0]
)
)
spacing = []
for bandwidth in BANDWIDTHS:
try:
gradient = readout(bandwidth)[0]
except ValueError:
spacing.append({"bandwidth": bandwidth, "realizable": False})
continue
spacing.append(
{
"bandwidth": bandwidth,
"realizable": True,
"spacing": pp.calc_duration(gradient),
"ramps": gradient.rise_time + gradient.fall_time,
"amplitude": gradient.amplitude / 42.576e6,
"echoes": int((REPETITION_TIME - overhead) // pp.calc_duration(gradient)),
}
)
realizable = [row for row in spacing if row["realizable"]]

bandwidth amplitude spacing ramps echoes
62 kHz 6.67 mT/m 2.180 ms 6% 15
83 kHz 8.90 mT/m 1.700 ms 9% 20
100 kHz 10.68 mT/m 1.480 ms 14% 23
125 kHz 13.35 mT/m 1.280 ms 19% 27
167 kHz 17.79 mT/m 1.060 ms 26% 32
250 kHz 26.69 mT/m 0.960 ms 46% 36
500 kHz beyond the amplitude limit
The acquisition window falls as the reciprocal of the bandwidth; the echo spacing does not. A shorter window at the same k-space extent is a stronger gradient, and a stronger gradient takes longer to ramp, at both ends of every echo. Over the sweep the window shortens fourfold and the spacing by a factor of little more than two, with the ramps growing from a sixteenth of the echo spacing to nearly half of it. Beyond the last point the amplitude the readout would need is above the limit, and the factory raises an error rather than lengthening the flat top.
The shorter spacing reduces the signal-to-noise ratio. The noise in a sample grows as the square root of the bandwidth, so the fourfold bandwidth of the sweep halves the signal-to-noise ratio of each echo, in exchange for the number of echoes and a shorter shortest echo time.
Total running time of the script: (0 minutes 0.281 seconds)