
.. DO NOT EDIT.
.. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY.
.. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE:
.. "generated/gallery/01-course/03_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_01-course_03_spoiling.py>`
        to download the full example code.

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

.. _sphx_glr_generated_gallery_01-course_03_spoiling.py:


===========
3. Spoiling
===========

With a TR of 20 ms and a T2 of tens of milliseconds, your gradient echo still
has transverse magnetisation when the next pulse arrives. That pulse refocuses
part of it, and the leftover from one repetition adds to the signal of the
next: after a few hundred repetitions the signal settles into a steady state
that depends on T2 as well as on T1 and the flip angle. A *spoiled* gradient
echo removes that leftover, so the steady state is the simple one that every
T1-weighted protocol assumes.

In this lesson you spoil your sequence in two steps: first with a spoiler
gradient, then by cycling the phase of the RF pulse. After each step you
compare the signal with the ideally spoiled one.

**Learning objectives**

- Add a spoiler gradient, prescribed by how many cycles it winds across a
  voxel.
- Understand why a spoiler alone does not reach the ideally spoiled signal.
- Cycle the phase of the RF pulse and of the ADC from one repetition to the
  next, and read the phases back from the sequence.
- Choose a phase increment, and know why 117 degrees is the common one.

Previous: :doc:`02_echo_and_repetition_time`. Next: :doc:`04_labels_and_metadata`,
where you tell the reconstruction what each readout is.

.. GENERATED FROM PYTHON SOURCE LINES 30-42








.. GENERATED FROM PYTHON SOURCE LINES 43-48

Events
------

The scanner, the protocol, the readout and the phase encoding of the
previous lessons:

.. GENERATED FROM PYTHON SOURCE LINES 48-83

.. 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
    TR = 20e-3
    delta_k = 1 / FOV

    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(
        "x", flat_area=MATRIX * delta_k, flat_time=readout_time, system=system
    )
    adc = pp.make_adc(MATRIX, dwell=dwell, delay=gx.rise_time, system=system)
    gx_pre = pp.make_trapezoid("x", area=-gx.area / 2, duration=1e-3, system=system)
    gy_pre = pp.make_trapezoid("y", area=MATRIX / 2 * delta_k, duration=1e-3, system=system)
    phase_steps = np.arange(-MATRIX // 2, MATRIX // 2) / (MATRIX / 2)








.. GENERATED FROM PYTHON SOURCE LINES 84-94

Gradient spoiling
-----------------

A spoiler is a gradient played after the readout. It winds the phase of the
transverse magnetisation across each voxel, so that what is left integrates
to (nearly) nothing over the voxel. You prescribe it by what it does, the
number of cycles it winds across a voxel, and
:func:`~pypulseqpp.make_crusher` returns the shortest gradient that does it.
Here the voxel is the in-plane pixel, 1.7 mm, and the spoiler winds 4 cycles
along z, where it does not disturb the k-space of the readout.

.. GENERATED FROM PYTHON SOURCE LINES 94-100

.. code-block:: Python

    VOXEL = FOV / MATRIX
    SPOILER_CYCLES = 4.0

    spoiler = pp.make_crusher(SPOILER_CYCLES, VOXEL, channel="z", system=system)[0]
    print(f"spoiler {1e3 * pp.calc_duration(spoiler):.2f} ms")





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

 .. code-block:: none

    spoiler 2.00 ms




.. GENERATED FROM PYTHON SOURCE LINES 101-121

The spoiler takes time out of the TR delay. The repetition is now the pulse,
the encoding, the readout, the spoiler and the delay.

RF phase
--------

The second tool is the phase of the RF pulse. Every RF and ADC event has a
``phase_offset``, in radians, which you can change from one repetition to
the next without designing a new event. *RF spoiling* advances the phase by
an increment that itself grows by :math:`\Delta\varphi` every repetition:

.. math::

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

The leftover magnetisation a pulse refocuses was created some repetitions
earlier, and with this quadratic phase each earlier repetition comes back at
a different phase, so their sum averages out. The ADC gets the same phase as
the pulse, so the signal you want is not affected.

.. GENERATED FROM PYTHON SOURCE LINES 121-128

.. code-block:: Python



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









.. GENERATED FROM PYTHON SOURCE LINES 129-132

Your spoiled gradient echo, as a function of the spoiler and of the phase
increment, so you can compare them. With an increment of zero every phase is
zero, and only the spoiler acts.

.. GENERATED FROM PYTHON SOURCE LINES 132-175

.. code-block:: Python



    def spoiled_gre(cycles, increment_deg=0.0, flip_angle_deg=FLIP_ANGLE_DEG):
        """The gradient echo, with a spoiler and an RF phase cycle."""
        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,
        )
        spoiler = pp.make_crusher(cycles, VOXEL, channel="z", system=system)[0]
        played = (
            pp.calc_duration(rf, gz)
            + pp.calc_duration(gx_pre, gy_pre, gz_reph)
            + pp.calc_duration(gx, adc)
            + pp.calc_duration(spoiler)
        )
        tr_delay = pp.make_delay(
            pp.round_to_raster(TR - played, system.block_duration_raster)
        )
        seq = pp.Sequence(system)
        for repetition, step in enumerate(phase_steps):
            phase = rf_phase(repetition, increment_deg) % (2 * np.pi)
            rf.phase_offset = phase
            adc.phase_offset = phase
            seq.add_block(rf, gz)
            seq.add_block(gx_pre, pp.scale_grad(gy_pre, step), gz_reph)
            seq.add_block(gx, adc)
            seq.add_block(spoiler)
            seq.add_block(tr_delay)
        return seq


    seq = spoiled_gre(SPOILER_CYCLES, increment_deg=117.0)
    ok, errors = seq.check_timing()
    print(f"timing ok: {ok}")
    seq.paper_plot(tr=1)




