Note
Go to the end to download the full example code.
Gradient echo#
The two previous lessons acquired signal without spatial encoding. This lesson turns the experiment into a two-dimensional acquisition: the excitation becomes slice-selective, the echo is formed by reversing a gradient rather than by a refocusing pulse, and a phase encode changes the acquired k-space line from one repetition to the next. Every Cartesian sequence in the later lessons extends this structure.
The prewinder area determines the sample of the readout window at which the k-space trajectory crosses zero, and the last section measures the echo time that follows from it.
The representation these objects belong to is described in Events and blocks.
Learning objectives#
After this lesson, you should be able to:
create a slice-selective excitation with its selection gradient and rephaser;
derive the readout gradient, the prewinder and the phase-encode steps from the field of view and the matrix;
assemble one repetition from blocks and pad it to the repetition time;
read the sequence diagram and the Cartesian k-space raster;
relate the prewinder area to the echo position and the echo time of an asymmetric echo.
Prescription#
The field of view and the matrix fix the k-space extent and the sample spacing: a line spans \(N/\mathrm{FOV}\) in 1/m and is sampled every \(1/\mathrm{FOV}\). Nothing below reads a length in metres except through those two numbers.
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 = 12.0
REPETITION_TIME = 20e-3
Slice-selective excitation#
A gradient played during the pulse makes the resonance frequency a function of position, so the pulse’s bandwidth selects a slab of the prescribed thickness. Position within the slice maps to phase accumulated under the second half of the selection lobe, which the rephaser unwinds; without it the signal integrates to nothing across the slice.
With return_gz=True the factory also returns the selection gradient and
the rephaser.
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,
)
print(
f"selection {1e3 * gz.amplitude / 42.576e6:.2f} mT/m, "
f"bandwidth {4.0 / 2e-3 * 1e-3:.1f} kHz, "
f"rephaser {1e3 * pp.calc_duration(gz_reph):.2f} ms"
)
selection 9.39 mT/m, bandwidth 2.0 kHz, rephaser 0.56 ms
Readout and phase encoding#
The readout gradient is prescribed by the area its flat top has to cover, and
the acquisition window is delayed into that flat top by the rise time, so
that every sample is taken at constant amplitude. The dwell and the flat time
come from calc_adc_timing(), which puts the one on the ADC
raster and the other on the gradient raster. The prewinder moves the k-space
position to one end of the line before the readout traverses it, and the phase encode displaces the line
perpendicular to it; one phase-encode step is \(1/\mathrm{FOV}\), so the
largest of them is half the k-space extent.
dwell, readout_time = pp.calc_adc_timing(
MATRIX,
26e-6,
grad_raster_time=system.grad_raster_time,
adc_raster_time=system.adc_raster_time,
)
gx = pp.make_trapezoid(
channel="x", flat_area=MATRIX / FOV, flat_time=readout_time, system=system
)
adc = pp.make_adc(num_samples=MATRIX, dwell=dwell, delay=gx.rise_time, system=system)
gx_pre = pp.make_trapezoid(channel="x", area=-gx.area / 2, duration=1e-3, system=system)
gy_pre = pp.make_trapezoid(
channel="y", area=MATRIX / (2 * FOV), duration=1e-3, system=system
)
# One line per phase encode, from one edge of k-space to the other.
phase_encodes = np.linspace(-1.0, 1.0, MATRIX, endpoint=False)
print(
f"readout {1e3 * gx.amplitude / 42.576e6:.2f} mT/m, "
f"dwell {1e6 * adc.dwell:.1f} us, "
f"receiver bandwidth {1e-3 / adc.dwell:.1f} kHz, "
f"{1 / (adc.dwell * MATRIX):.0f} Hz per pixel"
)
readout 3.56 mT/m, dwell 30.0 us, receiver bandwidth 33.3 kHz, 260 Hz per pixel
One repetition#
Four blocks: the pulse with its selection gradient, the prewinders and the
rephaser together, the readout with the acquisition window, and a delay that
brings the repetition up to the prescribed repetition time.
scale_grad() takes the largest phase encode to the step
this repetition acquires.
def gradient_echo(prewinder_fraction=0.5):
"""The sequence, with the prewinder at the given fraction of the line."""
prewinder = pp.scale_grad(gx_pre, 2 * prewinder_fraction)
played = (
pp.calc_duration(rf, gz)
+ pp.calc_duration(prewinder, gy_pre, gz_reph)
+ pp.calc_duration(gx, adc)
)
seq = pp.Sequence(system=system)
for step in phase_encodes:
seq.add_block(rf, gz)
seq.add_block(prewinder, pp.scale_grad(gy_pre, step), gz_reph)
seq.add_block(gx, adc)
seq.add_block(
pp.make_delay(
pp.round_to_raster(
REPETITION_TIME - played, system.block_duration_raster
)
)
)
return seq
seq = gradient_echo()
ok, errors = seq.check_timing()
print(
f"timing {ok}, {seq.num_blocks} blocks, "
f"{seq.duration()[0]:.3f} s for {MATRIX} lines"
)
timing True, 512 blocks, 2.560 s for 128 lines
Sequence diagram and k-space#
One repetition, drawn against the others it repeats.
seq.paper_plot(tr=1)

