Source code for blochsim.simulators.stimulated_echo

"""The spin echo and the stimulated echo of three pulses."""

from __future__ import annotations

__all__ = ["StimulatedEchoSimulator"]

import numpy.typing as npt
import torch

from ..model import REFOCUSED, Simulator, SpinPhysics
from ..sequence import Dephase, EventAction, Readout
from ..sequence._array import as_torch


[docs] class StimulatedEchoSimulator(Simulator): """Three pulses, sampled at the spin echo of the first two and at the stimulated echo. The second pulse, ``TE / 2`` after the first, refocuses part of the transverse magnetization into a spin echo at ``TE`` and stores part of it along z, where it relaxes with T1 alone; the third pulse, ``TM`` after the second, returns that part to the transverse plane, where it rephases into the stimulated echo ``TE / 2`` later [1]_. With instantaneous pulses about x, the two samples are ``i M0 sin(a1) sin^2(a2 / 2) exp(-TE / T2)`` and ``i M0 sin(a1) sin(a2) sin(a3) exp(-TE / T2) exp(-TM / T1) / 2``, whatever the off-resonance. The transverse magnetization is spoiled after the spin echo, so the second sample is the stimulated echo alone. With one flip angle, and ``TE`` twice and ``TM`` three times its own echo time, this is the sequence BART's ``epg`` plays as Spinecho. References ---------- .. [1] Hahn, E. L., "Spin echoes", Physical Review 80.4 (1950), pp. 580-594. https://doi.org/10.1103/PhysRev.80.580 Examples -------- .. exec:: from blochsim.simulators import StimulatedEchoSimulator sequence = StimulatedEchoSimulator(flip=90.0, TE=20.0, TM=100.0) spin_echo, stimulated_echo = sequence.simulate(T1=1000.0, T2=80.0) """ model = SpinPhysics( properties={ "T1": "t1_ms", "T2": "t2_ms", "M0": "m0", "B1": "b1", "B0": "b0_hz", }, operators=REFOCUSED, ) # One order either side of the second pulse and one after the third. states = 4
[docs] def layout( self, *, flip: float | npt.ArrayLike, TE: float, TM: float, ) -> list: """Return the three pulses and the two samples. Parameters ---------- flip : float or array-like Flip angle in degrees, scalar or one per pulse. TE : float Echo time in milliseconds: twice the spacing of the first two pulses, and of the third pulse and the stimulated echo. TM : float Mixing time in milliseconds, from the second pulse to the third. Raises ------ ValueError If the third pulse would play before the spin echo, or if ``flip`` is neither scalar nor one per pulse. """ angles = torch.deg2rad(torch.atleast_1d(as_torch(flip)).to(torch.float64)) if angles.numel() == 1: angles = angles.expand(3) if angles.numel() != 3: raise ValueError("flip must be scalar or contain one value per pulse") if TM < TE / 2: raise ValueError( "the mixing time is at least half the echo time, so the third " "pulse follows the spin echo" ) echo_s, mixing_s = 1e-3 * TE, 1e-3 * TM return [ (0.0, self.operators.excitation(angles[0])), (echo_s / 2, self.operators.refocusing(angles[1])), (echo_s, Readout(action=EventAction.SPOIL_AFTER)), (echo_s / 2 + mixing_s, self.operators.excitation(angles[2])), (3 * echo_s / 4 + mixing_s, Dephase()), (echo_s + mixing_s, Readout()), ]