.. image-sg:: /generated/gallery/01-course/images/sphx_glr_03_spoiling_001.png
   :alt: 03 spoiling
   :srcset: /generated/gallery/01-course/images/sphx_glr_03_spoiling_001.png
   :class: sphx-glr-single-img


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

 .. code-block:: none

    timing ok: True




.. GENERATED FROM PYTHON SOURCE LINES 176-178

The phases are stored in the blocks, where the scanner and the
reconstruction read them. The first six, read back from the sequence:

.. GENERATED FROM PYTHON SOURCE LINES 178-181

.. code-block:: Python

    first_pulses = [seq.get_block(1 + 5 * n).rf for n in range(6)]
    print([float(np.rad2deg(rf.phase_offset).round(1)) for rf in first_pulses])





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

 .. code-block:: none

    [0.0, 117.0, 351.0, 342.0, 90.0, 315.0]




.. GENERATED FROM PYTHON SOURCE LINES 182-199

Steady-state signal
--------------------

To see what each tool does, you play the sequence on a voxel. The simulation
below spreads 201 isochromats across one voxel and plays the repetition on
them a few hundred times: the pulse at its phase, relaxation until the echo
(where the signal is read with the ADC's phase), relaxation until the end of
the TR, and the spoiler, which winds each isochromat by its position. The
echo time and TR are read from your sequence, so the simulation plays what
you built. The ideally spoiled signal, the one with no leftover at all, is
the closed form

.. math::

    S = \sin\alpha\,\frac{1 - E_1}{1 - E_1\cos\alpha},\qquad E_1 = e^{-TR/T_1}.

The tissue has a T1 of 1000 ms and a T2 of 80 ms.

.. GENERATED FROM PYTHON SOURCE LINES 199-257








.. GENERATED FROM PYTHON SOURCE LINES 258-263

Gradient spoiling without RF spoiling
-------------------------------------

First without the phase cycle, at your 12 degrees and at 30 degrees, for
spoilers from nothing to 8 cycles across the voxel:

.. GENERATED FROM PYTHON SOURCE LINES 263-289

.. code-block:: Python

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

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





.. image-sg:: /generated/gallery/01-course/images/sphx_glr_03_spoiling_002.png
   :alt: 03 spoiling
   :srcset: /generated/gallery/01-course/images/sphx_glr_03_spoiling_002.png
   :class: sphx-glr-single-img





.. GENERATED FROM PYTHON SOURCE LINES 290-305

Below one cycle the voxel is not wound over the whole circle, and the signal
depends on the spoiler. Past about three cycles it does not any more, but it
settles above the ideally spoiled line: at 30 degrees, at more than one and a
half times the signal you want. No spoiler area removes this.

The reason is that the spoiler winds every repetition by the same phase. The
leftover a pulse refocuses has been wound and unwound by the same amount, and
comes back coherent. That is what the phase cycle breaks.

Phase increment
---------------

Now keep the 4-cycle spoiler and sweep the phase increment from 0 to 180
degrees, at three flip angles. The signal is divided by the ideally spoiled
one, so 1 is perfect spoiling.

.. GENERATED FROM PYTHON SOURCE LINES 305-343

.. 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
            ]
        )
        / ideally_spoiled(flip)
        for flip in INCREMENT_FLIPS
    }





