Gradient spoiling#

Open in Colab

The gradient echo of the previous section leaves transverse magnetisation at the end of each repetition, and the following pulses refocus part of it. This lesson adds a spoiler gradient after the acquisition. The spoiler dephases the remaining transverse magnetisation through several cycles across a voxel, so that it integrates to nearly zero there; because every repetition applies the same dephasing, a coherent pathway remains and contributes to the steady state.

The steady-state signal is computed by summing isochromats across a voxel over several hundred repetitions of the sequence built here, and compared with the signal of an ideally spoiled repetition. The next lesson, RF spoiling, suppresses the coherent pathway that gradient spoiling leaves.

Learning objectives#

After this lesson, you should be able to:

  • prescribe a spoiler gradient by its dephasing across a voxel and place it in the repetition;

  • compute a steady-state signal by an isochromat summation that uses the echo time, repetition time and spoiler area of the built sequence;

  • relate the steady-state signal to the spoiler area and to the flip angle;

  • explain why no spoiler area reproduces the ideally spoiled signal.

The spoiler#

The prescription is the same gradient echo, with one gradient added after the acquisition. A spoiler is stated as the phase it winds across a voxel, and the voxel here is the in-plane sample spacing of the prescription.

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
SPOILER_CYCLES = 4.0

VOXEL = FOV / MATRIX

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,
)

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 repetition#

The spoiler is played on the slice axis after the acquisition, where it dephases the transverse magnetisation the repetition leaves behind without adding to the k-space the readout traverses. It lengthens the repetition, so the delay that brings the repetition up to the repetition time absorbs less.

