Source code for deepmr.bloch.model.memprage

"""MEMPRAGE simulator"""

__all__ = ["memprage"]

import warnings
import numpy as np
import torch

import dacite
from dacite import Config

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


[docs]def memprage( nshots, nechoes, flip, ESP, TR, T1, T2, spoil_inc=117.0, sliceprof=False, diff=None, device="cpu", TI=0.0, **kwargs ): """ Simulate a Multi-Echo Magnetization Prepared (MP) Rapid Gradient Echo sequence. Parameters ---------- nshots : int Number of pulse in the ``Inversion`` block. nechoes : int Number of echoes in the ``SPGR`` block. flip : float | np.ndarray | torch.Tensor Flip angle 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``. TI : float Inversion time in ``[ms]``. Defaults to ``None`` (no preparation). 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``. B1sqrdTau : float, optional Pulse energies in ``[uT**2 * ms]`` when ``flip = 1.0 [deg]``. global_inversion : bool, optional Assume nonselective (``True``) or selective (``False``) inversion. Defaults to ``True``. inv_B1sqrdTau : float, optional Inversion pulse energy in ``[uT**2 * ms]`` when ``flip = 1.0 [deg]``. grad_tau : float, optional Gradient lobe duration in ``[ms]``. grad_amplitude : float, optional Gradient amplitude along unbalanced direction in ``[mT / m]``. If total_dephasing is not provided, this is used to compute diffusion and flow effects. grad_dephasing : float, optional Total gradient-induced dephasing across a voxel (in grad direction). If gradient_amplitude is not provided, this is used to compute diffusion and flow effects. voxelsize : str | list | tuple | np.ndarray | torch.Tensor, optional Voxel size (``dx``, ``dy``, ``dz``) in ``[mm]``. If scalar, assume isotropic voxel. Defaults to ``None``. grad_orient : str | list | tuple | np.ndarray | torch.Tensor, optional Gradient orientation (``"x"``, ``"y"``, ``"z"`` or ``versor``). Defaults to ``"z"``. slice_orient : str | list | tuple | np.ndarray | torch.Tensor, optional Slice orientation (``"x"``, ``"y"``, ``"z"`` or ``versor``). Ignored if pulses are non-selective. Defaults to ``"z"``. 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``. T2star : float | np.ndarray | torch.Tensor Effective relaxation time for main pool in ``[ms]``. Defaults to ``None``. D : float | np.ndarray | torch.Tensor Apparent diffusion coefficient in ``[um**2 / ms]``. Defaults to ``None``. v : float | np.ndarray | torch.Tensor Spin velocity ``[cm / s]``. Defaults to ``None``. chemshift : float | np.ndarray | torch.Tensor Chemical shift for main pool in ``[Hz]``. Defaults to ``None``. T1bm : float | np.ndarray | torch.Tensor Longitudinal relaxation time for secondary pool in ``[ms]``. Defaults to ``None``. T2bm : float | np.ndarray | torch.Tensor Transverse relaxation time for main secondary in ``[ms]``. Defaults to ``None``. kbm : float | np.ndarray | torch.Tensor Nondirectional exchange between main and secondary pool in ``[Hz]``. Defaults to ``None``. weight_bm : float | np.ndarray | torch.Tensor Relative secondary pool fraction. Defaults to ``None``. chemshift_bm : float | np.ndarray | torch.Tensor Chemical shift for secondary pool in ``[Hz]``. Defaults to ``None``. kmt : float | np.ndarray | torch.Tensor Nondirectional exchange between free and bound pool in ``[Hz]``. If secondary pool is defined, exchange is between secondary and bound pools (i.e., myelin water and macromolecular), otherwise exchange is between main and bound pools. Defaults to ``None``. weight_mt : float | np.ndarray | torch.Tensor Relative bound pool fraction. Defaults to ``None``. """ # constructor init_params = { "flip": flip, "ESP": ESP, "TR": TR, "T1": T1, "T2": T2, "diff": diff, "device": device, "TI": TI, **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_exc = True else: selective_exc = False # add moving pool if required if selective_exc and "v" in init_params: init_params["moving"] = True # check for global inversion if "global_inversion" in init_params: selective_inv = not (init_params["global_inversion"]) else: selective_inv = False # check for conflicts in inversion selectivity if selective_exc is False and selective_inv is True: warnings.warn("3D acquisition - forcing inversion pulse to global.") selective_inv = False # inversion pulse properties if TI is None: inv_props = {} else: inv_props = {"slice_selective": selective_inv} if "inv_B1sqrdTau" in kwargs: inv_props["b1rms"] = kwargs["inv_B1sqrdTau"] ** 0.5 inv_props["duration"] = 1.0 # check conflicts in inversion settings if TI is None: if inv_props: warnings.warn( "Inversion not enabled - ignoring inversion pulse properties." ) inv_props = {} # excitation pulse properties rf_props = {"slice_selective": selective_exc} if "B1sqrdTau" in kwargs: inv_props["b1rms"] = kwargs["B1sqrdTau"] ** 0.5 inv_props["duration"] = 1.0 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_exc: 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 # unbalanced gradient properties grad_props = {} if "grad_tau" in kwargs: grad_props["duration"] = kwargs["grad_tau"] if "grad_dephasing" in kwargs: grad_props["total_dephasing"] = kwargs["grad_dephasing"] if "voxelsize" in kwargs: grad_props["voxelsize"] = kwargs["voxelsize"] if "grad_amplitude" in kwargs: grad_props["grad_amplitude"] = kwargs["grad_amplitude"] if "grad_orient" in kwargs: grad_props["grad_direction"] = kwargs["grad_orient"] if "slice_orient" in kwargs: grad_props["slice_direction"] = kwargs["slice_orient"] # check for possible inconsistencies: if "total_dephasing" in rf_props and "grad_amplitude" in rf_props: warnings.warn( "Both total_dephasing and grad_amplitude are provided - using the first" ) # put all properties together props = { "inv_props": inv_props, "rf_props": rf_props, "grad_props": grad_props, "nechoes": nechoes, "nshots": nshots, "spoil_inc": spoil_inc, } # initialize simulator simulator = dacite.from_dict( MEMPRAGE, init_params, config=Config(check_types=False) ) # run simulator if diff: # actual simulation sig, dsig = simulator(flip=flip, TR=TR, TI=TI, ESP=ESP, props=props) # 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, TR=TR, TI=TI, ESP=ESP, props=props) # post processing if asnumpy: sig = sig.cpu().numpy() # prepare info info = {"trun": simulator.trun} if verbose: return sig, info else: return sig
# %% utils spin_defaults = {"T2star": None, "D": None, "v": None} class MEMPRAGE(epg.EPGSimulator): """Class to simulate inversion-prepared Multi-Echo Rapid Gradient Echo.""" @staticmethod def sequence( flip, TR, TI, ESP, props, T1, T2, B1, df, weight, k, chemshift, D, v, states, signal, ): # parsing pulses and grad parameters inv_props = props["inv_props"] rf_props = props["rf_props"] grad_props = props["grad_props"] spoil_inc = props["spoil_inc"] npulses = props["nshots"] nechoes = props["nechoes"] # define preparation Prep = blocks.InversionPrep(TI, T1, T2, weight, k, inv_props) # prepare RF pulse RF = blocks.ExcPulse(states, B1, rf_props) # prepare free precession period X, XS = blocks.SSFPFidStep( states, ESP, TR, T1, T2, weight, k, chemshift, D, v, grad_props ) # initialize phase phi = 0 dphi = 0 # magnetization prep states = Prep(states) # actual sequence loop for n in range(npulses): # update phase dphi = (phi + spoil_inc) % 360.0 phi = (phi + dphi) % 360.0 # apply pulse states = RF(states, flip, phi) # relax, recover and record signal for each TE for e in range(nechoes): states = X(states) signal[n, e] = ops.observe(states, RF.phi) # relax, recover and spoil states = XS(states) return ops.susceptibility( signal, ESP * torch.arange(nechoes, device=df.device, dtype=torch.float32), df, )