Source code for dcmri.sig

from scipy.linalg import expm

import numpy as np







# TODO handle the case where F of one or more compartments is infinite
def _Mz_K(R1, v, Fw):
    if np.isscalar(R1):
        return R1 + Fw/v
    nc = np.size(R1)
    K = np.zeros((nc, nc))
    if np.isscalar(Fw):
        Fw = np.full((nc, nc), Fw) - np.diag(np.full(nc, Fw))  
    elif isinstance(Fw, list):
        Fw = np.array(Fw)
    F = np.sum(Fw, axis=0)
    for c in range(nc):
        K[c, c] = R1[c] + F[c]/v[c]
        for d in range(nc):
            if d != c:
                K[c, d] = -Fw[c, d]/v[d]
    return K


def _Mz_J(R1, v, me, j=None):
    J = np.multiply(np.multiply(R1, v), me)
    J = J if j is None else J + np.multiply(j, me)
    return J


[docs] def Mz_free(R1, T, v=1, Fw=0, j=None, n0=0, me=1): """Free longitudinal magnetization. See section :ref:`basics-relaxation-T1` for more detail. Args: R1 (array-like): Longitudinal relaxation rates in 1/sec. For a tissue with n compartments, the first dimension of R1 must be n. For a single compartment, R1 can be scalar or a 1D time-array. T (array-like): duration of free recovery. v (array-like, optional): volume fractions of the compartments. For a one-compartment tissue this is a scalar - otherwise it is an array with one value for each compartment. Defaults to 1. Fw (array-like, optional): Water flow between the compartments and to the environment, in units of mL/sec/cm3. Generally Fw must be a nxn array, where n is the number of compartments, and the off-diagonal elements Fw[j,i] are the permeability for water moving from compartment i into j. The diagonal elements Fw[i,i] quantify the flow of water from compartment i to outside. For a closed system with equal permeabilities between all compartments, a scalar value for Fw can be provided. Defaults to 0. j (array-like, optional): normalized tissue magnetization flux. j has to have the same shape as R1. Defaults to None. n0 (array-like, optional): initial relative magnetization at T=0. If this is a scalar, all compartments are assumed to have the same initial magnetization. Defaults to 0. me (array-like, optional): equilibrium magnetization of the tissue compartments. If a scalar value is provided, all compartments are assumed to have the same equilibrium magnetization. Defaults to 1. Returns: np.ndarray: Magnetization in the compartments after a time T. Example: Magnetization recovery after inversion. .. plot:: :include-source: :context: close-figs >>> import numpy as np >>> import matplotlib.pyplot as plt >>> import dcmri as dc Plot magnetization recovery for the first 10 seconds after an inversion pulse, for a closed tissue with R1 = 1 sec, and for an open tissue with equilibrium inflow and inverted inflow: >>> TI = 0.1*np.arange(100) >>> R1 = 1 >>> f = 0.5 >>> Mz = dc.Mz_free(R1, TI, n0=-1) >>> Mz_e = dc.Mz_free(R1, TI, n0=-1, Fw=f, j=f) >>> Mz_i = dc.Mz_free(R1, TI, n0=-1, Fw=f, j=-f) >>> plt.plot(TI, Mz, label='No flow', linewidth=3) >>> plt.plot(TI, Mz_e, label='Equilibrium inflow', linewidth=3) >>> plt.plot(TI, Mz_i, label='Inverted inflow', linewidth=3) >>> plt.xlabel('Inversion time (sec)') >>> plt.ylabel('Magnetization (A/cm)') >>> plt.legend() >>> plt.show() Now consider a two-compartment model, with a central compartment that has in- and outflow, and a peripheral compartment that only exchanges with the central compartment: >>> R1 = [1,2] >>> v = [0.3, 0.7] >>> PS = 0.1 >>> Fw = [[f, PS], [PS, 0]] >>> Mz = dc.Mz_free(R1, TI, v, Fw, n0=-1, j=[f, 0]) >>> plt.plot(TI, Mz[0,:], label='Central compartment', linewidth=3) >>> plt.plot(TI, Mz[1,:], label='Peripheral compartment', linewidth=3) >>> plt.xlabel('Inversion time (sec)') >>> plt.ylabel('Magnetization (A/cm)') >>> plt.legend() >>> plt.show() In DC-MRI the more usual situation is one where TI is fixed and the relaxation rates are variable due to the effect of a contrast agent. As an illustration, consider the previous result again at TI=500 msec and an R1 that is linearly declining in the central compartment and constant in the peripheral compartment: >>> TI = 0.5 >>> nt = 1000 >>> t = 0.1*np.arange(nt) >>> R1 = np.stack((1-t/np.amax(t), np.ones(nt))) >>> j = np.stack((f*np.ones(nt), np.zeros(nt))) >>> Mz = dc.Mz_free(R1, TI, v, Fw, n0=-1, j=j) >>> plt.plot(t, Mz[0,:], label='Central compartment', linewidth=3) >>> plt.plot(t, Mz[1,:], label='Peripheral compartment', linewidth=3) >>> plt.xlabel('Time (sec)') >>> plt.ylabel('Magnetization (A/cm)') >>> plt.legend() >>> plt.show() The function allows for R1 and TI to be both variable. Computing the result for 10 different TI values and extracting the result corresponding to TI=0.5 gives again the same result: >>> TI = 0.1*np.arange(10) >>> Mz = dc.Mz_free(R1, TI, v, Fw, n0=-1, j=j) >>> plt.plot(t, Mz[0,:,5], label='Central compartment', linewidth=3) >>> plt.plot(t, Mz[1,:,5], label='Peripheral compartment', linewidth=3) >>> plt.xlabel('Time (sec)') >>> plt.ylabel('Magnetization (A/cm)') >>> plt.legend() >>> plt.show() """ if not np.isscalar(T): if np.isscalar(R1): Mz = np.empty(np.size(T)) for k, Tk in enumerate(T): Mz[k] = Mz_free(R1, Tk, v, Fw, j, n0, me) else: Mz = np.empty(np.shape(R1) + (np.size(T), )) for k, Tk in enumerate(T): Mz[...,k] = Mz_free(R1, Tk, v, Fw, j, n0, me) return Mz if np.isscalar(R1): K = _Mz_K(R1, v, Fw) J = _Mz_J(R1, v, me, j=j) E = np.exp(-T*K) Kinv = 1/K M0 = n0*me*v return E*M0 + (1-E)*Kinv*J elif np.ndim(R1)==1: if np.isscalar(v): nt = np.size(R1) M = np.empty(nt) for t in range(nt): jt = None if j is None else j[t] n0t = n0 if np.isscalar(n0) else n0[t] M[t] = Mz_free(R1[t], T, v=v, Fw=Fw, j=jt, n0=n0t, me=me) return M K = _Mz_K(R1, v, Fw) J = _Mz_J(R1, v, me, j=j) E = expm(-T*K) Kinv = np.linalg.inv(K) I = np.eye(np.size(R1)) M0 = np.multiply(np.multiply(n0, me), v) return np.dot(E, M0) + np.dot(I-E, np.dot(Kinv, J)) if np.isscalar(v): raise ValueError( 'In a tissue with n compartments, v must be an n-element array.') n = np.shape(R1) R1 = np.reshape(R1, (n[0], -1)) if j is not None: j = np.reshape(j, (n[0], -1)) M = np.empty(R1.shape) for t in range(R1.shape[1]): jt = None if j is None else j[:,t] if np.ndim(n0)==2: n0t = n0[:,t] else: n0t = n0 M[:,t] = Mz_free(R1[:,t], T, v=v, Fw=Fw, j=jt, n0=n0t, me=me) return M.reshape(n)
[docs] def Mz_ss(R1, TR, FA, v=1, Fw=0, j=None, me=1) -> np.ndarray: """Steady-state longitudinal tissue magnetization. See section :ref:`basics-relaxation-T1` for more detail. Args: R1 (array-like): Longitudinal relaxation rates in 1/sec. For a tissue with n compartments, the first dimension of R1 must be n. For a single compartment, R1 can be scalar or a 1D time-array. TR (float): repetition time. FA (float): flip angle. v (array-like, optional): volume fractions of the compartments. For a one-compartment tissue this is a scalar - otherwise it is an array with one value for each compartment. Defaults to 1. Fw (array-like, optional): Water flow between the compartments and to the environment, in units of mL/sec/cm3. Generally Fw must be a nxn array, where n is the number of compartments, and the off-diagonal elements Fw[j,i] are the permeability for water moving from compartment i into j. The diagonal elements Fw[i,i] quantify the flow of water from compartment i to outside. For a closed system with equal permeabilities between all compartments, a scalar value for Fw can be provided. Defaults to 0. j (array-like, optional): normalized tissue magnetization flux. j has to have the same shape as R1. Defaults to None. me (array-like, optional): equilibrium magnetization of the tissue compartments. If a scalar value is provided, all compartments are assumed to have the same equilibrium magnetization. Defaults to 1. Returns: np.ndarray: Magnetization in the compartments after a time T. Example: Compute steady-state magnetization with inflow magnetization (mi) ranging from fully inverted to equilibrium. Compare against magnetization of an isolated tissue without flow. .. plot:: :include-source: :context: close-figs Import packages: >>> import numpy as np >>> import matplotlib.pyplot as plt >>> import dcmri as dc Define constants: >>> FA, TR = 12, 0.005 >>> R1 = 1 >>> f, v = 0.5, 0.7 >>> mi = np.linspace(-1, 1, 100) Compute magnetization without/with inflow: >>> m_c = dc.Mz_ss(R1, TR, FA, v)/v >>> m_f = [dc.Mz_ss(R1, TR, FA, v, f, j=f*m)/v for m in mi] Plot the results: >>> plt.plot(mi, mi*0+m_c, label='No inflow', linewidth=3) >>> plt.plot(mi, m_f, label='Inflow', linewidth=3) >>> plt.xlabel('Inflow magnetization (A/cm/mL)') >>> plt.ylabel('Steady-state magnetization (A/cm/mL)') >>> plt.legend() >>> plt.show() Note the magnetization of the two tissues is the same when the inflow is at the steady-state of the isolated tissue: >>> m_f = dc.Mz_ss(R1, TR, FA, v, f, j=f*m_c)/v >>> print(m_f - m_c) 0.00023950879616352339 Now we consider the same situation again, this time for a two- compartment tissue with one central compartment that exchanges with the enviroment. >>> R1 = [0.5, 1.5] >>> v = [0.3, 0.6] >>> PS = 0.1 Compute magnetization without inflow: >>> Fw = [[0, PS], [PS, 0]] >>> M_c = dc.Mz_ss(R1, TR, FA, v, Fw) Compute magnetization with flow through the first compartment: >>> Fw = [[f, PS], [PS, 0]] >>> M_f = np.zeros((2, 100)) >>> for i, m in enumerate(mi): >>> M_f[:,i] = dc.Mz_ss(R1, TR, FA, v, Fw, j=[f*m, 0]) Plot the results for the central compartment: >>> plt.plot(mi, M_c[0]/v[0] + mi*0, label='No inflow', linewidth=3) >>> plt.plot(mi, M_f[0,:]/v[0], label='Inflow', linewidth=3) >>> plt.xlabel('Inflow magnetization (A/cm/mL)') >>> plt.ylabel('Steady-state magnetization (A/cm/mL)') >>> plt.legend() >>> plt.show() We can verify again that the magnetization is the same as that of the isolated tissue when the inflow is at the isolated steady-state: >>> m_c = M_c[0]/v[0] >>> M_f = dc.Mz_ss(R1, TR, FA, v, Fw=f, j=f*m_c) >>> print(M_f[0]/v[0] - m_c) 0.0002984954839493209 """ # One compartment if np.isscalar(R1): return me*_Nz_ss_1c(R1, TR, FA, v, Fw, j) elif np.ndim(R1)==1: if np.isscalar(v): nt = np.size(R1) M = np.empty(nt) for t in range(nt): jt = None if j is None else j[t] M[t] = Mz_ss(R1[t], TR, FA, v, Fw, jt, me) return M return me*_Nz_ss(R1, TR, FA, v, Fw, j) if np.isscalar(v): raise ValueError( 'In a tissue with n compartments, v must be an n-element array.') n = np.shape(R1) R1 = np.reshape(R1, (n[0], -1)) if j is not None: j = np.reshape(j, (n[0], -1)) M = np.empty(R1.shape) for t in range(R1.shape[1]): jt = None if j is None else j[:,t] M[:,t] = Mz_ss(R1[:,t], TR, FA, v, Fw, jt, me) return M.reshape(n)
def _Nz_ss_1c(R1, TR, FA, v, Fw=0, j=None): K = R1 + Fw/v J = R1*v if j is None else R1*v + j E = np.exp(-np.multiply(TR, K)) cFA = np.cos(FA*np.pi/180) n = (1-E) / (1-cFA*E) if K==0: return n else: return n * J/K def _Nz_ss(R1, TR, FA, v, Fw=0, j=None): nc = np.size(R1) # Closed tissue with constant permeabilities if np.isscalar(Fw): if Fw == np.inf: return _Nz_ss_fex(R1, TR, FA, v) elif Fw == 0: return _Nz_ss_nex(R1, TR, FA, v) else: Fw = np.full((nc, nc), Fw) - np.diag(np.full(nc, Fw)) return _Nz_ss_aex(R1, TR, FA, v, Fw, j) # Open tissue if np.shape(Fw) != (nc, nc): raise ValueError('Fw must be a square array with size equal to the ' 'number of compartments.') PSw = np.array(Fw) - np.diag(np.diag(Fw)) ninf = np.count_nonzero(np.isinf(PSw)) if np.linalg.norm(PSw) == 0: return _Nz_ss_nex(R1, TR, FA, v, Fw, j) elif ninf == nc*nc-nc: return _Nz_ss_fex(R1, TR, FA, v, Fw, j) elif 0 < ninf < nc*nc-nc: raise NotImplementedError( 'Water exchange with some (but not all) infinite PS ' 'values is currently not implemented.') else: return _Nz_ss_aex(R1, TR, FA, v, Fw, j) def _Nz_ss_fex(R1, TR, FA, v, Fw=0, j=None): R1 = np.sum(np.multiply(v, R1)) / np.sum(v) if j is None: N = _Nz_ss_1c(R1, TR, FA, np.sum(v)) else: fo = np.diag(Fw) N = _Nz_ss_1c(R1, TR, FA, np.sum(v), np.sum(fo), np.sum(j)) return np.stack([N*vc/np.sum(v) for vc in v]) def _Nz_ss_nex(R1, TR, FA, v, Fw=0, j=None): if j is None: N = [_Nz_ss_1c(R1[c], TR, FA, v[c]) for c in range(np.size(v))] else: N = [] fo = np.diag(Fw) for c in range(np.size(v)): Nc = _Nz_ss_1c(R1[c], TR, FA, v[c], fo[c], j[c]) N.append(Nc) return np.stack(N) def _Nz_ss_aex(R1, TR, FA, v, Fw=0, j=None): K = _Mz_K(R1, v, Fw) J = _Mz_J(R1, v, 1, j=j) E = expm(-TR*K) I = np.eye(np.size(R1)) cFA = np.cos(FA*np.pi/180) A = np.dot(K, I-cFA*E) Ainv = np.linalg.inv(A) return np.dot(Ainv, np.dot(I-E, J))
[docs] def Mz_spgr(R1, T, TR, FA, v=1, Fw=0, j=None, n0=0, me=1): """Longitudinal tissue magnetization of a spoiled gradient-echo sequence. See section :ref:`basics-relaxation-T1` for more detail. Args: R1 (array-like): Longitudinal relaxation rates in 1/sec. For a tissue with n compartments, the first dimension of R1 must be n. For a single compartment, R1 can be scalar or a 1D time-array. T (float): time since the first rf-pulse. TR (float): repetition time between rf-pulses. FA (float): flip angle of the rf-pulse. v (array-like, optional): volume fractions of the compartments. For a one-compartment tissue this is a scalar - otherwise it is an array with one value for each compartment. Defaults to 1. Fw (array-like, optional): Water flow between the compartments and to the environment, in units of mL/sec/cm3. Generally Fw must be a nxn array, where n is the number of compartments, and the off-diagonal elements Fw[j,i] are the permeability for water moving from compartment i into j. The diagonal elements Fw[i,i] quantify the flow of water from compartment i to outside. For a closed system with equal permeabilities between all compartments, a scalar value for Fw can be provided. Defaults to 0. j (array-like, optional): normalized tissue magnetization flux. j has to have the same shape as R1. Defaults to None. n0 (array-like, optional): initial relative magnetization at T=0. If this is a scalar, all compartments are assumed to have the same initial magnetization. Defaults to 0. me (array-like, optional): equilibrium magnetization of the tissue compartments. If a scalar value is provided, all compartments are assumed to have the same equilibrium magnetization. Defaults to 1. Returns: np.ndarray: Tissue magnetization in the compartments after a time T. Example: Compare SPGR tissue magnetization of a two-compartment tissue after inversion against free recovery and steady-state: .. plot:: :include-source: :context: close-figs Import packages: >>> import numpy as np >>> import matplotlib.pyplot as plt >>> import dcmri as dc Define constants: >>> FA, TR = 12, 0.005 >>> TI = np.linspace(0,3,100) >>> R1 = [1, 0.5] >>> v = [0.3, 0.7] >>> f, PS = 0.5, 0.1 >>> Fw = [[f, PS], [PS, 0]] Compute magnetization: >>> Mspgr = dc.Mz_spgr(R1, TI, TR, FA, v, Fw, j=[f, 0], n0=-1) >>> Mfree = dc.Mz_free(R1, TI, v, Fw, j=[f, 0], n0=-1) >>> Mss = dc.Mz_ss(R1, TR, FA, v, Fw, j=[f, 0]) Plot the results for the peripheral compartment: >>> c = 1 >>> plt.title('Peripheral compartment') >>> plt.plot(TI, Mfree[c,:], label='Free', linewidth=3) >>> plt.plot(TI, v[c]+0*TI, label='Equilibrium', linewidth=3) >>> plt.plot(TI, Mspgr[c,:], label='SPGR', linewidth=3) >>> plt.plot(TI, Mss[c]+0*TI, label='Steady-state', linewidth=3) >>> plt.xlabel('Time since start of pulse sequence (sec)') >>> plt.ylabel('Tissue magnetization (A/cm/cm3)') >>> plt.legend() >>> plt.show() This verifies that the free recovery magnetization goes to equilibrium, and that the SPGR magnetization relaxes to the steady state at a shorter time than the free recovery. """ if not np.isscalar(T): if np.isscalar(R1): M = np.empty(np.size(T)) for k, Tk in enumerate(T): M[k] = Mz_spgr(R1, Tk, TR, FA, v, Fw, j, n0, me) else: M = np.empty(np.shape(R1) + (np.size(T), )) for k, Tk in enumerate(T): M[...,k] = Mz_spgr(R1, Tk, TR, FA, v, Fw, j, n0, me) return M if np.isscalar(R1): nx = T/TR ncFA = np.cos(FA*np.pi/180)**nx M0 = n0*v*me Mss = Mz_ss(R1, TR, FA, v, Fw, j, me) K = _Mz_K(R1, v, Fw) E = np.exp(-T*K) return Mss + ncFA*E*(M0-Mss) elif np.ndim(R1)==1: if np.isscalar(v): nt = np.size(R1) M = np.empty(nt) for t in range(nt): jt = None if j is None else j[t] n0t = n0 if np.isscalar(n0) else n0[t] M[t] = Mz_spgr(R1[t], T, TR, FA, v, Fw, jt, n0t, me) return M nx = T/TR ncFA = np.cos(FA*np.pi/180)**nx M0 = np.multiply(np.multiply(n0, v), me) Mss = Mz_ss(R1, TR, FA, v, Fw, j, me) K = _Mz_K(R1, v, Fw) E = expm(-T*K) return Mss + ncFA*np.dot(E, M0-Mss) if np.isscalar(v): raise ValueError( 'In a tissue with n compartments, v must be an n-element array.') n = np.shape(R1) R1 = np.reshape(R1, (n[0], -1)) if j is not None: j = np.reshape(j, (n[0], -1)) M = np.empty(R1.shape) for t in range(R1.shape[1]): jt = None if j is None else j[:,t] if np.ndim(n0)==2: n0t = n0[:,t] else: n0t = n0 M[:,t] = Mz_spgr(R1[:,t], T, TR, FA, v, Fw, jt, n0t, me) return M.reshape(n)
def signal(sequence, R1, S0, TR=None, FA=None, TC=None): # Private for now - generalize to umbrella signal function if sequence == 'SRC': return signal_free(S0, R1, TC, FA) if sequence == 'SR': return signal_spgr(S0, R1, TC, TR, FA) elif sequence == 'SS': return signal_ss(S0, R1, TR, FA) else: raise ValueError( 'Sequence ' + str(sequence) + ' is not recognised.')
[docs] def signal_dsc(R1, R2, S0: float, TR, TE) -> np.ndarray: """Signal model for a DSC scan with T2 and T2-weighting. Args: R1 (array-like): Longitudinal relaxation rate in 1/sec. Must have the same size as R2. R2 (array-like): Transverse relaxation rate in 1/sec. Must have the same size as R1. S0 (float): Signal scaling factor (arbitrary units). TR (array-like): Repetition time, or time between successive selective excitations, in sec. If TR is an array, it must have the same size as R1 and R2. TE (array-like): Echo time, in sec. If TE is an array, it must have the same size as R1 and R2. Returns: np.ndarray: Signal in arbitrary units, same length as R1 and R2. """ return S0*np.exp(-np.multiply(TE, R2))*(1-np.exp(-np.multiply(TR, R1)))
[docs] def signal_t2w(R2, S0: float, TE) -> np.ndarray: """Signal model for a DSC scan with T2-weighting. Args: R2 (array-like): Transverse relaxation rate in 1/sec. Must have the same size as R1. S0 (float): Signal scaling factor (arbitrary units). TE (array-like): Echo time, in sec. If TE is an array, it must have the same size as R1 and R2. Returns: np.ndarray: Signal in arbitrary units, same length as R1 and R2. """ return S0*np.exp(-np.multiply(TE, R2))
[docs] def signal_free(S0, R1, T, FA, v=1, Fw=0, j=None, n0=0, R10=None): """Signal with readout after a free recovery. Args: S0 (float): Signal scaling factor (arbitrary units). R1 (array-like): Longitudinal relaxation rates in 1/sec. For a tissue with n compartments, the first dimension of R1 must be n. For a tissue with a single compartment, R1 can have any shape. If R1 is a scalar, the result for a well-mixed tissue is returned. T (float): time to center of k-space FA (float): flip angle v (array-like, optional): volume fractions of the compartments. For a one-compartment tissue this is a scalar - otherwise it is an array with one value for each compartment. Defaults to 1. Fw (array-like, optional): Water flow between the compartments and to the environment, in units of mL/sec/cm3. Generally Fw must be a nxn array, where n is the number of compartments, and the off-diagonal elements Fw[j,i] are the permeability for water moving from compartment i into j. The diagonal elements Fw[i,i] quantify the flow of water from compartment i to outside. For a closed system with equal permeabilities between all compartments, a scalar value for Fw can be provided. Defaults to 0. j (array-like, optional): normalized tissue magnetization flux. j has to have the same shape as R1. Defaults to None. n0 (array-like, optional): initial relative magnetization at T=0. If this is a scalar, all compartments are assumed to have the same initial magnetization. Defaults to 0. R10 (float, optional): R1-value where S0 is defined. If not provided, S0 is the scaling factor corresponding to infinite R10. Defaults to None. Returns: np.ndarray: Signal in the same units as S0 and with the same dimensions as R1. """ if R10 is not None: S0 = S0/signal_free(1, R10, T, FA, v, Fw, j, n0) Mz = Mz_free(R1, T, v, Fw, j, n0) if np.isscalar(Mz): return S0 * np.sin(FA*np.pi/180) * np.abs(Mz) elif np.ndim(Mz)==1: if np.isscalar(v): return S0 * np.sin(FA*np.pi/180) * np.abs(Mz) else: return S0 * np.sin(FA*np.pi/180) * np.abs(np.sum(Mz)) else: return S0 * np.sin(FA*np.pi/180) * np.abs(np.sum(Mz, axis=0))
[docs] def signal_ss(S0, R1, TR, FA, v=1, Fw=0, j=None, R10=None) -> np.ndarray: """Signal of a spoiled gradient echo sequence applied in steady state. Args: S0 (float): Signal scaling factor (arbitrary units). R1 (array-like): Longitudinal relaxation rates in 1/sec. For a tissue with n compartments, the first dimension of R1 must be n. For a tissue with a single compartment, R1 can have any shape. If R1 is a scalar, the result for a well-mixed tissue is returned. TR (float): Repetition time, or time between successive selective excitations, in sec. FA (float): Flip angle in degrees. v (array-like, optional): volume fractions of the compartments. For a one-compartment tissue this is a scalar - otherwise it is an array with one value for each compartment. Defaults to 1. Fw (array-like, optional): Water flow between the compartments and to the environment, in units of mL/sec/cm3. Generally Fw must be a nxn array, where n is the number of compartments, and the off-diagonal elements Fw[j,i] are the permeability for water moving from compartment i into j. The diagonal elements Fw[i,i] quantify the flow of water from compartment i to outside. For a closed system with equal permeabilities between all compartments, a scalar value for Fw can be provided. Defaults to 0. j (array-like, optional): normalized tissue magnetization flux. j has to have the same shape as R1. Defaults to None. R10 (float, optional): R1-value where S0 is defined. If not provided, S0 is the scaling factor corresponding to infinite R10. Defaults to None. Returns: np.ndarray: Signal in the same units as S0 """ if R10 is not None: S0 = S0/signal_ss(1, R10, TR, FA, v, Fw, j) Mz = Mz_ss(R1, TR, FA, v, Fw, j) if np.isscalar(Mz): return S0 * np.sin(FA*np.pi/180) * np.abs(Mz) elif np.ndim(Mz)==1: if np.isscalar(v): return S0 * np.sin(FA*np.pi/180) * np.abs(Mz) else: return S0 * np.sin(FA*np.pi/180) * np.abs(np.sum(Mz)) else: return S0 * np.sin(FA*np.pi/180) * np.abs(np.sum(Mz, axis=0))
[docs] def signal_spgr(S0, R1, T, TR, FA, TP=0.0, v=1, Fw=0, j=None, n0=0, R10=None) -> np.ndarray: """Signal of an SPGR sequence. Args: S0 (float): Signal scaling factor (arbitrary units). R1 (array-like): Longitudinal relaxation rate in 1/sec. T (float): time since the first readout rf-pulse. TR (float): Repetition time, or time between successive selective excitations, in sec. FA (array-like): Flip angle in degrees. TP (float, optional): Time (sec) between the preparation pre-pulse and the first readout pulse. Defaults to 0. v (array-like, optional): volume fractions of the compartments. For a one-compartment tissue this is a scalar - otherwise it is an array with one value for each compartment. Defaults to 1. Fw (array-like, optional): Water flow between the compartments and to the environment, in units of mL/sec/cm3. Generally Fw must be a nxn array, where n is the number of compartments, and the off-diagonal elements Fw[j,i] are the permeability for water moving from compartment i into j. The diagonal elements Fw[i,i] quantify the flow of water from compartment i to outside. For a closed system with equal permeabilities between all compartments, a scalar value for Fw can be provided. Defaults to 0. j (array-like, optional): normalized tissue magnetization flux. j has to have the same shape as R1. Defaults to None. n0 (array-like, optional): initial relative magnetization at T=0. If this is a scalar, all compartments are assumed to have the same initial magnetization. Defaults to 0. R10 (float, optional): R1-value where S0 is defined. If not provided, S0 is the scaling factor corresponding to infinite R10. Defaults to None. Raises: ValueError: If TP is larger than TC. Returns: np.ndarray: Signal in arbitrary units, of the same length as R1. """ if R10 is not None: S0 = S0/signal_spgr(1, R10, T, TR, FA, TP, v, Fw, j, n0) if TP>0: N0 = Mz_free(R1, TP, v, Fw, j, n0) if np.ndim(N0)==2: n0 = N0 for t in range(N0.shape[1]): n0[:,t] = np.divide(N0[:,t], v) else: n0 = np.divide(N0, v) Mz = Mz_spgr(R1, T, TR, FA, v, Fw, j, n0) if np.isscalar(Mz): return S0 * np.sin(FA*np.pi/180) * np.abs(Mz) elif np.ndim(Mz)==1: if np.isscalar(v): return S0 * np.sin(FA*np.pi/180) * np.abs(Mz) else: return S0 * np.sin(FA*np.pi/180) * np.abs(np.sum(Mz)) else: return S0 * np.sin(FA*np.pi/180) * np.abs(np.sum(Mz, axis=0))
[docs] def signal_lin(R1, S0: float) -> np.ndarray: """Signal for any sequence operating in the linear regime. Args: R1 (array-like): Longitudinal relaxation rate in 1/sec. S0 (float): Signal scaling factor (arbitrary units). Returns: np.ndarray: Signal in arbitrary units, of the same length as R1. """ return S0 * R1
[docs] def conc_t2w(S, TE: float, r2=0.5, n0=1) -> np.ndarray: """Concentration for a DSC scan with T2-weighting. Args: S (array-like): Signal in arbitrary units. TE (float): Echo time in sec. r2 (float, optional): Transverse relaxivity in Hz/M. Defaults to 0.5. n0 (int, optional): Baseline length. Defaults to 1. Returns: np.ndarray: Concentration in M, same length as S. """ # S/Sb = exp(-TE(R2-R2b)) # ln(S/Sb) = -TE(R2-R2b) # R2-R2b = -ln(S/Sb)/TE # R2 = R2b + r2C # C = (R2-R2b)/r2 # C = -ln(S/Sb)/TE/r2 Sb = np.mean(S[:n0]) C = -np.log(S/Sb)/TE/r2 return C
[docs] def conc_ss(S, TR: float, FA: float, T10: float, r1=0.005, n0=1) -> np.ndarray: """Concentration of a spoiled gradient echo sequence applied in steady state. Args: S (array-like): Signal in arbitrary units. TR (float): Repetition time, or time between successive selective excitations, in sec. FA (float): Flip angle in degrees. T10 (float): baseline T1 value in sec. r1 (float, optional): Longitudinal relaxivity in Hz/M. Defaults to 0.005. n0 (int, optional): Baseline length. Defaults to 1. Returns: np.ndarray: Concentration inM , same length as S. """ # S = Sinf * (1-exp(-TR*R1)) / (1-cFA*exp(-TR*R1)) # Sb = Sinf * (1-exp(-TR*R10)) / (1-cFA*exp(-TR*R10)) # Sn = (1-exp(-TR*R1)) / (1-cFA*exp(-TR*R1)) # Sn * (1-cFA*exp(-TR*R1)) = 1-exp(-TR*R1) # exp(-TR*R1) - Sn *cFA*exp(-TR*R1) = 1-Sn # (1-Sn*cFA) * exp(-TR*R1) = 1-Sn Sb = np.mean(S[:n0]) E0 = np.exp(-TR/T10) c = np.cos(FA*np.pi/180) Sn = (S/Sb)*(1-E0)/(1-c*E0) # normalized signal # Replace any Nan values by interpolating between nearest neighbours outrange = Sn >= 1 if np.sum(outrange) > 0: inrange = Sn < 1 x = np.arange(Sn.size) Sn[outrange] = np.interp(x[outrange], x[inrange], Sn[inrange]) R1 = -np.log((1-Sn)/(1-c*Sn))/TR # relaxation rate in 1/msec return (R1 - 1/T10)/r1
[docs] def conc_src(S, TC: float, T10: float, r1=0.005, n0=1) -> np.ndarray: """Concentration of a saturation-recovery sequence with a center-encoded readout. Args: S (array-like): Signal in arbitrary units. TC (float): Time (sec) between the saturation pulse and the acquisition of the k-space center. T10 (float): baseline T1 value in sec. r1 (float, optional): Longitudinal relaxivity in Hz/M. Defaults to 0.005. n0 (int, optional): Baseline length. Defaults to 1. Returns: np.ndarray: Concentration in M, same length as S. Example: We generate some signals from ground-truth concentrations, then reconstruct the concentrations and check against the ground truth: .. plot:: :include-source: >>> import matplotlib.pyplot as plt >>> import numpy as np >>> import dcmri as dc First define some constants: >>> T10 = 1 # sec >>> TC = 0.2 # sec >>> r1 = 0.005 # Hz/M >>> FA = 15 # deg Generate ground truth concentrations and signal data: >>> t = np.arange(0, 5*60, 0.1) # sec >>> C = 0.003*(1-np.exp(-t/60)) # M >>> R1 = 1/T10 + r1*C # Hz >>> S = dc.signal_free(100, R1, TC, FA) # au Reconstruct the concentrations from the signal data: >>> Crec = dc.conc_src(S, TC, T10, r1) Check results by plotting ground truth against reconstruction: >>> plt.plot(t/60, 1000*C, 'ro', label='Ground truth') >>> plt.plot(t/60, 1000*Crec, 'b-', label='Reconstructed') >>> plt.title('SRC signal inverse') >>> plt.xlabel('Time (min)') >>> plt.ylabel('Concentration (mM)') >>> plt.legend() >>> plt.show() """ # S = S0*(1-exp(-TC*R1)) # S/Sb = (1-exp(-TC*R1))/(1-exp(-TC*R10)) # (1-exp(-TC*R10))*S/Sb = 1-exp(-TC*R1) # 1-(1-exp(-TC*R10))*S/Sb = exp(-TC*R1) # ln(1-(1-exp(-TC*R10))*S/Sb) = -TC*R1 # -ln(1-(1-exp(-TC*R10))*S/Sb)/TC = R1 Sb = np.mean(S[:n0]) E = np.exp(-TC/T10) R1 = -np.log(1-(1-E)*S/Sb)/TC return (R1 - 1/T10)/r1
[docs] def conc_lin(S, T10, r1=0.005, n0=1): """Concentration for any sequence operating in the linear regime. Args: S (array-like): Signal in arbitrary units. T10 (float): baseline T1 value in sec. r1 (float, optional): Longitudinal relaxivity in Hz/M. Defaults to 0.005. n0 (int, optional): Baseline length. Defaults to 1. Returns: np.ndarray: Concentration in M, same length as S. """ Sb = np.mean(S[:n0]) R10 = 1/T10 R1 = R10*S/Sb # relaxation rate in 1/msec return (R1 - R10)/r1