def gradient_spoiled(cycles, flip_angle_deg=FLIP_ANGLE_DEG):
    """The sequence, with a spoiler of the given dephasing across a voxel."""
    pulse, selection, rephaser = 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,
    )
    spoiler = pp.make_crusher(cycles, VOXEL, channel="z", system=system)[0]
    played = (
        pp.calc_duration(pulse, selection)
        + pp.calc_duration(gx_pre, gy_pre, rephaser)
        + pp.calc_duration(gx, adc)
        + pp.calc_duration(spoiler)
    )
    seq = pp.Sequence(system=system)
    for step in np.linspace(-1.0, 1.0, MATRIX, endpoint=False):
        seq.add_block(pulse, selection)
        seq.add_block(gx_pre, pp.scale_grad(gy_pre, step), rephaser)
        seq.add_block(gx, 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 = gradient_spoiled(SPOILER_CYCLES)

ok, errors = seq.check_timing()
spoiler_duration = pp.calc_duration(
    pp.make_crusher(SPOILER_CYCLES, VOXEL, channel="z", system=system)[0]
)
print(
    f"timing {ok}, {seq.num_blocks} blocks, "
    f"spoiler {1e3 * spoiler_duration:.2f} ms for {SPOILER_CYCLES:.0f} cycles "
    f"across a {1e3 * VOXEL:.2f} mm voxel"
)

seq.paper_plot(tr=1)
01 gradient spoiling
timing True, 640 blocks, spoiler 2.00 ms for 4 cycles across a 1.72 mm voxel

Isochromats across a voxel#

The steady state is reached by playing the repetition several hundred times on a set of isochromats spread across one voxel. Each one has a complex transverse component and a longitudinal one, and each repetition applies the pulse, the interval to the echo, the interval from the echo to the end of the repetition, and the phase the spoiler winds at that isochromat’s position.

The echo time and the repetition time come from the sequence that was built, and the spoiler phase from the dephasing it was prescribed with, so the summation follows the design rather than a restatement of it.

ISOCHROMATS = 201

_, _, t_excitation, _, t_adc = seq.calculate_kspacePP(block_range=[1, 5])
ECHO_TIME = float(t_adc[MATRIX // 2] - t_excitation[0])

print(
    f"echo time {1e3 * ECHO_TIME:.2f} ms, repetition time {1e3 * REPETITION_TIME:.2f} ms"
)


def steady_state(cycles, flip_angle_deg, t1=1000e-3, t2=80e-3, repetitions=600):
    """The signal the repetition settles at, summed across a voxel."""
    flip = np.deg2rad(flip_angle_deg)
    position = (np.arange(ISOCHROMATS) + 0.5) / ISOCHROMATS
    spoiler_phase = np.exp(2j * np.pi * cycles * position)

    transverse = np.zeros(ISOCHROMATS, dtype=complex)
    longitudinal = np.ones(ISOCHROMATS)
    signal = np.zeros(repetitions, dtype=complex)

    for repetition in range(repetitions):
        # The pulse, about an axis in the transverse plane.
        rotated = (
            np.cos(flip / 2) ** 2 * transverse
            + np.sin(flip / 2) ** 2 * np.conj(transverse)
            - 1j * np.sin(flip) * longitudinal
        )
        longitudinal = np.cos(flip) * longitudinal + np.sin(flip) * np.imag(transverse)
        transverse = rotated

        # Excitation to echo, where the signal is read.
        transverse *= np.exp(-ECHO_TIME / t2)
        longitudinal = 1.0 + (longitudinal - 1.0) * np.exp(-ECHO_TIME / t1)
        signal[repetition] = transverse.mean()

        # Echo to the end of the repetition, and the spoiler.
        rest = REPETITION_TIME - ECHO_TIME
        transverse *= np.exp(-rest / t2) * spoiler_phase
        longitudinal = 1.0 + (longitudinal - 1.0) * np.exp(-rest / t1)

    return signal


def ideally_spoiled(flip_angle_deg, t1=1000e-3):
    """The signal of a repetition that begins with no transverse component."""
    flip = np.deg2rad(flip_angle_deg)
    recovery = np.exp(-REPETITION_TIME / t1)
    return np.sin(flip) * (1 - recovery) / (1 - recovery * np.cos(flip))
echo time 4.05 ms, repetition time 20.00 ms

Steady state against spoiler area#

The sweep is run at the prescribed flip angle and at one large enough for the residual pathway to be substantial.

CYCLES = np.linspace(0.0, 8.0, 41)
SWEPT_FLIPS = (FLIP_ANGLE_DEG, 30.0)

against_area = {
    flip: np.array([abs(steady_state(cycles, flip)[-1]) for cycles in CYCLES])
    for flip in SWEPT_FLIPS
}
dashed: ideally spoiled, 30 deg, 4 cycles
at 30 degrees: ideally spoiled 0.0655, gradient spoiled 0.1054, rippling by 4.7 percent above three cycles

Below one cycle across a voxel the isochromats are not spread over the whole circle, and the steady state depends on how far the spoiler winds them. Above it the dependence is a ripple that decays as the reciprocal of the cycle count, because a uniform voxel wound through a non-integral number of cycles does not quite average to zero, and past about three cycles the curve is flat to the last digit.

The plateau consists of a pathway that the pulse refocuses from one repetition to the next: the spoiler winds every repetition through the same phase, so it leaves that pathway unchanged. At 30 degrees that residual is more than one and a half times the ideally spoiled signal it is being compared with, and no spoiler area removes it.

The right-hand panel shows the approach to the steady state at the prescribed spoiler, which takes a few hundred repetitions — long enough that the first lines of a scan are acquired before it.

Steady state against flip angle#

The residual pathway contains magnetisation that the pulse returns from the longitudinal axis, so it grows with the flip angle, and it adds to or subtracts from the ideally spoiled signal depending on where the flip angle sits.

FLIP_ANGLES = np.arange(2.0, 61.0, 2.0)

by_flip = np.array(
    [abs(steady_state(SPOILER_CYCLES, flip)[-1]) for flip in FLIP_ANGLES]
)
by_flip_ideal = ideally_spoiled(FLIP_ANGLES)
01 gradient spoiling
 flip   gradient spoiled   ideally spoiled   ratio
   10             0.0901            0.0991    0.91
   20             0.1087            0.0858    1.27
   30             0.1054            0.0655    1.61
   40             0.0948            0.0511    1.86
   50             0.0830            0.0410    2.02
   60             0.0719            0.0336    2.14
peak: ideally spoiled at 12 degrees, gradient spoiled at 22 degrees

The ideally spoiled curve peaks at the Ernst angle for this repetition time and T1, and its shape is what a signal model inverted for T1 assumes. The gradient-spoiled curve peaks well beyond it, falls below it at small flip angles and rises to twice it at large ones, and the whole departure depends on T2, which that model does not include. The RF phase cycle of the next lesson reduces this dependence.

Total running time of the script: (0 minutes 2.327 seconds)

Gallery generated by Sphinx-Gallery