
.. DO NOT EDIT.
.. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY.
.. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE:
.. "generated/gallery/02-spoiling/01_spoiling.py"
.. LINE NUMBERS ARE GIVEN BELOW.

.. only:: html

    .. note::
        :class: sphx-glr-download-link-note

        :ref:`Go to the end <sphx_glr_download_generated_gallery_02-spoiling_01_spoiling.py>`
        to download the full example code.

.. rst-class:: sphx-glr-example-title

.. _sphx_glr_generated_gallery_02-spoiling_01_spoiling.py:


========
Spoiling
========

The gradient echo of the previous lesson,
:doc:`/generated/gallery/01-pulseq-basics/03_gradient_echo`, leaves transverse
magnetisation at the end of each repetition, and the following pulses refocus
part of it. This lesson first 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 lesson then suppresses that pathway by
advancing the phase of the RF pulse and of the receiver by a quadratically
increasing amount from one repetition to the next, and measures which phase
increments bring the steady-state signal close to that of ideal spoiling.

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,
:doc:`/generated/gallery/03-gre-to-epi/03_epi`, acquires many k-space lines
after one excitation.

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;
- explain why no spoiler area reproduces the ideally spoiled signal;
- write the quadratic phase cycle of RF spoiling, set the phase offset of the
  RF and ADC events per repetition, and read it back from the blocks;
- identify phase increments that leave the residual pathway coherent;
- compare the gradient-spoiled and RF-spoiled steady states with the ideally
  spoiled signal over flip angle.

.. GENERATED FROM PYTHON SOURCE LINES 40-50








.. GENERATED FROM PYTHON SOURCE LINES 51-57

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.

.. GENERATED FROM PYTHON SOURCE LINES 57-97

.. code-block:: Python


    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
    PHASE_INCREMENT_DEG = 117.0

    VOXEL = FOV / MATRIX

    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
    )








.. GENERATED FROM PYTHON SOURCE LINES 98-110

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.

``phase_offset`` is the phase of an RF event or an ADC event, in radians. The
function below sets both to the phase of the cycle introduced later in this
lesson; with an increment of zero every phase is zero, and the sequence is
gradient spoiled only.

.. GENERATED FROM PYTHON SOURCE LINES 110-168

.. code-block:: Python



    def transmit_phase(repetition, increment_deg):
        """The phase of one repetition's pulse and receiver, in radians."""
        return np.deg2rad(increment_deg) * repetition * (repetition + 1) / 2


    def spoiled(cycles, increment_deg=0.0, flip_angle_deg=FLIP_ANGLE_DEG):
        """The sequence, with a spoiler and an RF phase cycle of the given increment."""
        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)
        )
        recovery = pp.make_delay(
            pp.round_to_raster(REPETITION_TIME - played, system.block_duration_raster)
        )
        seq = pp.Sequence(system=system)
        steps = np.linspace(-1.0, 1.0, MATRIX, endpoint=False)
        for repetition, step in enumerate(steps):
            phase = transmit_phase(repetition, increment_deg) % (2 * np.pi)
            pulse.phase_offset = phase
            adc.phase_offset = phase
            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(recovery)
        return seq


    seq = 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)




.. image-sg:: /generated/gallery/02-spoiling/images/sphx_glr_01_spoiling_001.png
   :alt: 01 spoiling
   :srcset: /generated/gallery/02-spoiling/images/sphx_glr_01_spoiling_001.png
   :class: sphx-glr-single-img


.. rst-class:: sphx-glr-script-out

 .. code-block:: none

    timing True, 640 blocks, spoiler 2.00 ms for 4 cycles across a 1.72 mm voxel




.. GENERATED FROM PYTHON SOURCE LINES 169-183

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 at its transmit phase, 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 receiver is advanced with the transmitter, so the
transmit phase of the current repetition is removed from the recorded signal.

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.

.. GENERATED FROM PYTHON SOURCE LINES 183-239