.. image-sg:: /generated/gallery/01-course/images/sphx_glr_03_spoiling_003.png
   :alt: 03 spoiling
   :srcset: /generated/gallery/01-course/images/sphx_glr_03_spoiling_003.png
   :class: sphx-glr-single-img


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

 .. code-block:: none

      12 deg: 0.951 of the ideal at 117 deg
      30 deg: 0.967 of the ideal at 117 deg
      60 deg: 1.020 of the ideal at 117 deg




.. GENERATED FROM PYTHON SOURCE LINES 344-355

The curve is spiky. An increment that brings the phase back to a few values,
like 0 or 180 degrees, leaves the leftover as coherent as no cycle at all,
and each flip angle has a few bad increments of its own. 117 degrees is
within a few percent of the ideal at all three flip angles, which is why it
is the common choice; its good neighbourhood is narrow.

Combined spoiling
-----------------

Last, the steady state against the flip angle, with the spoiler alone and
with the spoiler and the 117 degree cycle:

.. GENERATED FROM PYTHON SOURCE LINES 355-381

.. code-block:: Python

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

    spoiler_only = [abs(steady_state(SPOILER_CYCLES, 0.0, f)[-1]) for f in FLIP_ANGLES]
    rf_spoiled = [
        abs(steady_state(SPOILER_CYCLES, 117.0, f, repetitions=500)[-1])
        for f in FLIP_ANGLES
    ]





.. image-sg:: /generated/gallery/01-course/images/sphx_glr_03_spoiling_004.png
   :alt: 03 spoiling
   :srcset: /generated/gallery/01-course/images/sphx_glr_03_spoiling_004.png
   :class: sphx-glr-single-img





.. GENERATED FROM PYTHON SOURCE LINES 382-402

With the spoiler alone, the curve peaks far from the Ernst angle and is
twice the ideal at large flip angles, by an amount that depends on T2. With
the phase cycle it follows the ideally spoiled curve, and peaks back at the
Ernst angle. A few percent remain, which depend on T2 and the flip angle:
keep that in mind if you fit T1 from this signal with the ideal model.

Specification
-------------

What this lesson built, stated the way you would ask an agent for it:

.. code-block:: text

   Add spoiling to the lesson-2 gradient echo: a z spoiler after the
   readout made with make_crusher for 4 cycles across the in-plane pixel,
   and RF spoiling with a 117 degree quadratic phase increment applied to
   both the RF and the ADC phase_offset. Simulate the steady state with an
   isochromat sum across one voxel (T1 1000 ms, T2 80 ms, TE and TR read
   from the sequence), and plot it against the spoiler area, the phase
   increment and the flip angle, next to the ideally spoiled signal.


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

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


.. _sphx_glr_download_generated_gallery_01-course_03_spoiling.py:

.. only:: html

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

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

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

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

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

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

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


.. only:: html

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

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