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

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

.. _sphx_glr_generated_gallery_01-course_05_testing_on_the_virtual_scanner.py:


========================================
5. Testing on the virtual scanner
========================================

This lesson is optional: it extends the course rather than completing it. The
virtual scanner stands in for the scanner and nothing else, so a sequence and
a reconstruction plugin can be tested before a scanner is involved. It
acquires in two ways: along the played trajectory from an analytic phantom,
which leaves relaxation out, or by a Bloch simulation of every block the cache
plays. The conversion itself is checked by comparing the gradients a sequence
asks for with those its cache plays. The models are described in
:doc:`/explanations/virtual-scanner`.

**Learning objectives**

- Acquire a phantom with :func:`~pulserver.virtual.acquire` and simulate it on
  isochromats with :func:`~pulserver.virtual.simulate`, and state what each
  models.
- Check a cache against the sequence it was converted from with
  :func:`~pulserver.validate.validate`.

The previous lesson reconstructed a series with a plugin of its own.

.. GENERATED FROM PYTHON SOURCE LINES 25-31








.. GENERATED FROM PYTHON SOURCE LINES 32-39

A sequence with T1 contrast
---------------------------

The design is the 2D gradient echo at a 20 ms TR and a 30° flip angle,
without dummy scans, converted into its cache. The phantom holds two
ellipses of equal proton density and T2, with a T1 of 300 ms on the left and
1500 ms on the right.

.. GENERATED FROM PYTHON SOURCE LINES 39-61

.. code-block:: Python

    import tempfile
    from pathlib import Path

    import numpy as np
    import pypulseqpp as pp
    from pypulseqpp.sequences.sequence.gre2D_sequence import gre2d

    from pulserver import ir, virtual
    from pulserver.proxy import SequenceTable

    system = pp.Opts(max_grad=40, grad_unit="mT/m", max_slew=150, slew_unit="T/m/s")
    path = Path(tempfile.mkdtemp()) / "gre2d.seq"
    gre2d(system, n_x=64, n_y=64, tr=0.02, flip_angle_deg=30.0, n_dummy=0).write(path)
    ir.convert(path, system)

    phantom = virtual.Phantom(
        [
            virtual.Ellipse((-0.04, 0.0, 0.0), (0.03, 0.05), t1=0.3, t2=0.08),
            virtual.Ellipse((0.04, 0.0, 0.0), (0.03, 0.05), t1=1.5, t2=0.08),
        ]
    )








.. GENERATED FROM PYTHON SOURCE LINES 62-72

Two acquisitions
----------------

:func:`~pulserver.virtual.acquire` evaluates the phantom's analytic
transform at the k-space location of every sample: each excitation tips the
whole magnetization, whatever the flip angle and T1. The isochromats of
:meth:`~pulserver.virtual.Phantom.isochromats`, here on a 1 mm grid, are
played block by block by :func:`~pulserver.virtual.simulate` with the Bloch
equation, so the flip angle, relaxation and the approach to steady state
act on the signal.

.. GENERATED FROM PYTHON SOURCE LINES 72-75

.. code-block:: Python

    analytic = virtual.acquire(path, phantom)
    simulated = virtual.simulate(path, phantom.isochromats(1e-3))








.. GENERATED FROM PYTHON SOURCE LINES 76-78

Both return one array of samples per readout in play order; the encoding
counters of the sequence place them in k-space.

.. GENERATED FROM PYTHON SOURCE LINES 78-108

.. code-block:: Python

    lines = SequenceTable.read(path).counters["LIN"]


    def image(readouts):
        kspace = np.zeros((64, readouts[0].shape[-1]), complex)
        kspace[lines] = np.stack([readout[0] for readout in readouts])
        pixels = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift(kspace)))
        return np.abs(pixels[:, 32:96])


    images = {"acquire": image(analytic), "simulate": image(simulated)}
    inside = images["acquire"] > 0.5 * images["acquire"].max()
    short_t1, long_t1 = inside.copy(), inside.copy()
    short_t1[:, 32:], long_t1[:, :32] = False, False
    for name, pixels in images.items():
        ratio = pixels[short_t1].mean() / pixels[long_t1].mean()
        print(f"{name}: mean signal, short T1 over long T1, {ratio:.2f}")





.. image-sg:: /generated/gallery/01-course/images/sphx_glr_05_testing_on_the_virtual_scanner_001.png
   :alt: acquire, simulate
   :srcset: /generated/gallery/01-course/images/sphx_glr_05_testing_on_the_virtual_scanner_001.png
   :class: sphx-glr-single-img


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

 .. code-block:: none

    acquire: mean signal, short T1 over long T1, 1.00
    simulate: mean signal, short T1 over long T1, 3.28




.. GENERATED FROM PYTHON SOURCE LINES 109-122

The analytic acquisition shows two ellipses of equal signal: it tests the
trajectory, the encoding and the reconstruction. The Bloch simulation shows
the T1 weighting of a spoiled gradient echo at a short TR and, since the
scan plays no dummy scans, the ghosts along the phase-encoding axis that the
approach to steady state leaves. It tests the sequence's contrast.

Conversion check
----------------

:func:`~pulserver.validate.validate` compares the gradients the sequence asks
for with a second rendering. Without a recording of a scanner playing it, the
rendering is the cache's own waveforms. Agreement establishes that the
conversion kept the sequence; it does not establish that a scanner plays it.

.. GENERATED FROM PYTHON SOURCE LINES 122-125

.. code-block:: Python

    from pulserver.validate import validate

    print(validate(path, system=system))




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

 .. code-block:: none

    the sequence agrees with the ir waveforms
      gx     largest difference 9.542e-06 of 38.79 (0.00%)
      gy     largest difference 7.34e-08 of 21.35 (0.00%)
      gz     largest difference 3.35e-10 of 35.63 (0.00%)





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

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


.. _sphx_glr_download_generated_gallery_01-course_05_testing_on_the_virtual_scanner.py:

.. only:: html

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

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

      :download:`Download Jupyter notebook: 05_testing_on_the_virtual_scanner.ipynb <05_testing_on_the_virtual_scanner.ipynb>`

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

      :download:`Download Python source code: 05_testing_on_the_virtual_scanner.py <05_testing_on_the_virtual_scanner.py>`

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

      :download:`Download zipped: 05_testing_on_the_virtual_scanner.zip <05_testing_on_the_virtual_scanner.zip>`


.. only:: html

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

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