Source code for deepmr.bloch.model.t1t2shuffling

"""T1-T2 Shuffling simulator"""

__all__ = ["t1t2shuffling"]

import numpy as np

import dacite
from dacite import Config

from .. import blocks
from .. import ops
from . import epg


[docs]def t1t2shuffling( flip, phases, ESP, TR, T1, T2, sliceprof=False, diff=None, device="cpu", **kwargs ): """ Simulate a T1T2Shuffling Spin Echo sequence. Only single-pool for now. Parameters ---------- flip : float | np.ndarray | torch.Tensor Flip angle in ``[deg]`` of shape ``(npulses,)`` or ``(npulses, nmodes)``. phases : float | np.ndarray | torch.Tensor Refocusing angle phases in ``[deg]`` of shape ``(npulses,)`` or ``(npulses, nmodes)``. ESP : float Echo spacing in [ms]. TR : float Repetition time in [ms]. T1 : float | np.ndarray | torch.Tensor Longitudinal relaxation time for main pool in ``[ms]``. T2 : float | np.ndarray | torch.Tensor Transverse relaxation time for main pool in ``[ms]``. sliceprof : float | np.ndarray | torch.Tensor Excitation slice profile (i.e., flip angle scaling across slice). If ``False``, pulse are non selective. If ``True``, pulses are selective but ideal profile is assumed. If array, flip angle scaling along slice is simulated. Defaults to ``False``. spoil_inc : float, optional RF spoiling increment in ``[deg]``. Defaults to ``117°``. diff : str | tuple[str], optional String or tuple of strings, saying which arguments to get the signal derivative with respect to. Defaults to ``None`` (no differentation). device : str Computational device (e.g., ``cpu`` or ``cuda:n``, with ``n=0,1,2...``). Defaults to ``cpu``. Other Parameters ---------------- nstates : int, optional Maximum number of EPG states to be retained during simulation. High numbers improve accuracy but decrease performance. Defaults to ``10``. max_chunk_size : int, optional Maximum number of atoms to be simulated in parallel. High numbers increase speed and memory footprint. Defaults to ``natoms``. verbose : bool, optional If ``True``, prints execution time for signal (and gradient) calculations. Defaults to ``False``. B1 : float | np.ndarray | torch.Tensor , optional Flip angle scaling factor (``1.0 := nominal flip angle``). Defaults to ``None``. B0 : float | np.ndarray | torch.Tensor , optional Bulk off-resonance in [Hz]. Defaults to ``None`` B1Tx2 : float | np.ndarray | torch.Tensor Flip angle scaling factor for secondary RF mode (``1.0 := nominal flip angle``). Defaults to ``None``. B1phase : float | np.ndarray | torch.Tensor B1 relative phase in ``[deg]``. (``0.0 := nominal rf phase``). Defaults to ``None``. """ # constructor init_params = { "flip": flip, "phases": phases, "ESP": ESP, "TR": TR, "T1": T1, "T2": T2, "diff": diff, "device": device, **kwargs, } # get verbosity if "verbose" in init_params: verbose = init_params["verbose"] else: verbose = False # get verbosity if "asnumpy" in init_params: asnumpy = init_params["asnumpy"] else: asnumpy = True # get selectivity: if sliceprof: selective = True else: selective = False # add moving pool if required if selective and "v" in init_params: init_params["moving"] = True # excitation pulse properties exc_props = {"slice_selective": selective} if "exc_flip" in kwargs: exc_props["flip"] = kwargs["exc_flip"] else: exc_props["flip"] = 90.0 # refocusing pulse properties rf_props = {"slice_selective": selective} if np.isscalar(sliceprof) is False: rf_props["slice_profile"] = kwargs["sliceprof"] # get nlocs if "nlocs" in init_params: nlocs = init_params["nlocs"] else: if selective: nlocs = 15 else: nlocs = 1 # interpolate slice profile: if "slice_profile" in rf_props: nlocs = min(nlocs, len(rf_props["slice_profile"])) else: nlocs = 1 # assign nlocs init_params["nlocs"] = nlocs # put all properties together props = {"exc_props": exc_props, "rf_props": rf_props} # initialize simulator simulator = dacite.from_dict( T1T2Shuffling, init_params, config=Config(check_types=False) ) # run simulator if diff: # actual simulation sig, dsig = simulator(flip=flip, phases=phases, ESP=ESP, TR=TR, props=props) # flatten sig, dsig = sig.swapaxes(-1, -2), dsig.swapaxes(-1, -2) sig = sig.reshape(-1, sig.shape[-1] * sig.shape[-2]) dsig = dsig.reshape(-1, sig.shape[-1] * sig.shape[-2]) # post processing if asnumpy: sig = sig.detach().cpu().numpy() dsig = dsig.detach().cpu().numpy() # prepare info info = {"trun": simulator.trun, "tgrad": simulator.tgrad} if verbose: return sig, dsig, info else: return sig, dsig else: # actual simulation sig = simulator(flip=flip, phases=phases, ESP=ESP, TR=TR, props=props) # flatten sig, dsig = sig.swapaxes(-1, -2) sig = sig.reshape(-1, sig.shape[-1] * sig.shape[-2]) # post processing if asnumpy: sig = sig.cpu().numpy() # prepare info info = {"trun": simulator.trun} if verbose: return sig, info else: return sig
# %% utils class T1T2Shuffling(epg.EPGSimulator): """Class to simulate T1-T2 Shuffling.""" @staticmethod def sequence(flip, phases, ESP, TR, props, T1, T2, B1, states, signal): # parsing pulses and grad parameters exc_props = props["exc_props"] rf_props = props["rf_props"] # get number of frames and echoes npulses = flip.shape[0] # define preparation Exc = blocks.ExcPulse(states, B1, exc_props) # prepare RF pulse RF = blocks.ExcPulse(states, B1, rf_props) # prepare free precession period Xpre, Xpost = blocks.FSEStep(states, ESP, T1, T2) # magnetization prep states = Exc(states, exc_props["flip"]) # get recovery times rectime = TR - (npulses + 1) * ESP # T + 1 to account for fast recovery # actual sequence loop for n in range(npulses): # relax, recover and shift for half echo spacing states = Xpre(states) # apply refocusing states = RF(states, flip[n], phases[n]) # relax, recover and spoil for half echo spacing states = Xpost(states) # observe magnetization signal[n] = ops.observe(states, RF.phi) return ops.t1sat(signal * 1j, rectime, T1)