The phase encodes make the raster; the prewinder and the ramps carry k-space between the lines and are not sampled.
pp.plot.plot_kspace(seq, plane="xy")

Asymmetric echo#
The prewinder area sets how much of the line the readout covers before the echo. Moving the k-space position only part of the way to the edge and shortening the readout by the same amount keeps the far edge of the line where it was, so the resolution is unchanged, and removes samples from the near side, which a partial-Fourier reconstruction then has to supply from conjugate symmetry. The echo time falls by the time those samples would have taken.
A sample advances k-space by \(1/\mathrm{FOV}\), so the prewinder has to cancel the ramp of the readout gradient plus one such step per sample taken before the echo. The additional half step is the offset from the start of the window to the centre of its first sample; including it in the prewinder places the echo on a sample rather than between two.
HALF_LINE = MATRIX // 2
FRACTIONS = (1.0, 0.75, 0.5, 0.25)
def asymmetric_readout(fraction):
"""The readout, its window and its prewinder, with a partial near side."""
before = round(fraction * HALF_LINE)
samples = before + HALF_LINE
readout = pp.make_trapezoid(
channel="x",
amplitude=gx.amplitude,
flat_time=pp.round_to_raster(samples * dwell, system.grad_raster_time),
system=system,
)
window = pp.make_adc(
num_samples=samples, dwell=dwell, delay=readout.rise_time, system=system
)
ramp_area = readout.amplitude * readout.rise_time / 2
prewinder = pp.make_trapezoid(
channel="x",
area=-(ramp_area + (before + 0.5) / FOV),
duration=pp.calc_duration(gx_pre),
system=system,
)
return readout, window, prewinder
def one_repetition(fraction):
"""A single repetition, at the largest phase encode."""
readout, window, prewinder = asymmetric_readout(fraction)
seq = pp.Sequence(system=system)
seq.add_block(rf, gz)
seq.add_block(prewinder, gy_pre, gz_reph)
seq.add_block(readout, window)
return seq
measured = []
for fraction in FRACTIONS:
k_adc, _, t_excitation, _, t_adc = one_repetition(fraction).calculate_kspacePP()
echo = int(np.argmin(np.abs(k_adc[0])))
measured.append(
{
"fraction": fraction,
"sample": echo,
"samples": k_adc.shape[1],
"echo_time": t_adc[echo] - t_excitation[0],
"kx": 2 * k_adc[0] * FOV / MATRIX,
}
)

near side samples echo at echo time
1.00 128 64 4.05 ms
0.75 112 48 3.58 ms
0.50 96 32 3.09 ms
0.25 80 16 2.62 ms
Every line reaches the same \(+k_\mathrm{max}\), so all four have the resolution the matrix prescribes; they differ in how far the near side is measured and in when the echo occurs. Acquiring a quarter of the near side removes three quarters of the samples on that side and shortens the echo time by the amount the figure reports.
The repetition time is unchanged throughout, so a shorter echo time here reduces the signal decay before the echo is measured; it does not shorten the scan. Shortening the scan is the subject of Segmented echo planar.
Total running time of the script: (0 minutes 0.314 seconds)