.. code-block:: Python


    ISOCHROMATS = 201

    _, _, t_excitation, _, t_adc = seq.calculate_kspace(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, increment_deg, flip_angle_deg, t1=1000e-3, t2=80e-3, repetitions=600
    ):
        """The signal of each repetition, 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)
        rest = REPETITION_TIME - ECHO_TIME

        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 at the transmit phase.
            turn = np.exp(1j * transmit_phase(repetition, increment_deg))
            rotated = (
                np.cos(flip / 2) ** 2 * transverse
                + np.sin(flip / 2) ** 2 * turn**2 * np.conj(transverse)
                - 1j * np.sin(flip) * turn * longitudinal
            )
            longitudinal = np.cos(flip) * longitudinal + np.sin(flip) * np.imag(
                np.conj(turn) * transverse
            )
            transverse = rotated

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

            # Echo to the end of the repetition, and the spoiler.
            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))






.. rst-class:: sphx-glr-script-out

 .. code-block:: none

    echo time 4.05 ms, repetition time 20.00 ms




.. GENERATED FROM PYTHON SOURCE LINES 240-245

Steady state against spoiler area
---------------------------------

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

.. GENERATED FROM PYTHON SOURCE LINES 245-290

.. code-block:: Python


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

    against_area = {
        flip: np.array([abs(steady_state(cycles, 0.0, flip)[-1]) for cycles in CYCLES])
        for flip in SWEPT_FLIPS
    }





.. image-sg:: /generated/gallery/02-spoiling/images/sphx_glr_01_spoiling_002.png
   :alt: dashed: ideally spoiled, 30 deg, 4 cycles
   :srcset: /generated/gallery/02-spoiling/images/sphx_glr_01_spoiling_002.png
   :class: sphx-glr-single-img


.. rst-class:: sphx-glr-script-out

 .. code-block:: none


    at 30 degrees: ideally spoiled 0.0655, gradient spoiled 0.1054, rippling by 4.7 percent above three cycles




.. GENERATED FROM PYTHON SOURCE LINES 291-307

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.

.. GENERATED FROM PYTHON SOURCE LINES 309-328

The RF phase cycle
------------------

The transmit phase of repetition :math:`n` is advanced by :math:`n` times a
fixed increment, so that the phase itself grows quadratically:

.. math::

    \varphi_n = \varphi_{n-1} + n \, \Delta\varphi
            = \tfrac{1}{2} n (n + 1) \, \Delta\varphi .

The pathway a pulse refocuses from an earlier repetition arrives with a phase
that depends on how many repetitions ago it was produced, so a phase that is
not linear in :math:`n` puts successive repetitions' residuals at different
phases and their sum towards zero. The signal of the current repetition is
unaffected, because the receiver is advanced by the same amount.

The phase the events were written with is read back from the blocks, which
is where an interpreter and a reconstruction find it.

.. GENERATED FROM PYTHON SOURCE LINES 328-339

.. code-block:: Python


    rf_spoiled = spoiled(SPOILER_CYCLES, PHASE_INCREMENT_DEG)

    ok, errors = rf_spoiled.check_timing()
    print(f"timing {ok}, {rf_spoiled.num_blocks} blocks, {rf_spoiled.duration()[0]:.3f} s")

    written = np.array(
        [np.rad2deg(rf_spoiled.get_block(1 + 5 * n).rf.phase_offset) for n in range(6)]
    )
    print("transmit phase of the first repetitions (degrees): ", np.round(written, 1))





.. rst-class:: sphx-glr-script-out

 .. code-block:: none

    timing True, 640 blocks, 2.560 s
    transmit phase of the first repetitions (degrees):  [  0. 117. 351. 342.  90. 315.]




.. GENERATED FROM PYTHON SOURCE LINES 340-345

Steady state against phase increment
------------------------------------

The summation above, with the transmit phase of each repetition applied to the
pulse and removed from the signal, swept over the increment.

.. GENERATED FROM PYTHON SOURCE LINES 345-396

.. code-block:: Python


    INCREMENTS = np.arange(0.0, 181.0, 1.0)
    INCREMENT_FLIPS = (FLIP_ANGLE_DEG, 30.0, 60.0)

    against_increment = {
        flip: np.array(
            [
                abs(steady_state(SPOILER_CYCLES, step, flip, repetitions=500)[-1])
                for step in INCREMENTS
            ]
        )
        for flip in INCREMENT_FLIPS
    }





.. image-sg:: /generated/gallery/02-spoiling/images/sphx_glr_01_spoiling_003.png
   :alt: 01 spoiling
   :srcset: /generated/gallery/02-spoiling/images/sphx_glr_01_spoiling_003.png
   :class: sphx-glr-single-img


.. rst-class:: sphx-glr-script-out

 .. code-block:: none


     flip   at 117 deg              lowest             highest
       12        0.950      0.950 at  86      0.995 at   2
       30        0.967      0.897 at  32      1.609 at   0
       60        1.020      0.767 at  82      2.137 at   0




.. GENERATED FROM PYTHON SOURCE LINES 397-405

The curve is spiky rather than smooth. An increment that returns to a small
set of phases leaves the residual as coherent as no phase cycle at all: zero
and 180 degrees are the clearest, and each of the flip angles swept here has
others of its own. An increment of 117 degrees is within a few percent of the
ideally spoiled value at every one of them, which is the reason for its
common use, and its neighbourhood is narrow — at 60 degrees of flip, an
increment of 82 degrees gives a quarter less signal than the ideal and 117
gives it to two percent.

.. GENERATED FROM PYTHON SOURCE LINES 407-414

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. Both spoiling schemes are compared with the ideally spoiled signal.

.. GENERATED FROM PYTHON SOURCE LINES 414-465

.. code-block:: Python


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

    gradient_only = np.array(
        [abs(steady_state(SPOILER_CYCLES, 0.0, flip)[-1]) for flip in FLIP_ANGLES]
    )
    phase_cycled = np.array(
        [
            abs(
                steady_state(SPOILER_CYCLES, PHASE_INCREMENT_DEG, flip, repetitions=500)[-1]
            )
            for flip in FLIP_ANGLES
        ]
    )
    ideal = ideally_spoiled(FLIP_ANGLES)





.. image-sg:: /generated/gallery/02-spoiling/images/sphx_glr_01_spoiling_004.png
   :alt: 01 spoiling
   :srcset: /generated/gallery/02-spoiling/images/sphx_glr_01_spoiling_004.png
   :class: sphx-glr-single-img


.. rst-class:: sphx-glr-script-out

 .. code-block:: none


     flip   gradient spoiled   RF spoiled   ideally spoiled
       10             0.0901       0.0944            0.0991
       20             0.1087       0.0811            0.0858
       30             0.1054       0.0633            0.0655
       40             0.0948       0.0515            0.0511
       50             0.0830       0.0421            0.0410
       60             0.0719       0.0343            0.0336
    peak: ideally spoiled at 12 degrees, gradient spoiled at 22 degrees, RF spoiled at 12 degrees
    largest departure of RF spoiling from the ideally spoiled signal: 5.5 percent, at 20 degrees




.. GENERATED FROM PYTHON SOURCE LINES 466-478

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-spoiled curve lies with the ideally spoiled one over the whole range,
and its peak is back at the Ernst angle. A departure of a few percent
remains, which depends on T2 and on the flip angle: the phase cycle cancels
the residual pathway approximately rather than removing it, and a T1
estimated from this signal under an ideally spoiled model is biased by the
remainder.


.. rst-class:: sphx-glr-timing

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


.. _sphx_glr_download_generated_gallery_02-spoiling_01_spoiling.py:

.. only:: html

  .. container:: sphx-glr-footer sphx-glr-footer-example

    .. container:: sphx-glr-download sphx-glr-download-jupyter

      :download:`Download Jupyter notebook: 01_spoiling.ipynb <01_spoiling.ipynb>`

    .. container:: sphx-glr-download sphx-glr-download-python

      :download:`Download Python source code: 01_spoiling.py <01_spoiling.py>`

    .. container:: sphx-glr-download sphx-glr-download-zip

      :download:`Download zipped: 01_spoiling.zip <01_spoiling.zip>`


.. only:: html

 .. rst-class:: sphx-glr-signature

    `Gallery generated by Sphinx-Gallery <https://sphinx-gallery.github.io>`_
